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 Raspberry Pi Zero W or WH
- A Logic Level Converter
- Some WS2812 or NeoPixel LEDs
- A 5V Power Supply
- A DC Barrel Jack to 2-Pin Terminal Block Adapter
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 |
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_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...
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:
If we apply the right dilations to make it useful for our purpose we get something like this:
Colour temperature is the vertical axis and hour of the day is on the horizontal axis.
The equation written in maths is below:
- 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:
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:
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!