Circadian Lighting

Updated 03 December 2021

After experimenting with automatic blue light filters on my electronic devices I wanted to replicate the idea with the lighting around the house so that it would gradually transition to warmer colours as the night progresses. I believe this kind of feature does exist in some commercial lights, but I did not want to spend heaps of money and also wanted to make it customisable so that I could add things like animations later down the track.

Bill of Materials

A plant in front of a wall with a variety of sunset colours

Setup

The setup for this project mimics the setup that was explained in the tutorial for controlling a strip of ws2812 LEDs with a Raspberry Pi by Core Electronics. If you would like to use the raspberry pi without a monitor, keyboard, and mouse, here is a tutorial on how to do so for Windows users. Otherwise, you can just set up SSH and connect via port 22 on macOS and Linux distros ask on the Core Electronics forum if you require assistance, there's a couple tutorials linked on there.

Automatic Blue Light Filter

Changing the LED strip colour to the specific RGB values

Before we start considering anything to do with 'warm' and 'cool' colours, we want to first be able to display a specific colour on the led strips. I am going to assume that you have set up the led strips and have been able to get the python examples to work without any issues. There will be a link at the start of this tutorial to a useful page showing you how to do so.

First, we need to import some things from the rpi_ws281x library:

from rpi_ws281x import PixelStrip, Color

Next, we need to define a bunch of constants that are required to instantiate (basic setup) the led strip object.

# LED strip configuration:
LED_COUNT = 120       # Number of LED pixels.
LED_PIN = 18          # GPIO pin connected to the pixels (18 uses PWM!).
# LED_PIN = 10        # GPIO pin connected to the pixels (10 uses SPI /dev/spidev0.0).
LED_FREQ_HZ = 800000  # LED signal frequency in hertz (usually 800khz)
LED_DMA = 10          # DMA channel to use for generating a signal (try 10)
LED_BRIGHTNESS = 255  # Set to 0 for darkest and 255 for brightest
LED_INVERT = False    # True to invert the signal (when using NPN transistor-level shift)
LED_CHANNEL = 0       # set to '1' for GPIOs 13, 19, 41, 45 or 53

# Create NeoPixel object with appropriate configuration.
pixels = PixelStrip(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL)
# Intialize the library (must be called once before other functions).
pixels.begin()

Don't worry if you do not understand some of the comments, they are taken from an example in the rpi_ws218x library. We only need to change the LED_COUNT variable and the LED_PIN variable for the moment. You can play around with changing the LED_BRIGHTNESS variable to your preference.

The LED_COUNT variable should be set to the total number of led's in your led strip. The LED_PIN variable should be set to the GPIO pin that you connected the led strip to on the raspberry pi. For example, If you used GPIO 18, it should be set to 18.

Next, how to display a colour across the whole led strip:

def display_colour(red, green, blue):
    for i in range(strip.numPixels()):
        strip.setPixelColor(i, Color(red, green, blue))
        strip.show()

display_colour(200, 50, 0)

Basically, we just have to loop through each individual led and set it to the colour that was specified. strip.show() will display what is in the buffer to the pixels, so we can call it in each iteration of the for loop to get the effect of the new colour '

sweeping' across the led strips. Or, if you want all of the led pixels to update at once, just move strip.show() to after the for loop.

 

Converting colour temperatures from Kelvin (K) to RGB

First, we want to consider how we go about finding the RGB values of 'cool' and 'warm' colours. We use 'Colour Temperature' as a way of describing how cool or warm a colour is. Cooler colours are more blueish, and warmer colours are more reddish. The Colour Temperature is measured in Kelvins (K) which is a measurement for temperature just like Fahrenheit or Celsius but is typically used for more scientific contexts. Feel free to research more about colour temperature, however, it is not necessary for understanding this project. I found this useful page with some pseudo-code for how to convert from colour temperature to RGB values.

My python version of the pseudo-code from the above link:

This algorithm will give you a level of precision that is complete overkill for our application. It will give you values to a precision of 10 decimal places. This is unnecessary for any typical led strip, which only allows for integer values between 0 - 255 for the red, green, and blue values. However, it is a very very small amount of work for the pi zero so I have left the algorithm intact (apart from adding round()).

import math # we need the math library to calculate logarithms

def clamp(n, minn, maxn):
    # function for limiting a number to a specified number range
    return max(min(maxn, n), minn)

def convertTempToRGB(temp):
    # Function for converting a colour temperature in Kelvin (K) to RGB
    # Algorithm from: https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html

    temp = temp / 100
    red = 0
    green = 0
    blue = 0

    # RED
    if temp <= 66:
        red = 255
    else:
        red = temp - 60
        red = 329.698727446 * (red ** -0.1332047592)
        red = clamp(red, 0, 255)

    # GREEN
    if temp <= 66:
        green = temp
        green = 99.4708025861 * math.log(green) - 161.1195681661
        green = clamp(green, 0, 255)
    else:
        green = temp - 60
        green = 288.1221695283 * (green ** -0.0755148492)
        green = clamp(green, 0, 255)

    # BLUE
    if temp >= 66:
        blue = 255
    else:
        if temp <= 19:
            blue = 0
        else:
            blue = temp - 10
            blue = 138.5177312231 * math.log(blue) - 305.0447927307
            blue = clamp(blue, 0, 255)
           
    # round() is used because the leds only require integer values. Can be omitted for other use cases
    return (round(red), round(green), round(blue))

 

Choosing Colour temperature range

The next thing to consider is the range of colours that the led strips will transition through during the evening.

Some example temperature ranges shown as a gradient (the first value is the top, the second value is the bottom):

10000K to 700K              8000K to 2000K              4000K to 600K           
10000K-to-700K-temperature-range 80000K-to-2000K-temperature-range 4000K-to-6000K-temperature-range

One thing to bear in mind is that these will look very different on a set of led strips than on a computer screen, so you will have to experiment with what looks better on the led strips. For example, without any kind of colour correction, the range shown in the image on the right appears to all be a blend of orange and red but the values from about 4000K to 2500K appear as mainly white on the LEDs

You can test how different colour temperatures will look on your led strip with the functions we have created so far. For example, adding this at the end of the file:

red, green, blue = convertTempToRGB(1000)  # change 1000 to any colour temp 
displayColour(red, green, blue)

 

Getting the current time

This can be done easily with python using the datetime library in python. You may also need to adjust the time zone of your raspberry pi to get the correct time for your location. You can find how to do that here: https://raspberrytips.com/time-sync-raspberry-pi/

import datetime

def get_current_time():
    time = datetime.datetime.now()
    hour = time.hour
    minute = time.minute
    return hour, minute

We are storing the time as a tuple (basically two separate values for hour and minute stored together) so that it is easier to work with later.

 

Choosing the transitioning function

Now we have to figure out how we want the colour temperature to transition as the time progresses. We can choose from a variety of different mathematical functions to get the right transition that we want.

Inverse Tangent (Arctan)

 arctan-in-geogebra

arctan-on-top-of-temperature-scale

arctan_geogebra

arctan_bg

Originally this was my preference because it does the bulk of colour changing around the sunset time but still changes slightly at other hours. However, when trying to extend this curve so that it could be used for sunrise as well and have a smooth transition over a full 24 hour period, it got more complicated.

The only solution I could think of was to basically stitch together 4 arctan graphs each with slightly different horizontal translations (how much they are shifted left or right), and then depending on the hour, pick one of the 4 graphs. This gave a smooth transition over a 24 hour period at the expense of some confusing code and the fact that the temperatures won't actually reach the Min or Max temperatures.

The alternative to that was to do a similar thing but with a sine graph. Using a section of a sine graph for sunrise and the reverse for sunset, and for any hours in between, the colour temperature will be either the minimum (night time) or the maximum (daytime).

The two methods both had their merits and I couldn't decide which to use in the project so I just included both in the final code.

Sine graph

A typical sine graph looks like this...

sin-graph

We want some sections of the graph to be relatively flat (for during the day and during night), so we can cut and paste some sections of a sine graph and some straight lines and get something like this:

trimmed-sin-graph

If we apply the right dilations to make it useful for our purpose we get something like this:

edited-function

Colour temperature is the vertical axis and hour of the day is on the horizontal axis.

The equation written in maths is below:

equations-used-to-plot-temperature

  • Max is the maximum colour temperature or daytime colour temperature.
  • Min is the minimum colour temperature or night time colour temperature.
  • sunrise is the sunrise time
  • sunset is the sunset time
  • hours is the number of hours during sunset or sunrise that we want the colour changing to occur for. e.g, 2 hours

The python version of this:

def get_current_temp(time, Max=4000, Min=650, hours=2, sunset=(20, 0), sunrise=(6, 0)):
    time = time[0] + time[1]/60
    sunset = sunset[0] + sunset[1]/60
    sunrise = sunrise[0] + sunrise[1]/60

    n = math.pi / hours

    if time < sunrise - hours/2:
        return Min
    elif time < sunrise + hours/2:
        return ((Max - Min) / 2) * math.sin(n * (time - sunrise)) + ((Max + Min) / 2)
    elif time < sunset - hours/2:
        return Max
    elif time < sunset + hours/2:
        return -1 * ((Max - Min) / 2) * math.sin(n * (time - sunset)) + ((Max + Min) / 2)
    else:
        return Min

 

So far our basic flow is:

system-flow-chart

Getting the sunset and sunrise times

Because the sunrise and sunset times can change by a few hours in different parts of the year, I wanted to use the current sunset and sunrise times for my location. There are a few APIs that are available to do this, some of which do not seem to take things like daylight savings into account. https://ipgeolocation.io/astronomy-api.html was the one I used and seemed to work quite well. Instead of having to worry about finding out my longitude and latitude, I could just use the nearest city in the request. In order to use this service, you will have to make an account (free account) with them to get an API key to use in the requests.

A simple get request to get the sunrise and sunset times is below:

import requests
import json
import constants # a file called constants.py that you need to put in the same directory as app.py
def get_sunset_sunrise_time():
   
    # these values are from the constants.py file
    city = constants.CITY
    api_key = constants.API_KEY

    response = requests.get(f"https://api.ipgeolocation.io/astronomy?apiKey={api_key}&location={city}")
   
    response = json.loads(response.content)
    sunrise = response["sunrise"]
    sunset = response["sunset"]
    date = response["date"]

    logging.info(f"Today's date: {date}, sunrise: {sunrise}, sunset: {sunset}")

    sunrise = sunrise.split(":")
    sunrise = (int(sunrise[0]), int(sunrise[1]))

    sunset = sunset.split(":")
    sunset = (int(sunset[0]), int(sunset[1]))

    return {"sunrise": sunrise, "sunset": sunset}

constants.py:

API_KEY = "d3f1n1t3ly 4n ap1 k3y"
CITY = "canberra"

replace the values in constants.py to match your API key and your nearest city.

 

Putting it all together

 

This flow chart summarises the main structure of the code:

code-flow-chart


Here is the final python code:

from rpi_ws281x import PixelStrip, Color
import math
import time
import datetime
import logging
import requests
import json
import constants # a file called constants.py that you need to put in the same directory as app.py

# LED strip configuration:
LED_COUNT = 119        # Number of LED pixels.
LED_PIN = 18          # GPIO pin connected to the pixels (18 uses PWM!).
# LED_PIN = 10        # GPIO pin connected to the pixels (10 uses SPI /dev/spidev0.0).
LED_FREQ_HZ = 800000  # LED signal frequency in hertz (usually 800khz)
LED_DMA = 10          # DMA channel to use for generating signal (try 10)
LED_BRIGHTNESS = 255  # Set to 0 for darkest and 255 for brightest
LED_INVERT = False    # True to invert the signal (when using NPN transistor level shift)
LED_CHANNEL = 0       # set to '1' for GPIOs 13, 19, 41, 45 or 53

# Create NeoPixel object with appropriate configuration.
strip = PixelStrip(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL)
# Intialize the library (must be called once before other functions).
strip.begin()

def display_colour(red, green, blue):
    for i in range(strip.numPixels()):
        strip.setPixelColor(i, Color(red, green, blue))
        strip.show() # if you don't want the effect of each pixel changing one by one, move this to after the for loop


def clamp(n, minn, maxn):
    # Function for limiting a number to a specified number range
    return max(min(maxn, n), minn)


def convertTempToRGB(temp):
    # Function for converting a colour temperature in Kelvin (K) to RGB
    # Algorithm from: https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html

    temp = temp / 100
    red = 0
    green = 0
    blue = 0

    # RED
    if temp <= 66:
        red = 255
    else:
        red = temp - 60
        red = 329.698727446 * (red ** -0.1332047592)
        red = clamp(red, 0, 255)

    # GREEN
    if temp <= 66:
        green = temp
        green = 99.4708025861 * math.log(green) - 161.1195681661
        green = clamp(green, 0, 255)
    else:
        green = temp - 60
        green = 288.1221695283 * (green ** -0.0755148492)
        green = clamp(green, 0, 255)

    # BLUE
    if temp >= 66:
        blue = 255
    else:
        if temp <= 19:
            blue = 0
        else:
            blue = temp - 10
            blue = 138.5177312231 * math.log(blue) - 305.0447927307
            blue = clamp(blue, 0, 255)

    # return (red, green, blue)
    return (round(red), round(green), round(blue))


# def get_current_temp(time, Max=4000, Min=650, slope=2, sunset=(20, 0), sunrise=(6, 0)):
#     # This an alternative version of ‘get_current_temp’ that uses arctan

#     time = time[0] + time[1]/60
#     sunset = sunset[0] + sunset[1]/60
#     sunrise = sunrise[0] + sunrise[1]/60

#     if time < (sunset - 24 + sunrise)/2:
#         return round(((Min - Max)/math.pi) * math.atan(slope * (time + 24 - sunset)) + (Max + Min)/2)
#     elif time < (sunrise + sunset) / 2:
#         return round(((Min - Max)/math.pi) * math.atan(-slope * (time - sunrise)) + (Max + Min)/2)
#     elif time < (sunset - 24 + sunrise)/2 + 24 :
#         return round(((Min - Max)/math.pi) * math.atan(slope * (time - sunset)) + (Max + Min)/2)
#     else:
#         return round(((Min - Max)/math.pi) * math.atan(-slope * (time - (sunrise + 24))) + (Max + Min)/2)


def get_current_temp(time, Max=4000, Min=650, hours=2, sunset=(20, 0), sunrise=(6, 0)):
    # Function for getting the relevant colour temperature for a the time of day
    # times are converted from a tuple (hour, minute) to a float
    time = time[0] + time[1]/60
    sunset = sunset[0] + sunset[1]/60
    sunrise = sunrise[0] + sunrise[1]/60

    n = math.pi / hours

    if time < sunrise - hours/2:
        return Min
    elif time < sunrise + hours/2:
        return ((Max - Min) / 2) * math.sin(n * (time - sunrise)) + ((Max + Min) / 2)
    elif time < sunset - hours/2:
        return Max
    elif time < sunset + hours/2:
        return -1 * ((Max - Min) / 2) * math.sin(n * (time - sunset)) + ((Max + Min) / 2)
    else:
        return Min


def get_sunset_sunrise_time():
    # Function for getting the relevant sunrise and sunset times for your city
    city = constants.CITY
    api_key = constants.API_KEY

    response = requests.get(f"https://api.ipgeolocation.io/astronomy?apiKey={api_key}&location={city}")
   
    response = json.loads(response.content)
    sunrise = response["sunrise"]
    sunset = response["sunset"]
    date = response["date"]

    logging.info(f"Today's date: {date}, sunrise: {sunrise}, sunset: {sunset}")

    sunrise = sunrise.split(":")
    sunrise = (int(sunrise[0]), int(sunrise[1]))

    sunset = sunset.split(":")
    sunset = (int(sunset[0]), int(sunset[1]))

    return {"sunrise": sunrise, "sunset": sunset}


if __name__ == "__main__":
    format = "%(asctime)s [%(levelname)s]: %(message)s "
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    day = datetime.datetime.now().day # getting the current day
    results = get_sunset_sunrise_time()
    sunrise = results["sunrise"]
    sunset = results["sunset"]

    while True:
        currentDT = datetime.datetime.now() # getting the current time
        currentDT = (currentDT.hour, currentDT.minute)

        temp = get_current_temp(currentDT, sunrise=sunrise, sunset=sunset, hours=1.5) # getting current colour temperature
        red, green, blue = convertTempToRGB(temp) # converting current colour temperature to RGB
        display_colour(red, green, blue) # displaying RGB values on the led strip
        logging.info(f"Colour temperature = {temp},  red={red}, green={green}, blue={blue}")

        time.sleep(60) # this adjusts how often to update the colours of the lights

        if datetime.datetime.now().day != day:
            # its a new day
            # request sunset and sunrise times
            try:
                results = get_sunset_sunrise_time()
                sunrise = results["sunrise"]
                sunset = results["sunset"]
            except:
                logging.error('Could not retrieve sunrise and sunset information!')
         pass

Next Steps

I'm pretty happy with how the project has turned out. I plan to further develop this project into a complete smart lighting system for the LED strips, creating a web app where I can control the colours of the lights with a phone or laptop.

Some ideas to inspire anyone working on similar projects:

  • dynamic colours: currently with this code, at each point in time there is only one static colour displayed on all of the led pixels. A cool idea would be to make it constantly changing like a wave or different animations.
  • automatic brightness: the brightness of the led lights could change depending on the hour of the day. The lights would be dimmer as it gets later at night.

Thanks for reading!

Have a question? Ask the Author of this guide today!

Please enter minimum 20 characters

Your comment will be posted (automatically) on our Support Forum which is publicly accessible. Don't enter private information, such as your phone number.

Expect a quick reply during business hours, many of us check-in over the weekend as well.

Comments


Loading...
Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.