Circadian light with Philips Hue, independent from world clocks ;-)

Tags: #<Tag:0x00007fa73b43fb18>

UPDATED March 2018:

  • can now adapt to any time zone (only relies on azimuth and sun-related data, such as sunrise/sunset, next_rising, next_setting, next_dawn, next_dusk) and becomes more a “circadian” light project than it was before
  • takes into account differences in the actual length of days in hours, depending on seasons (winter to summer), so the light experience automatically “evolves” over the calendar year
  • Light experience that differentiates between daytime, dusk, dawn and night periods

Hi there!

I’ve been fiddling with HA since July 2017 and this is my first post. You’re all a great community and (besides my wife) you inspired me to publish something which I believe is really cool and might help others as well.

Since I am a light addict (there are scientists who confirm the positive effects of natural light on our well-being) I started playing around with my Philips Hue, in order to find a way of automating the light temperature (in kelvin or mired) during the day.

This automation simulates the sun color temperature from sunrise to sunset, from a warm light in the morning over a bright light at noon with more blue color elements, and back to a warm (almost yellow) light in the evening.

It also takes into account that summer brings longer days than winter, thus the color temperature reaches its peak faster in winter than it does during summer.

Time Color Temperature in Kelvin Color Temperature in Mired
High Noon Sun 5300-6500 kelvin 188-154
Sunrise/Sunset 2000-3000 kelvin 500-333

As Philips Hue can “digest” mired values directly through the color_temp attribute, I came up with the idea to create a background automation which seamlessly updates the mired value over the day. It can then update the color_temp on any trigger, in this example I chose a time-based check (every 15 minutes).

Here’s how I did it:

First, I created a sensor that dynamically calculates the approximative length of the day. The product is something I call the “factor u” which I need in a table calculation.

  - platform: template
    sensors:
      length_of_day_factor:
        friendly_name: 'length of day factor for circadian light calculation'
        value_template: >
          {% if is_state('sun.sun' , 'above_horizon') %}
            {% set daylength = ((as_timestamp(states.sun.sun.attributes.next_setting) - as_timestamp(states.sun.sun.attributes.next_rising)) / 3600) + 24 %}
              {{ (daylength*-0.0063616)+0.11131 }}
          {% endif %}

Please note that the daylength variable uses a trick when it comes to calculating the actual length of a day. Since I found no “ready-to-consume” value in the sun component, I simply took the values of the next day and substracted it by 24 (hours).

The length_of_day_factor actually takes into account values which I have manually and previously calculated in a table calculation sheet (unfortunately I cannot upload this sheet to the post). In summary, it translates the actual length of a day into a value which influences the speed a mired value changes over time.

Second, I built a sensor template which integrates the automatic calculation of mired over day. It looks like this:

  - platform: template
    sensors:
      farbtemp:
        friendly_name: 'circadian light - color temperature'
        unit_of_measurement: 'mired'
        value_template: >
          {% if is_state('sun.sun' , 'above_horizon') %}
            {{ ((states.sensor.length_of_day_factor.state | round (5))*((states.sun.sun.attributes.azimuth)-180)**2 + 175) | int }}
          {% elif (as_timestamp(states.sun.sun.attributes.next_dusk)) - (as_timestamp(states.sun.sun.attributes.next_setting)) < 0 or  (as_timestamp(states.sun.sun.attributes.next_rising)) - (as_timestamp(states.sun.sun.attributes.next_dawn)) < 0 %}
            350
          {% else %}
            390
          {% endif %}

Some remarks: this template runs in the background and calculates the appropriate mired (microreciprocal degree) value according to the progression of the sun (azimuth). The updated version does no longer only rely on the azimuth which I found being too volatile: In Zurich/CH it ranges from (sunrise) 53 to 306 (sunset) of azimuth in summer and 125 to 235 azimuth in winter. Given that I have adapted the calculation to my geographical location, the “circadian” mired value kicks in at the right point of time (from sunrise until sunset) and is independent from changes in “where does the sun go up and down” (azimuth). There also is no impact by daylight saving time, different time zones, differences in sun elevation (summer = sun climbs higher, winter = sun climbs lower), etc. The “real value” calculation only kicks in while the sun is above the horizon. Outside of this period it either falls back to 390 mireds (which is a cozy yellowish light) in the evening and night, or a lightbulb standard value (350 mired) during dusk/dawn periods.

You can find the kelvin-to-mired-conversion here: https://en.wikipedia.org/wiki/Mired

The algorithm can be replicated in a table calculation through curve sketching and it creates a graph that looks like this:
image

Second, I integrate the states.sensor.colortemp.state value into any light automation, e.g.:

 - alias: 'office - update mireds every 15 minutes'                                                                                
    trigger:                                                                                                                        
      platform: time                                                                                                                
      minutes: '/15'                                                                                                                
      seconds: 00                                                                                                                   
    condition:                                                                                                                      
      - condition: state                                                                                                            
        entity_id: light.office                                                                                                     
        state: 'on'                                                                                                                 
    action:                                                                                                                         
      - service: light.turn_on                                                                                                      
        data_template:                                                                                                              
          entity_id: light.office                                                                                                   
          color_temp: "{{ states.sensor.colortemp.state }}"

Any comments or questions are welcome! Enjoy!

9 Likes

cool project. A newb question, will this work only during daylight?

Hi Claudio,

the current setup is meant to be for a natural daylight simulation, and my definition of “day” is as soon as the sun goes up (sunrise around 80 degrees azimuth) until the sun goes down (sunset around 280 degrees azimuth). The mired update continues working automatically, even beyond sunset. It will then depend on your use case, how you’re going to change the algorithm to your needs…

Cheers,
Markus

Isn’t this already addressed by Flux Light Adjustment?

I had a look at Flux 'cause it has some similarities with my project. What Flux could not do for me was to be a bi-directional (increase/decrease) solution, since Flux is unidirectional (from start_colortemp to stop_colortemp).

If I understand correctly, this follows the actual position of the sun, as it changes around the year. I live in Sweden, so I would really prefer not to have an emulated indoor sunset at 15:30 mid-winter… Is there any way one could define an ideal day, or sunset & sunrise times and have that repeat all year round? The Swedish Brainlit uses April 16th (if I recall correctly) for the “ideal” workday as office lighting schedule.

That’s an interesting thought, so let me try to elaborate on how I understand your request. If you opt for a “static” approach, you might want to have a look at this example:

In this way, you would be able to set static color_temp as per the hour of the day and likewise simulate the “ideal day” that you’re looking for. As my wife spent some time in Sweden during studies, I learnt the hard way, how frustrating short daylight-periods can be during the winter, so totally understand why you’re looking for this approach :wink:

3 Likes

HI

Trying your approach, please let me ask first if here you point to the sensor.farbtemp? there’s no sensor.colortemp in your post so I hope im not mistaken…

Checking the states of my Hue-Lights, I cant find the color temp any longer… are we sure this still works as you describe, od do we need th adapt further, along the changes in the Hue/Hassio setup lately:

I have tried with the light.Hue_go_1:

customize to show in the frontend:

light.hue_go_1:
  icon: mdi:transfer
  extra_data_template: >
    if (attributes.hs_color || attributes.rgb_color) 
        return 'Hs: ' + attributes.hs_color
        + ' | ' + 'Rgb: ' + attributes.rgb_color;
    return null;
  state_card_mode: break-slider
  stretch_slider: true
  hide_control: false
  show_last_changed: true
  confirm_controls: true
  slider_theme:
    min: 0
    max: 255
    pin: true
    off_when_min: true
    report_when_not_changed: true
  extra_badge:
    - attribute: brightness
      unit: Br
    - attribute: color_temp
      unit: Co

34

and the states:

As you can see, no more color temp…

checking the Hue Api for this light, there is ct , but this isnt supported on the Hassio instance?:

10":{"state":{"on":true,"bri":136,"hue":7676,"sat":199,"effect":"none","xy":[0.5016,0.4151],"ct":443,"alert":"none","colormode":"xy","mode":"homeautomation","reachable":true},"swupdate":{"state":"noupdates","lastinstall":"2018-02-19T13:27:28"},"type":"Extended color light","name":"Hue go 1","modelid":"LLC020","manufacturername":"Philips","productname":"Hue go","capabilities":{"certified":true,"control":{"mindimlevel":40,"maxlumen":300,"colorgamuttype":"C","colorgamut":[[0.6915,0.3083],[0.1700,0.7000],[0.1532,0.0475]],**"ct":{"min":153,"max":500}**},"streaming":{"renderer":true,"proxy":true}},"config":{"archetype":"huego","function":"decorative","direction":"omnidirectional"},"uniqueid":"unique idxxxxx","swversion":"5.105.0.21169"}

since your circadian sensor calculates the ct to be this:

51
something must be wrong :wink:

still, I see the light change but right now it keeps turning down, instead of turning to the regular evening setting…

What should I check?


update:

setting it manually in the dev tools works!, and also reveals the color_temp attribute in the state machine:

see the frontend:

22

I am somewhat puzzled…

btw any thoughts on the brightness to go with the color_temp? Is there a circadian setting for that too?

thx!
Marius

My apologies: yes, I mean sensor.farbtemp. Sorry for that.

I have this config up and running in version 0.60.1 as per today. No idea if there have been any “breaking changes” to the HUE API in the meantime. Can anybody else find out?

Your setup is working fine for me. I had to wiggle a few things to support the new automations.yaml formatting that was introduced some time ago, and I caught the sensor.farbtemp issue too, but aside from that, this is working great for me on 0.72.1.

working right now, just have to figure out why the light switches off , or on with rgb/hs settings, in which case the color_temp is not available of course. (believed the circadian automation to do that, but now believe something else is doing it, since the circadian sensor is correct, and now also showing on the light in the frontend)

4433

the thing that might confuse the light is, that this is a color lamp, and color_temp is mutually exclusive for hs_color, or any other color setting. So when any Hue scene is set on this light that actually uses a real color setting this circadian sensor and setting is lost…

why would you only calculate the length of day when the sun is up? if the sun is down, the sensor has no value… For the other sensor, and the automation depending on it, it doesn’t make any difference if the length of day sensor is simply as follows doesn’t it? Since you’re calling the condition of the sun being up/above horizon there also?:

  length_of_day_factor:
    friendly_name: 'Length of day' # factor for circadian light calculation'
    value_template: >
        {% set daylength = ((as_timestamp(states.sun.sun.attributes.next_setting) - 
                             as_timestamp(states.sun.sun.attributes.next_rising)) / 
                             3600) + 24 %}
          {{ ((daylength*-0.0063616)+0.11131)|round(5) }}

just don’t like empty sensors…

Correct, as this is true for any other value which can get overridden as soon as a new automation kicks in. That’s why my automation re-calculates the circadian light every time this automation gets triggered.

This is true. In reality, the “length of day factor” does calculate the length of the NEXT day because I did not find a way to calculate the day-length for the current day. Therefore, as soon as the sun sets (= it’s getting night), the states.sun.sun.attributes.next_setting switches to a point in time which will happen the day after, so the result of the equation gets inverted (negative) and the sensor becomes useless. I would recommend using this sensor only in helper mode, without even being displayed in the GUI.

This is the sensor.farbtemp you’re referring to, and be aware that my mechanism aims at a) displaying circadian light as long as the sun is above horizon, b) then switching to a predefined value during dusk/dawn

and c) goes to a predefined value as soon as it’s neither day, nor dusk, nor dawn.

You can find more information on my personal dusk/dawn calculation here, however it’s not perfect yet: Automations with dusk, dawn, night and day period

So in fact, it goes a little further than what you suggested above and that’s whay I would still need to build in a condition to check whether the sun is above horizon.

thank you! very useful and nice tool. learning both about Hue and mired here. Which is why I started with the Hue light system in the first place, so, again thanks!

for the icons you might be able to use this template I use in another sensor, ive adapted it quickly to your period_of_day:

  icon_template: >-
    {% if (as_timestamp(states.sun.sun.attributes.next_dusk)) - (as_timestamp(states.sun.sun.attributes.next_setting)) < 0 %}
      mdi:weather-sunset-down
    {% elif (as_timestamp(states.sun.sun.attributes.next_rising)) - (as_timestamp(states.sun.sun.attributes.next_dawn)) < 0 %}
      mdi:weather-sunset-up
    {% elif (states.sun.sun.attributes.elevation) < 0 %}
      mdi:weather-night
    {% else %}
      mdi:weather-sunny
    {% endif %}

Hi @mastermarkush,

Thank you for your excellent formula, it works spledidly at my home. Could you please provide your calculation sheet also? My wife finds the light too “cold” during noon, so I would like to reduce the peak from 175 mireds to something around 200…

As an idea for all adopters of circadian lights: I defined a Hue scene for each room called “Circadian Light”. I update this scene via REST service to the Hue API every 10 minutes so that it always includes the current color_temp from the sensor. My Hue dimmer switches all use the “Circadian” scenes, so the lights are immediately set to their correct color_temp as soon as I switch them on. In HA, I defined templates switches for the lights (turn_on action activates the “Circadian” scene") so I can facilitate the same logic from HA too.

In addition to that, I also established an automation that updates the color_temp of active lights every 10 minutes. This automation compares the color_temp the light has at the moment with the sensor value. If they do not differ too much (i. e. <15 mired, will probably be adjusted over the next months…), the light is updated. That way I avoid that if I manually activate another scene, it gets overwritten after 10 minutes by the circadian light.

HI @cicero222, thanks for your suggestions and ideas!
would you share your code please, so we could easily adopt and implement into our own settings?

Ive been testing the circadian sensor with several of my lights, and indeed enjoy the results, your bigger setting might be very useful.
thanks,
Marius

REST service to update the HUE scenes:

rest_command:
  set_dynamic_hue_scene:
    url: "http://192.168.1.33/api/<YOUR_API_CODE_HERE>/scenes/{{scene_id}}/lightstates/{{light_id}}"
    method: "put"
    content_type: "application/json"
    payload: '{"on":true,"bri":{{states.sensor.circadian_brightness.state}},"ct":{{states.sensor.circadian_color_temp.state}}'

The service updates both color_temp and brightness for which I have another sensor defined. Unfortunately you have to update each single light within a scene and cannot update the whole scene with one call… :slightly_frowning_face:
Reference: https://developers.meethue.com/documentation/scenes-api#43_modify_scene

This automation kicks in every 10 minutes and sets the color_temp to the up-to-date value:

automation:
  - alias: "Update Hue Circadian Scenes"
    trigger:
      - platform: time
        minutes: "/10"
        seconds: 05
      
    action:
      - service: rest_command.set_dynamic_hue_scene
        data_template:
          scene_id: "mkaM4wCS0zTytUr"
          light_id: 1
      - service: rest_command.set_dynamic_hue_scene
        data_template:
          scene_id: "mkaM4wCS0zTytUr"
          light_id: 2

This automation updates all active lights every 10 minutes with the current color_temp value:

automation:
  - alias: "Update Circadian Lights"
    trigger: 
      - platform: time
        minutes: "/10"
        seconds: 00
      
    action:     
      - service: light.turn_on
        data_template:
          entity_id: >-
            {% for lamp in states.light if lamp.state == 'on' -%}
              {% if lamp.attributes.color_temp %}
                {% set currentvalue = lamp.attributes.color_temp %}
              {% elif lamp.attributes.xy_color %}
                {% set currentvalue = (-449*((lamp.attributes.xy_color[0]-0.332)/(lamp.attributes.xy_color[1]-0.1858))**3)+(3525*((lamp.attributes.xy_color[0]-0.332)/(lamp.attributes.xy_color[1]-0.1858))**2)-(6823.3*((lamp.attributes.xy_color[0]-0.332)/(lamp.attributes.xy_color[1]-0.1858)))+(5520.33) %}
                {% set currentvalue = (1 / currentvalue) * 1000000 %}
              {% else %}
                {% set currentvalue = 0 %}
              {% endif %}
              {% if (currentvalue - states.sensor.circadian_color_temp.state|float)|abs < 15 %}
                {{lamp.entity_id}},
              {% endif %}
            {% endfor -%}
            light.dummy
          color_temp: "{{ states.sensor.circadian_color_temp.state }}"
          transition: 3

As also other users have noticed, Hue lights may have either a color_temp attribute in HA or an xy color attribute, depending on the last action performed on them. So I have to check which one is defined. If it is xy, I have to convert it to Kelvin and then to mired. I found one formula here (although there seem to be also others): https://developers.meethue.com/comment/3046#comment-3046

Combining all lamps whose current values do not differ too much from the sensor value into one list is just beyond my programming skills, so I compose a list that always contains one dummy light. I would really appreciate if someone could show me a cleaner way to handle this (and also cater with the case that no light at all shall be updated)!

That’s easy. In this case, the just modify the value 175 to 200 in the sensor.colortemp at the end of the formula (see above).

No, it’s not. That way the entire parabola would move up and would not deliver 350 mireds at the beginning/end of the day cycle but 375. I would need to change the u-factor in your formula. But anyway, for the moment it’s fine…

[Edit]: Just saw your PM, thanks a lot for the file!

You’re right. I forgot about the dependencies. You’ll see the calculation file and understand how to set it up correctly.