Mimic the sun indoors with RGBW(W) lights

Moderator: leecollings

Post Reply
Bliepo
Posts: 2
Joined: Wednesday 09 December 2020 16:43
Target OS: Linux
Domoticz version:
Contact:

Mimic the sun indoors with RGBW(W) lights

Post by Bliepo »

Introduction
As most people probably know, lights has a very important impact on sleep (https://www.sleepfoundation.org/bedroom ... -and-sleep), or as the page linked in this sentence mentions
When exposed to only natural light, a person’s circadian rhythm becomes closely synchronized with sunrise and sunset3, staying awake during the day and sleeping when it’s dark. In modern society, though, electricity creates an abundance of light sources that affect the brain’s circadian pacemaker.
At the time of writing, which is during the covid-19 pandemic, some of us are working at home and may therefore be unable to go outside during the day. Or maybe you just don't feel like going outside (I'm not judging). Whatever your reason, you are staying inside but also like to reduce the negative impact that artificial lighting has on your sleep. Wouldn't it be nice if there were some way to mimic the sun indoors? That is where this script comes in. It uses weather data and RGBW(W) lights to mimic the colour temperature of the sunlight as best as possible.

Note 1) I am no medical profession and have not done any in depth research on the health related effects. I'm operating under the assumption that mimicking sunlight as best as possible will reduce negative effects on sleep, but it is an assumption. If you are a medical professional or an expert in fields related, please contact me by PM, rather than starting a discussion in the thread.
Note 2) The code only tries to mimic the colour temperature of the sunlight. It does not adjust the brightness to mimic outdoor conditions.

Acknowledgments
I would like to thank the maker (Alex Mellnik) of the EdiSun project on Github (https://github.com/amellnik/EdiSun). Without his code, this entire project would not have been possible. All I did was take his code, written in Julia, and adapt it so it could be used in dzVents. That's it. All the rest is his work. All credit goes to him.

License
Since the work is based on GPL v2 licensed code, everything here is also using the GPL v2 license (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).

How to use
  1. Sign-up for an account at openweathermap.org (https://home.openweathermap.org/users/sign_up
  2. Verify your email address
  3. Replace the <CITY> in the code with your city and the <API_KEY> with the key you receive by e-mail.
  4. Replace the maxTemp and minTemp variable values by the minimun and maximum colour temperature values your lights are specced at.
  5. Replace the devId by the id of your light.
  6. Optionally: calibrate the colour temperature output (see calibration section)
Code

Code: Select all

return {
    on = {
        timer = { 'every 10 minutes' },
        httpResponses = { 'gotWeather' }
    },
    execute = function(domoticz, item)
        ----------------------
        -- Helper functions --
        ----------------------
        local function getTemp(mult)
            -- Calculates the color temperature of the sunlight outside
            local currentTime = domoticz.time.minutesSinceMidnight
            local sunriseTime = domoticz.time.sunriseInMinutes
            local sunsetTime = domoticz.time.sunsetInMinutes
            local percentDay = (currentTime - sunriseTime)/(sunsetTime - sunriseTime)
            if (percentDay >= 0 and percentDay <= 1) then
                return mult*(6990.5 +
                    percentDay*74508 +
                    percentDay*percentDay*-4.5021e5 +
                    percentDay*percentDay*percentDay*9.5966e5 +
                    percentDay*percentDay*percentDay*percentDay*-8.6916e5 +
                    percentDay*percentDay*percentDay*percentDay*percentDay*2.8073e5)
            else
                return 2519
            end
        end
        
        local function getMultFromCode(code)
            -- Calculates a multiplier based on the type of weather
            if (math.floor(code/100) == 2) then
                return 1.8
            elseif (math.floor(code/100) == 3) then
                return 1.3
            elseif (math.floor(code/100) == 5) then
                return 1.5
            elseif (math.floor(code/100) == 7) then
                return 1.74
            else
                return -1
            end
        end
        
        local function getKelvinFromTemp(temp)
            local maxTemp = 6500
            local minTemp = 2000
            if (temp > maxTemp) then
                return 0
            elseif (temp < minTemp) then
                return 100
            else
                local kelvin = (temp - minTemp)/(maxTemp - minTemp)
                return math.floor(kelvin + 0.5)
            end
        end
        
        -------------------
        -- Main function --
        -------------------
        if (item.isTimer) then
            domoticz.openURL({
                url = 'https://api.openweathermap.org/data/2.5/weather?q=<CITY>&appid=<API_KEY>',
                method = 'GET',
                callback = 'gotWeather'
                })
        elseif (item.isHTTPResponse) then
            if (item.ok) then
                local code = item.json["weather"][1]["id"]
                if (code ~= nil) then
                    mult = getMultFromCode(code)
                else
                    -- Don't have a code, estimate from cloud cover
                    local coverage = item.json.clouds.all
                    mult = 1 + 0.4*coverage/100
                end
                
                colorTemp = getTemp(mult)
                local k = getKelvinFromTemp(colorTemp)
                local devId = 1
        
                if (domoticz.devices(devId).active) then
                    domoticz.devices(devId).setWhiteMode()
                    domoticz.devices(devId).setKelvin(k)
                    domoticz.devices(devId).dimTo(100)
                end
            end
        end
    end
}
Optional: calibration
The code above assumes that the colour temperature output of the light follows a linear distribution. As an example, if you have a light that has a minimum colour temperature of 1800K and a maximum of 6500K, it assumes that setKelvin(0) = 1800K, setKelvin(10) = 2270K, setKelvin(20) = 2740K, setKelvin(30) = 3210K, ..., setKelvin(90) = 6030K, setKelvin(100) = 6500K. In other words, increasing setKelvin by 1% should increase the colour temperature by 1%.

This is however rarely the case. For example, my light bulbs, the Innr rb 285 c-2, follows a graph that looks more like this:
Image

So the change in colour temperature is much larger in the beginning and smaller towards the end. The line shows a third order polynomial fit and the black dots show the actual measured data points.

You can calibrate your own lights by using a camera, pointing it at a white wall and reading out the automatically chosen colour temperatures, while using the setKelvin function to change the output of the light bulb. This is not super accurate, but better than having no calibration at all. If somebody knows of a better way to measure the colour temperature cheaply I'd love to hear it by the way!

Using the measured points, you can fit a curve through them and use that to adjust your lights. If you don't know how to do curve fitting, please send me a PM with the measuring points you have and I will do it for you if/when I have time.

This is the code I used for calibrating my light bulbs:

Code: Select all

        local function getKelvinFromTemp(temp)
            if (temp > 5150) then
                return 0
            elseif (temp < 2050) then
                return 100
            else
                -- Normalized color temperature
                local tn = (temp - 3183)/955.3
                
                local kelvin = -3.85*tn*tn*tn + 12.43*tn*tn - 29.86*tn + 40.41
                return math.floor(kelvin + 0.5)
            end
        end
Improvements and suggestions
If you have improvements or suggestions you can let me know here in the thread or by PM. I probably won't have time to program these myself however, so if you know how to code please code it yourself and then share it here so everybody can benefit. Be aware that I may incorporate anything shared by thread or PM in the main post - unless you specifically request I do not - so the code will end up under the GPL v2 license.

That is all folks! I hope you found this post useful.
aleph0
Posts: 85
Joined: Thursday 12 May 2016 15:47
Target OS: Linux
Domoticz version: 11838
Location: South of France
Contact:

Re: Mimic the sun indoors with RGBW(W) lights

Post by aleph0 »

Very interesting project ! Thanks for sharing

Envoyé de mon moto g(6) en utilisant Tapatalk

User avatar
kiddigital
Posts: 437
Joined: Thursday 10 August 2017 6:52
Target OS: Raspberry Pi / ODroid
Domoticz version: Beta
Location: Netherlands
Contact:

Re: Mimic the sun indoors with RGBW(W) lights

Post by kiddigital »

Very nice idea Would you mind if I incorporate this somehow into the OpenWeatherMap module of Domoticz? Maybe create a custom sensor that holds the values so these can be used for the lights of your choice.

Btw. similar but not the same, do you know/have a good curve to simulate sunrise? This way you can create a ‘wake-up light’, a light that does a (speed-up) sunrise to wake up more naturally?
One RPi with Domoticz, RFX433e, aeon labs z-wave plus stick GEN5, ha-bridge 5.4.0 for Alexa, Philips Hue Bridge, Pimoroni Automation Hat
One RPi with Pi foundation standard touch screen to display Dashticz
Bliepo
Posts: 2
Joined: Wednesday 09 December 2020 16:43
Target OS: Linux
Domoticz version:
Contact:

Re: Mimic the sun indoors with RGBW(W) lights

Post by Bliepo »

I wouldn't mind at all! In fact, I think that would be a very nice thing to have! Do keep in mind that the GPL v2 license applies however.

Unfortunately not no. I found it quite hard to find good information on the colour temperature of the sun (unless if it's a black-body radiator but that is not useful for this application). That is also the main reason I didn't write this from scratch but instead opted to use the code by the EdiSun project.

I did find a scientific paper (it is linked here: https://physics.stackexchange.com/quest ... n-in-the-s) that describes how to calculate intensity as a function of wavelength based on the sun's position, but then you need to calculate the colour temperature from that info, which is not something I have done before and the paper also mentioned that the spectra were not Planckian. I could probably figure out how to do it if I invested enough time into it, but it's not a priority. Besides, it does not take into account weather information as far as I can tell.

An easier way would probably be to use a cheap webcam and use that to determine the colour temperature of the outside lighting based on a known reference.
Sarcas
Posts: 86
Joined: Wednesday 11 October 2017 8:50
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1ß
Location: Friesland
Contact:

Re: Mimic the sun indoors with RGBW(W) lights

Post by Sarcas »

I implemented this in my scripts some days ago, but I can’t get it to work properly. Kelvin is always 100% or 0%, never anything in between. The multiplier (mult) is always -1 because the weather code is always 8-ish (clear sky etc). So the colortemp always turns out negative. Except when the sun is down (i think), then kelvin is 0%. Did weather codes change, or am i missing something?

Code: Select all

-- https://openweathermap.org/api/one-call-api
-- https://www.domoticz.com/forum/viewtopic.php?f=72&t=34816&p=263153

return {
    on = {
        timer = {
            'every 5 minutes'
--        },
--        httpResponses = {
--            'OWMForecastFunc'
        }
    },

	logging = {
		level   = domoticz.LOG_DEBUG,
		marker  = "OWMFunctionKelvin"
	},

    execute = function(domoticz, item)
        ----------------------
        -- Helper functions --
        ----------------------
        local function getTemp(mult)
            -- Calculates the color temperature of the sunlight outside
            local currentTime = domoticz.time.minutesSinceMidnight
            local sunriseTime = domoticz.time.sunriseInMinutes
            local sunsetTime = domoticz.time.sunsetInMinutes
            local percentDay = (currentTime - sunriseTime)/(sunsetTime - sunriseTime)
            if (percentDay >= 0 and percentDay <= 1) then
                return mult*(6990.5 +
                    percentDay*74508 +
                    percentDay*percentDay*-4.5021e5 +
                    percentDay*percentDay*percentDay*9.5966e5 +
                    percentDay*percentDay*percentDay*percentDay*-8.6916e5 +
                    percentDay*percentDay*percentDay*percentDay*percentDay*2.8073e5)
            else
                return 2519
            end
        end
        
        local function getMultFromCode(code)
            -- Calculates a multiplier based on the type of weather
            if (math.floor(code/100) == 2) then
                return 1.8
            elseif (math.floor(code/100) == 3) then
                return 1.3
            elseif (math.floor(code/100) == 5) then
                return 1.5
            elseif (math.floor(code/100) == 7) then
                return 1.74
            else
                return -1
            end
        end
        
local function getKelvinFromTemp(temp)
            local maxTemp = 6535
            local minTemp = 2000
            if (temp > maxTemp) then
                return 0
            elseif (temp < minTemp) then
                return 100
            else
                local kelvin = (temp - minTemp)/(maxTemp - minTemp)
                return math.floor(kelvin + 0.5)
            end
        end
        
        -------------------
        -- Main function --
        -------------------
        if (item.isTimer) then
            domoticz.log('isTimer', domoticz.LOG_DEBUG)
            local myJSON = domoticz.globalData.OWMForecast
--                local code = myJSON["current"]["weather"]["id"]
                local code = myJSON.current.weather[1].id
                if (code ~= nil) then
                    mult = getMultFromCode(code)
                    domoticz.log('code ~= nil (' .. code .. '), mult = ' .. mult, domoticz.LOG_DEBUG)
                else
                    -- Don't have a code, estimate from cloud cover
                    local coverage = myJSON.current.clouds
                    mult = 1 + 0.4*coverage/100
                    domoticz.log('code = nil, coverage = ' .. coverage .. '  mult = ' .. mult, domoticz.LOG_DEBUG)
                end
                
                colorTemp = getTemp(mult)
                local k = getKelvinFromTemp(colorTemp)
                local kelvinBuiten = k
                domoticz.globalData.kelvinBuiten = kelvinBuiten
                
                domoticz.devices('KelvinPercentageBuiten').updatePercentage(kelvinBuiten)
                
                domoticz.log('mult = ' .. mult .. '  colorTemp = '.. colorTemp .. '  kelvinBuiten = ' .. kelvinBuiten, domoticz.LOG_DEBUG)
--                local devId = 1
        
--                if (domoticz.devices(devId).active) then
--                    domoticz.devices(devId).setWhiteMode()
--                    domoticz.devices(devId).setKelvin(k)
--                    domoticz.devices(devId).dimTo(100)
--                end
        end
    end
}

Log:

Code: Select all

2021-06-07 17:25:00.455 Status: dzVents: Info: OWMFunctionKelvin: ------ Start internal script: z - OWMFunctionKelvin:, trigger: "every 5 minutes"
2021-06-07 17:25:00.458 Status: dzVents: Debug: OWMFunctionKelvin: isTimer
2021-06-07 17:25:00.458 Status: dzVents: Debug: OWMFunctionKelvin: code ~= nil (800), mult = -1
2021-06-07 17:25:00.458 Status: dzVents: Debug: OWMFunctionKelvin: mult = -1 colorTemp = -6163.3995631262 kelvinBuiten = 100
2021-06-07 17:25:00.472 Status: dzVents: Info: OWMFunctionKelvin: ------ Finished z - OWMFunctionKelvin
2021-06-07 17:30:00.563 Status: dzVents: Info: OWMFunctionKelvin: ------ Start internal script: z - OWMFunctionKelvin:, trigger: "every 5 minutes"
2021-06-07 17:30:00.566 Status: dzVents: Debug: OWMFunctionKelvin: isTimer
2021-06-07 17:30:00.566 Status: dzVents: Debug: OWMFunctionKelvin: code ~= nil (800), mult = -1
2021-06-07 17:30:00.567 Status: dzVents: Debug: OWMFunctionKelvin: mult = -1 colorTemp = -6150.929302665 kelvinBuiten = 100
2021-06-07 17:30:00.581 Status: dzVents: Info: OWMFunctionKelvin: ------ Finished z - OWMFunctionKelvin
2021-06-07 17:35:00.677 Status: dzVents: Info: OWMFunctionKelvin: ------ Start internal script: z - OWMFunctionKelvin:, trigger: "every 5 minutes"
2021-06-07 17:35:00.681 Status: dzVents: Debug: OWMFunctionKelvin: isTimer
2021-06-07 17:35:00.681 Status: dzVents: Debug: OWMFunctionKelvin: code ~= nil (800), mult = -1
2021-06-07 17:35:00.681 Status: dzVents: Debug: OWMFunctionKelvin: mult = -1 colorTemp = -6136.6741345792 kelvinBuiten = 100
2021-06-07 17:35:00.695 Status: dzVents: Info: OWMFunctionKelvin: ------ Finished z - OWMFunctionKelvin
--

Domoticz on rPi4 - RFXCOM RFXtrx433 USB - ZW090 Z-Stick Gen5 EU - IKEA Tradfri - Philips HUE - YouLess meter - SolarEdge
Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests