Automatic sunrise and sunset color / colour temperature control (with LIFX)

Hi all,

I came across an example and another question around how to re-produce f.lux style changes with bulbs that are capable of doing so. This had been on my wish list for a while so a month or so back I had a bash at making it a reality. The bulbs I’m using are LIFX Original A21, but this approach should work with most bulbs. This Hue solution served as a starting point, although it had some short comings that I wanted to solve.

My aim was to automatically mimic the transition to a bluer colour of daylight during the day and then to a warmer colour of candlelight / tungsten bulbs in the evening. In order to ensure this adjusted year round I use the sunrise and sunset times from the astro plugin. I experimented quite a bit but eventually landed with making the transition in the two hours before sunrise and sunset.

The main change I made from the Hue solution was to execute on a cron every minute rather than kickoff a background process that does the change over an hour. This makes for much easier testing and also means that restarting OpenHAB will not stop the colour change process.

In addition I found I needed to work around the fact that it isn’t possible to set the colour temperature of a LIFX bulb whilst it is off. Setting it turns the bulb on! My work around is to check the state of the bulb and only update it if it is on. In order to deal with bulbs that are switched on but are the wrong colour because they were last switched on at another time I also trigger my rule on bulb state changes and fix them up as quickly as possible. This does mean that there is sometimes a short period of time (~1s) when it is the wrong colour before the rule can update it. I suspect other bulbs might not have this problem.

My things/astro.things looks like:

astro:sun:home [ geolocation="51.47135,-0.09212", interval=300]
astro:moon:home [ geolocation="51.47135,-0.09212", interval=300]

and the corresponding items/astro.items looks like:

DateTime Sunrise_Time  "Sunrise [%1$tH:%1$tM]"  { channel="astro:sun:home:rise#start" }
DateTime Sunset_Time   "Sunset [%1$tH:%1$tM]"   { channel="astro:sun:home:set#start" }

My LIFX bulbs are defined as follows:

// LIFX bulbs that have their temperature automatically updated go in this group
Group gColourTemperature
// This group is used to update bulb colour temp when they are switched on and to
// ensure that colour temp of a bulb that isn't on will not be updated.
Group:Switch gColourTemperatureSwitch

Color lounge_sofa_light_color "Sofa" <light> (gLounge) [ "Lighting" ] { channel="lifx:colorlight:D073D500E6EE:color" }
Switch lounge_sofa_light_switch (gLounge, gColourTemperatureSwitch) { channel="lifx:colorlight:D073D500E6EE:color" }
Dimmer lounge_sofa_light_dimmer { channel="lifx:colorlight:D073D500E6EE:color" }
Dimmer lounge_sofa_light_temperature (gColourTemperature) { channel="lifx:colorlight:D073D500E6EE:temperature" }

Color lounge_main_light_color "Main" <light> (gLounge) [ "Lighting" ] { channel="lifx:colorlight:D073D500EC7D:color" }
Switch lounge_main_light_switch (gLounge, gColourTemperatureSwitch) { channel="lifx:colorlight:D073D500EC7D:color" }
Dimmer lounge_main_light_dimmer { channel="lifx:colorlight:D073D500EC7D:color" }
Dimmer lounge_main_light_temperature (gColourTemperature) { channel="lifx:colorlight:D073D500EC7D:temperature" }

Color hallway_light_color "Hallway" <light> (gHallway) [ "Lighting" ] { channel="lifx:colorlight:D073D500E81E:color" }
Switch hallway_light_switch (gHallway, gColourTemperatureSwitch) { channel="lifx:colorlight:D073D500E81E:color" }
Dimmer hallway_light_dimmer { channel="lifx:colorlight:D073D500E81E:color" }
Dimmer hallway_light_temperature (gColourTemperature) { channel="lifx:colorlight:D073D500E81E:temperature" }

Color bedroom_main_light_color "Bed" <light> (gBedroom) [ "Lighting" ] { channel="lifx:colorlight:D073D500E456:color" }
Switch bedroom_main_light_switch (gBedroom, gColourTemperatureSwitch) { channel="lifx:colorlight:D073D500E456:color" }
Dimmer bedroom_main_light_dimmer { channel="lifx:colorlight:D073D500E456:color" }
Dimmer bedroom_main_light_temperature (gColourTemperature) { channel="lifx:colorlight:D073D500E456:temperature" }

Color bedroom_corridor_light_color "Corridor" <light> (gBedroom) [ "Lighting" ] { channel="lifx:colorlight:D073D500E7AC:color" }
Switch bedroom_corridor_light_switch (gBedroom, gColourTemperatureSwitch) { channel="lifx:colorlight:D073D500E7AC:color" }
Dimmer bedroom_corridor_light_dimmer { channel="lifx:colorlight:D073D500E7AC:color" }
Dimmer bedroom_corridor_light_temperature (gColourTemperature) { channel="lifx:colorlight:D073D500E7AC:temperature" }

There are two things to note above:

  • the two groups that help the rules to find the dimmer controls and switches of the lights that should be controlled
  • that the naming of the switch and temperature have a common root and end in _switch and _temperature respectively

Finally this is the rule in rules/daylight.rule:

import org.joda.time.*

/*
A rule to update the colour temperature of LIFX bulbs throughout the day.

At present this is warm evening and overnight. It cools the light in the two hours
before sunrise, stays cool throughout the day and then warms again over four
hours around sunset (two before, two after).

Unfortunately, setting the colour temperature of a bulb also turns it on (rolls eyes).
To work around this we do two things:
 - Find the switch and check that it is ON, if not we don't update the colour temp
 - Ask for updates when a light is switched on or off and ASAP after a bulb is switched
   on we set it to the current colour temperature
*/
rule "light_colour_temp"
when
    // every 10 seconds for debugging purposes
    //Time cron "0/10 * * ? * * *" or
    // apply every two minutes when actually running - that should mean it rarely/never changes
    // by more than a 1% increment
	Time cron "0 0/2 * ? * * *" or
    Item gColourTemperatureSwitch received update
then
    logInfo("daylight", "Sunset time: "+Sunset_Time.state)
    var sunrise = new DateTime(Sunrise_Time.state.toString)
    var sunset = new DateTime(Sunset_Time.state.toString)
    var preSunrise = sunrise.minusHours(2)
    var preSunset = sunset.minusHours(2)
    var postSunset = sunset.plusHours(0)
    logInfo("daylight", "Times: "+preSunrise+" "+preSunset+" "+postSunset)
    var now = new DateTime()
    var int CT_COOL = 60
    var int CT_WARM = 92
    var float colourTemp = 0

    if (now.isBefore(preSunrise)) {
        logInfo("daylight", "early hours: WARM")
        colourTemp = CT_WARM
    } else if (now.isBefore(sunrise)) {
        logInfo("daylight", "dawn: CHANGING TO COOL")
        var long period = sunrise.millis - preSunrise.millis
        var long progress = now.millis - preSunrise.millis
        var float factor = progress.floatValue / period.floatValue
        logInfo("daylight", period+" "+progress+" "+factor)
        colourTemp = ((1-factor) * (CT_WARM-CT_COOL) + CT_COOL).intValue
    } else if (now.isBefore(preSunset)) {
        logInfo("daylight", "daytime: COOL")
        colourTemp = CT_COOL
    } else if (now.isBefore(postSunset)) {
        logInfo("daylight", "evening: CHANGING TO WARM")
        var long period = postSunset.millis - preSunset.millis
        var long progress = now.millis - preSunset.millis
        var float factor = progress.floatValue / period.floatValue
        logInfo("daylight", period+" "+progress+" "+factor)
        colourTemp = (factor * (CT_WARM-CT_COOL) + CT_COOL).intValue
    } else {
        logInfo("daylight", "late evening: WARM")
        colourTemp = CT_WARM
    }

    logInfo("daylight", "setting CT to "+colourTemp+"%")
    gColourTemperature.members.forEach(ct_dimmer | {
        var switchName = ct_dimmer.name.replaceFirst("_temperature$", "_switch")
        var switchItem = gColourTemperatureSwitch.members.filter(x|x.name == switchName).map[it].head
        logDebug("daylight", "for "+ct_dimmer.name+" got "+switchItem.name)
        if (ct_dimmer.state != colourTemp && switchItem.state == ON) {
            logDebug("daylight", "updating "+ct_dimmer.name+" to "+colourTemp)
            sendCommand(ct_dimmer, colourTemp)
        }
    })
end

I’ve had this running the the last month or so and have been pleased with how well it works.

I’d be glad of any suggestions for improvements and other feedback.

11 Likes

I am surprised by how little traction this post has received.

I’m trying to implement this rule in my system and i’m failing at the lambda part. VSCode just doesn’t let me do it that way.

this gives me the error: There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.

can you help out?

Typically the problem is that java does not know what type ct_dimmer is. Try

forEach(ct_dimmer as DimmerType | {

or whatever type ist ct_dimmer

Thanks, @sihil! I was looking for something like this. I’ve adapted your code a bit:

  • I’ve updated it to work with openHAB 3.0. I had to replace Joda time based objects and functions with java.time equivalents.
  • I removed the forEach part. My bulbs (IKEA Trådfi) can receive color temperature commands while off, so I don’t need that.
  • I changed item names to match my setup.
  • Used placeholders in the logging statements, instead of string concatenation.
  • Used val instead of var (except for colourTemp)
  • Used Duration to calculate the duration, instead of manually substracting the milliseconds.

Here’s the code, perhaps someone can use it:

rule "light_colour_temp"
when
    // every 10 seconds for debugging purposes
    //Time cron "0/10 * * ? * * *" or
    // apply every two minutes when actually running - that should mean it rarely/never changes
    // by more than a 1% increment
	Time cron "0 0/2 * ? * * *" or
    Item gWoonkamer_Kleur received update or
    Item gWoonkamer_Dimmer received update
then
    logInfo("light_colour_temp", "Sunset time: {}", vSunset_Time.state)
    val sunrise = (vSunrise_Time.state as DateTimeType).getZonedDateTime
    val sunset = (vSunset_Time.state as DateTimeType).getZonedDateTime 
    val preSunrise = sunrise.minusHours(2)
    val preSunset = sunset.minusHours(2)
    val postSunset = sunset.plusHours(0)
    logInfo("light_colour_temp", "Times: {}, {}, {}", preSunrise, preSunset, postSunset)
    val now = ZonedDateTime.now()
    val int CT_COOL = 60
    val int CT_WARM = 92
    var float colourTemp = 0

    if (now.isBefore(preSunrise)) {
        logInfo("light_colour_temp", "early hours: WARM")
        colourTemp = CT_WARM
    } else if (now.isBefore(sunrise)) {
        logInfo("light_colour_temp", "dawn: CHANGING TO COOL")
        val long period = Duration.between(preSunrise, sunrise).getSeconds
        val long progress = Duration.between(preSunrise, now).getSeconds
        val float factor = progress.floatValue / period.floatValue
        logInfo("light_colour_temp", "period: {}, progress: {}, factor: {}", period, progress, factor)
        colourTemp = ((1-factor) * (CT_WARM-CT_COOL) + CT_COOL).intValue
    } else if (now.isBefore(preSunset)) {
        logInfo("light_colour_temp", "daytime: COOL")
        colourTemp = CT_COOL
    } else if (now.isBefore(postSunset)) {
        logInfo("light_colour_temp", "evening: CHANGING TO WARM")
        val long period = Duration.between(preSunset, postSunset).getSeconds
        var long progress = Duration.between(preSunset, now).getSeconds
        val float factor = progress.floatValue / period.floatValue
        logInfo("light_colour_temp", "period: {}, progress: {}, factor: {}", period, progress, factor)
        colourTemp = (factor * (CT_WARM-CT_COOL) + CT_COOL).intValue
    } else {
        logInfo("light_colour_temp", "late evening: WARM")
        colourTemp = CT_WARM
    }

    logInfo("light_colour_temp", "setting CT to {}% ", colourTemp)
    logDebug("light_colour_temp", "updating {} to {}", gWoonkamer_Kleur.name, colourTemp)
    sendCommand(gWoonkamer_Kleur, colourTemp)
end
1 Like

Hi There, looking to implement this or something like it. Where do you get vSunset_Time and vSunrise_Time from? Are these declared somewhere else as I’m getting an error.

Those variables are equivalent to the variables Sunrise_Time and Sunset_Time from the opening post. In my setup they were already present by these other names, that’s why I changed my example.

I have something similar but I only use 3 colours and different brightness levels.
I use a Hue motion sensor to send an ON command to the HSB light bulb if the lux level is less than 4.
The astro binding is used to determine the phase of day. If it is daylight then the light will be green and 100% brightness. If the phase of the day is night then the light will be red and 50% brightness so you don’t get blinded in the middle of the night :slight_smile:
If the phase of the day if not daylight or night (dusk, dawn etc) then the light is blue and 75% brightness.
The programming is done using the UI and javascript. (no DSL)
It is basic but works well.

image

//If daylight the light will be 100% bright and green. If night it will be 50%bright and red
//Any other day phase it will be 75% and blue

// logger.info("About to test executeCommandLine");
var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

var Exec = Java.type("org.openhab.core.model.script.actions.Exec");
var Duration = Java.type("java.time.Duration");


//if it is not NIGHT then change colour
if (itemRegistry.getItem("LocalSun_SunPhaseName").getState() == 'NIGHT' ) {
//HSB Hue,Saturation, Brightness Below is 50% bright  
events.sendCommand("LightHall_Lighthallcolour", "350,99,50"); //red
  
} else if (itemRegistry.getItem("LocalSun_SunPhaseName").getState() == 'DAYLIGHT' ) {
events.sendCommand("LightHall_Lighthallcolour", "100,99,100"); //green

}else{
//it is not NIGHT so only do green light  
events.sendCommand("LightHall_Lighthallcolour", "225,99,75"); //blue
}

logger.info("hall light = " + itemRegistry.getItem("LocalSun_SunPhaseName").getState() );

I could have integrated the motion and the on rules but I kept them separate because sometimes I want to turn all the lights on regardless of light levels. I have and all light group switch for that.

image

@bartkummel Ah! Got it, light dawns on marblehead (or it will soon in all it’s correct colour temperature glory!).

@ubeaut 0-o the code…it hurt my eeeeyeyeeessss!. I’ll need to figure out some understanding of Java I think. Only just wrapping my head around DSL but thank you, something for me to aspire towards.

I am not a javascript expert. I just cut and paste to make it work and try to keep it simple. I keep examples of my code in one place so I can cut and paste.
The editor is better. Just ctrl-space and it sometimes auto fills the code.
I had DSL in OH2.5 but that was messy. I decided just to use the UI and javascript in OH3 and so far it has worked fine.
I am glad someone likes my code. :slightly_smiling_face: