DIY Circadian Lighting. no flux and no custom components

I was recently asked on Reddit how I setup my circadian lighting at my house. There are a couple of my posts around where I was asking for help on this, but I wanted to do the full write-up.

I tried the flux component and the custom circadian lighting component. Both worked very well, but each had something about them that didn’t fit my needs/wants.

The quick breakdown is that there are two template sensors (one for brightness and one for color_temp), a group, an input_boolean and a series of automations.

Setup as follows:

Created two template sensors in sensors.yaml

  - platform: template
    sensors:
      cl_brightness:
        friendly_name: Circadian Brightness
        value_template: >
          {% set state = states('sensor.time') %}
          {% if '05:00' <= state  < '06:00' %}
            25
          {% elif '06:00' <= state < '06:30' %}
            85
          {% elif '06:30' <= state < '07:00' %}
            105
          {% elif '07:00' <= state < '07:30' %}
            125
          {% elif '07:30' <= state < '08:00' %}
            190
          {% elif '08:00' <= state < '09:00' %}
            210
          {% elif '09:00' <= state < '16:30' %}
            255
          {% elif '16:30' <= state < '17:00' %}
            255
          {% elif '17:00' <= state < '017:30' %}
            240
          {% elif '17:30' <= state < '18:00' %}
            235
          {% elif '18:00' <= state < '18:30' %}
            225
          {% elif '18:30' <= state < '19:00' %}
            200
          {% elif '19:00' <= state < '19:30' %}
            175
          {% elif '19:30' <= state < '20:00' %}
            125
          {% elif '20:00' <= state < '20:30' %}
            115
          {% elif '20:30' <= state < '21:00' %}
            110
          {% elif '21:00' <= state < '22:00' %}
            100
          {% elif '22:00' <= state < '22:30' %}
            85
          {% elif '22:30' <= state < '23:00' %}
            25
          {% else %}
            1
          {% endif %}
      cl_color_temp:
        friendly_name: Circadian Color Temperature
        value_template: >
          {% set state = states('sensor.time') %}
          {% if '05:00' <= state  < '06:00' %}
            500
          {% elif '06:00' <= state < '06:30' %}
            450
          {% elif '06:30' <= state < '07:00' %}
            400
          {% elif '07:00' <= state < '07:30' %}
            350
          {% elif '07:30' <= state < '08:00' %}
            350
          {% elif '08:00' <= state < '09:00' %}
            325
          {% elif '09:00' <= state < '16:30' %}
            300
          {% elif '16:30' <= state < '17:00' %}
            350
          {% elif '17:00' <= state < '017:30' %}
            360
          {% elif '17:30' <= state < '18:00' %}
            370
          {% elif '18:00' <= state < '18:30' %}
            375
          {% elif '18:30' <= state < '19:00' %}
            400
          {% elif '19:00' <= state < '19:30' %}
            425
          {% elif '19:30' <= state < '20:00' %}
            450
          {% elif '20:00' <= state < '20:30' %}
            465
          {% elif '20:30' <= state < '21:00' %}
            475
          {% elif '21:00' <= state < '22:00' %}
            500
          {% elif '22:00' <= state < '22:30' %}
            500
          {% elif '22:30' <= state < '23:00' %}
            500
          {% else %}
            500
          {% endif %}

Circadian Brightness shows the brightness I want betwixt the specified times and Circadian Color Temperature obviously shows the color temp. The {% else %} value is RGB color for overnight nightlight giving the light a more reddish hue. This comes into play on the automation side as you’ll need to have two automations to accommodate this. (found out that this doesn’t actually work)
Thank you @123 and @Mutt for help with the template sensors and really overall help with my setup. Those folks are awesome!

in groups.yaml

circadian:
  entities:
  - light.lamp
  - light.dining_bulb
  - light.sengled_cabinet_strip
  - light.desk_lamp
  - light.back_hall_lights

add whichever entity here that you’d like to follow this circadian rhythm. At my house I have them all doing the same thing but you could create separate groups and template sensors by room or whatever if you want different areas to act differently (great room, kitchen, basement, etc.)

I also use an input_boolean used as a condition for these automations to run. When I change the color of the lights for a notification (doorbell rings, laundry done, etc.) this input_boolean is turned off. When it turns back on, triggers a “pre-color” scene.

input_boolean.yaml

  circadian_on:
    initial: 'on'
    name: Circadian On

Next comes the automations. I have 3 for the actual lighting and 2 for the boolean. The first two are for a trigger when any entity turns from off to on. The first of those is for daytime (color_temp data in the service call) and the second is for overnight (rgb_color data in the service call) as mentioned earlier.These are now just one automation since the rgb_color wasn’t represented as a string value. In this automation, the transition is 5 seconds so the color/brightness will basically change right as the entity is turned on. The third second automation triggers at each of the time changes so if any of the lights are already on, the transition is 30 seconds so the shift in brightness and color isn’t jarring. You could make this longer but I find this to be plenty sufficient.

in automation.yaml

- id: circadian_on
  alias: Circadian On Trigger
  initial_state: true
  trigger:
  - platform: state
    entity_id: light.lamp
    from: 'off'
    to: 'on'
  - platform: state
    entity_id: light.desk_lamp
    from: 'off'
    to: 'on'
  - platform: state
    entity_id: light.sengled_cabinet_strip
    from: 'off'
    to: 'on'
  - platform: state
    entity_id: light.dining_bulb
    from: 'off'
    to: 'on'
  - platform: state
    entity_id: light.back_hall_lights
    from: 'off'
    to: 'on'
  condition:
  - condition: state
    entity_id: input_boolean.circadian_on
    state: 'on'
  action:
  - service: light.turn_on
    data_template:
      entity_id: "{{ trigger.entity_id }}"
      brightness: "{{ states('sensor.cl_brightness') }}"
      color_temp: "{{ states('sensor.cl_color_temp') }}"
      transition: '5'

######################################

##slow transition automation trigger##

######################################

- id: circadian_time_transition
  alias: Circadian Time Transition
  initial_state: true
  trigger:
  - platform: time
    at: '05:00:00'
  - platform: time
    at: '06:00:00'
  - platform: time
    at: '06:30:00'
  - platform: time
    at: '07:00:00'
  - platform: time
    at: '07:30:00'
  - platform: time
    at: '08:00:00'
  - platform: time
    at: '09:00:00'
  - platform: time
    at: '16:30:00'
  - platform: time
    at: '17:00:00'
  - platform: time
    at: '17:30:00'
  - platform: time
    at: '18:00:00'
  - platform: time
    at: '18:30:00'
  - platform: time
    at: '19:00:00'
  - platform: time
    at: '19:30:00'
  - platform: time
    at: '20:00:00'
  - platform: time
    at: '20:30:00'
  - platform: time
    at: '21:00:00'
  - platform: time
    at: '22:00:00'
  - platform: time
    at: '22:30:00'
  - platform: time
    at: '23:00:00'
  condition:
  - condition: state
    entity_id: input_boolean.circadian_on
    state: 'on'
  - condition: template
    value_template: >
      {% set lights = ['group.circadian'] %}
      {{ expand(lights)|selectattr('state','eq','on')|list|length > 0 }}
  action:
  - service: light.turn_on
    data_template:
      entity_id: >
        {% set lights = ['group.circadian'] %}  
        {{ expand(lights)|selectattr('state','eq','on')
           |map(attribute='entity_id')|join(',') }}
      brightness: "{{ states('sensor.cl_brightness') }}"
      color_temp: "{{ states('sensor.cl_color_temp') }}"
      transition: '30'

These last automations are for my input_boolean. When it turns off, it creates a pre-color scene and when it turns on, it triggers that scene.

- id: color_on_scene_create
  alias: Color on Scene Create
  trigger:
  - platform: state
    entity_id: input_boolean.circadian_on
    from: 'on'
    to: 'off'
  condition: []
  action:
  - service: scene.create
    data:
      scene_id: before_color
      snapshot_entities: #######could probably just use group.circadian here######
      - light.desk_lamp
      - light.sengled_cabinet_strip
      - light.dining_bulb
      - light.lamp
      - light.back_hall_lights

##############################

- id: color_reset
  alias: Reset Colors
  trigger:
  - platform: state
    entity_id: input_boolean.circadian_on
    from: 'off'
    to: 'on'
  action:
  - service: scene.turn_on
    data:
      entity_id: scene.before_color

One thing to note is that when if you turn off the input_boolean in an automation, you may want to add a 1 second delay afterwards before you change the colors do allow the pre_color scene to be created properly. Might not matter but I’ve noticed occasionally that the pre_color scene didn’t capture everything before the colors change.

I hope this helps. Let me know if there’s any questions.

4 Likes

You’re welcome. I recall the discussion but I’m surprised that you chose to create Template Sensors that use a long-winded if-else structure. In the original discussion, the suggestion was to use a list (to store the times and values) thereby making the template more compact. For example, here are the Circadian Brightness and Color Temperature sensors:

      cl_brightness:
        friendly_name: 'Circadian Brightness'
        value_template: >
          {% set hours = [[0,1],[500,25],[600,85],[630,105],[700,125],[730,190],
                          [800,210],[900,255],[1630,255],[1700,240],[1730,235],
                          [1800,225],[1830,200],[1900,175],[1930,125],[2000,115],
                          [2030,110],[2100,100],[2200,85],[2230,25],[2300,1]] %}
          {% set t = states('sensor.time').replace(':','')|int %}
          {% set ns = namespace(level = 0) %}
          {% for h in hours if t >= h[0] %}
            {% set ns.level = h[1] %}
          {% endfor %}
          {{ ns.level }}

      cl_color_temp:
        friendly_name: 'Circadian Color Temperature'
        value_template: >
          {% set hours = [[0,1],[500,500],[600,450],[630,400],[700,350],[730,350],
                          [800,325],[900,300],[1630,350],[1700,360],[1730,370],
                          [1800,375],[1830,400],[1900,425],[1930,450],[2000,465],
                          [2030,475],[2100,500],[2200,500],[2230,500],[2300,500]] %}
          {% set t = states('sensor.time').replace(':','')|int %}
          {% set ns = namespace(level = 0) %}
          {% for h in hours if t >= h[0] %}
            {% set ns.level = h[1] %}
          {% endfor %}
          {{ ns.level }}

Anyway, it’s your choice.

BTW, in your Circadian Color Temperature sensor, the final else provides this [255, 125, 0] for color_temp instead of an integer value.

Service data attribute Optional Description
color_temp yes An integer in mireds representing the color temperature you want the light to be.
1 Like

This is actually why I went with the

I started with the list that you had perscribed but I found that I wanted an RGB value for my nightlights instead of a color temperature. I like the redder hue rather than the max 500 Kelvin.
With this, I had to created two automations: automation.circadian_day and automation.circadian_night that are the same except for the color_temp/rgb_color in the data of the light.turn_on service call.
edit: I’m now going back to this original template for a more condensed configuration.

Your suggestions so far have made my code exponentially better from where it started. And everything is working much snappier and more reliably than my original code. As you may recall that was basically having ~19 automations per entity. One for each time of day.

Also thank you for linking to the original discussion in case someone else is interested in the list-based template.

Ah, I see that now. You’re saying that only this template in the “Circadian Night On Trigger” automation

rgb_color: "{{ states('sensor.cl_color_temp') }}"

will receive this [255, 125, 0] because the automation’s condition ensures it’s after 23:00.

Honestly, I’m a bit surprised that works because Jinja2 templates can only return string values (so rgb_color fails to get a true list, only a string that looks like a list). However, if you say it works, then it works.

Well shoot. Of course you now have me second guessing myself.
Upon testing, guess what?
It doesn’t work.
I guess I hadn’t tested the overnight part of the setup in real life. OR more likely if I did get up in the middle of the night, I didn’t notice. Haha. I guess 500k actually is okay.

Well thanks for setting me straight again.

Will be editing original post and code.

I had my doubts it would work; everything produced by a template is always a string value (even though it may have the appearance of a list).

1 Like

I implemented my colour temperature using the position of the sun.

It works better at higher or lower latitudes and in timezones subject to Daylight savings:

- platform: template
  sensors:
    lights_temp:
      friendly_name: "Lights temperature"
      unit_of_measurement: 'temp'
      value_template: "{{ [400,400-(cos(((-1*[([states.sun.sun.attributes.elevation-15,-90]|max),0]|min|float)/30)*(3.14159265359/2))*289)]|min|int }}"

It produces a nice smooth curve, so you hardly notice the colours of the lights changing.

The brightness changes looks really cool btw! I might steal that one :wink:

3 Likes

I like this… I’ve been thinking of doing something like this as I live in Montana so in the winter the sun doesn’t rise until almost 8a sometimes and is dark by 5p.
In summer the sun is up by 530 sometimes and won’t go set until after 9p. Time based works fairly well but I’ve wanted to try this based on the sun.
I may have to give this a try sometime when my wife is out of town. :laughing:

Why wait? Join the rest of us in the dog house :wink: … and make changes randomly throughout the day while the missus is home.

2 Likes

I already do that haha

Hey @auto.neub. Awesome job with this, thank you. I’ve just implemented it for testing and as I only have four dimmable lights in the house with no color temp adjustments, its pretty straight forward so far.

My question though is how do I do a quick bypass if I just want to turn on a light at a certain brightness level temporarily overriding the circadian automation?

      - service: light.turn_on
        data:
          entity_id: light.lounge_light
          brightness: 30

This always sets the brightness to the circadian value.

So I use an input boolean called input_boolean.circadian_on that as a condition of the automations.

If you want to make a temporary change, turn off the boolean and the lights will stay at your current setting.

Turning off the boolean triggers the automation to create the pre-color scene. I use it for color changes but this also works for your temporary brightness change.

Then when you turn the boolean back on it triggers the pre-color scene

Hope this helps.

Thanks. Was actually quite easy. I just turn off the circadian IB before implementing the custom light brightness then turn it back on.

  - service: input_boolean.turn_off
    entity_id: input_boolean.circadian_on
  - service: light.turn_on
    data:
      entity_id: light.lounge_light
      brightness: 30
  - service: input_boolean.turn_on
    entity_id: input_boolean.circadian_on

Yessir! Glad it worked for you

I like this idea. And started playing with it as soon as I found this. I made some changes.
For triggering every every 30 minutes I am using time pattern:
please not I am using kelvin instead of color_temp as that is what I am used to work with. Also with my lamps it gives better results

- id: circadian_time_transition
  alias: Circadian Time Transition
  initial_state: true
  trigger:
  - platform: time_pattern
    minutes: "/30"
  condition:
  - condition: state
    entity_id: input_boolean.circadian_on
    state: 'on'
  - condition: template
    value_template: >
      {% set lights = ['group.circadian'] %}
      {{ expand(lights)|selectattr('state','eq','on')|list|length > 0 }}
  action:
  - service: light.turn_on
    data_template:
      entity_id: >
        {% set lights = ['group.circadian'] %}
        {{ expand(lights)|selectattr('state','eq','on')
           |map(attribute='entity_id')|join(',') }}
      brightness: "{{ states('sensor.cl_brightness') }}"
      kelvin: "{{ states('sensor.cl_color_temp') }}"
      transition: '10'

Further I made an automation that as soon as you switch on the input_boolean.circadion_on to on, it will update the lamps as well, so you do not have to wait for 30 minutes (or whatever value you are using in the time_pattern).

- id: circadian_time_start
  alias: Circadian Time Start
  initial_state: true
  trigger:
  - platform: state
    entity_id: input_boolean.circadian_on
    from: 'off'
    to: 'on'
  condition:
  - condition: template
    value_template: >
      {% set lights = ['group.circadian'] %}
      {{ expand(lights)|selectattr('state','eq','on')|list|length > 0 }}
  action:
  - service: light.turn_on
    data_template:
      entity_id: >
        {% set lights = ['group.circadian'] %}
        {{ expand(lights)|selectattr('state','eq','on')
           |map(attribute='entity_id')|join(',') }}
      brightness: "{{ states('sensor.cl_brightness') }}"
      kelvin: "{{ states('sensor.cl_color_temp') }}"
      transition: '10'

May I ask what scale this is? mired?
For me it gives something like 111 in the afternoon

The OP is using Mired [https://en.wikipedia.org/wiki/Mired] and thus needs color_temp in its data section. The relation between Mired and Kelvin is simple: Mired = 1000000 / T (with T in Kelvin).

111 mired = 9009 Kelvin
255 mired = 3922 Kelvin
500 mired = 2000 Kelvin

1 Like

Thanks. How do you calculate the color in your setup?

In the same way, but with other values for the color temperatures. Below is what I have right now. I still need to tweak the exact values, but you can get the idea. I get up at 7:00 so then I want to have the highest value for the color temperature (5000), during the day it will be a bit less and after 17:00 (5pm) I want it to slowly get warmer. At around 19:30 (7:30pm) I want it to be at it warmest.

- platform: template
  sensors:
    cl_brightness:
      friendly_name: Circadian Brightness
      value_template: >
        {% set hours = [[0,1],[500,10],[600,55],[630,105],[700,195],[730,220],
                        [800,230],[900,255],
                        [1830,225],[1900,200],[1930,125],[2000,115],
                        [2030,110],[2100,100],[2200,85],[2230,25],[2300,1]] %}
        {% set t = states('sensor.time').replace(':','')|int %}
        {% set ns = namespace(level = 0) %}
        {% for h in hours if t >= h[0] %}
          {% set ns.level = h[1] %}
        {% endfor %}
        {{ ns.level }}

    cl_color_temp:
      friendly_name: 'Circadian Color Temperature'
      value_template: >
        {% set hours = [[0,1800],[600,2500],[630,3500],[645,4500],[700,5000],
                        [800,4500],[1705,4500],[1730,4000],
                        [1800,3500],[1830,3000],[1900,2550],[1930,2000],[2000,1900],
                        [2030,1800]] %}
        {% set t = states('sensor.time').replace(':','')|int %}
        {% set ns = namespace(level = 0) %}
        {% for h in hours if t >= h[0] %}
          {% set ns.level = h[1] %}
        {% endfor %}
        {{ ns.level }}

This I have in my sensors.yaml

1 Like

Can you explain your formula?