Dynamic brightness and color temperature based on time of the day - setup with Lovelace sliders

You are reading a how to for ‘triggered dynamic brightness based on time of the day’ or call it ‘time to brightness.’
This is very easy to implement, it’s just a somewhat big post to explain the ins and outs.

Latest update here: Dynamic brightness and color temperature based on time of the day - setup with Lovelace sliders - #7 by Saturnus

Here is how it looks in Lovelace:
Lovelace

The following setup will be useful when you want to illuminate an area with different brightness levels at different times. For example: brightness 16 at night, brightness 192 during the day, fade in from night to day 6:00-9:00, and fade out from day to night 22:00-23:30.

These brightness levels will be set when the automation is triggered, so the light does not have to be on all day. This setup also does not interfere with other automations that set specific brightness levels because you can obviously use conditions. For example use this setup with a motion sensor, and override it with a manual button that maybe temporarily disables the automation we will set up now.

To get a better understanding of dynamic brightness, have a look at the graph I made at Desmos.

The graph describes the brightness of the light during the day (only when triggered). It has the minutes of the day (0-1439) on the x-axis and the brightness (0-255) on the y-axis. Now in Desmos, open the “3c. Formulas components (dynamic)” folder in the left pane. What appears are the sliders just as how they will work in HA via Lovelace after setup.

  1. The lower level brightness (m / y1)
  2. The upper level brightness (l / y2)
  3. The fade in start time (a / x1)
  4. The fade in finish time (b / x2)
  5. The fade out start time (c / x3)
  6. The fade out finish time (d / x4)

Play with the sliders to get an understanding.

To the code then…

Latest update here: Dynamic brightness and color temperature based on time of the day - setup with Lovelace sliders - #7 by Saturnus

Original post:
In the code below, the example of hallway_light is used. It is easy to change this but make sure you do change everything systematically then.

We need 6 input_numbers in the configuration.yaml per light:

input_number:
  hallway_light_x1:
    name: Hallway light x1
    min: 0
    max: 12
    step: 0.25
  hallway_light_x2:
    name: Hallway light x2
    min: 0
    max: 12
    step: 0.25
  hallway_light_x3:
    name: Hallway light x3
    min: 12
    max: 24
    step: 0.25
  hallway_light_x4:
    name: Hallway light x4
    min: 12
    max: 24
    step: 0.25
  hallway_light_y1:
    name: Hallway light y1
    min: 0
    max: 255
    step: 5
  hallway_light_y2:
    name: Hallway light y2
    min: 0
    max: 255
    step: 5

This is the automation for HA:

automation:
- id: '001'
  alias: Hallway light dynamic brightness < Motion
  trigger:
  - platform: state
    entity_id: binary_sensor.motion_sensor
    to: 'on'
  condition:
  - condition: state
    entity_id: light.hallway
    state: 'off'
  action:
  - service: light.turn_on
    data_template:
      entity_id: light.hallway
      color_temp: 250
      brightness: >-
        {% if ( ( states('input_number.hallway_light_x1') | float ) *60 ) > ( ( ( now().hour ) *60 ) + ( now().minute ) ) >= ( 0 ) %}
        {{ states('input_number.hallway_light_y1') | int }}
        {% elif ( ( states('input_number.hallway_light_x1') | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states('input_number.hallway_light_x2') | float ) *60 ) %}
        {{ ( (now().hour*60 + now().minute) * (states('input_number.hallway_light_y2' ) | int - states('input_number.hallway_light_y1') | int ) / (states('input_number.hallway_light_x2') | int *60 - states('input_number.hallway_light_x1') | int *60 ) + (states('input_number.hallway_light_y2' ) | int - states('input_number.hallway_light_y1') | int ) / (states('input_number.hallway_light_x2') | int *60 - states('input_number.hallway_light_x1') | int *60 ) * -1 *states('input_number.hallway_light_x1') | int *60 + states('input_number.hallway_light_y1') | int ) | int }}
        {% elif ( ( states('input_number.hallway_light_x2') | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states('input_number.hallway_light_x3') | float ) *60 ) %}
        {{ states('input_number.hallway_light_y2') | int }}
        {% elif ( ( states('input_number.hallway_light_x3') | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states('input_number.hallway_light_x4') | float ) *60 ) %}
        {{ ( (now().hour*60 + now().minute) * (states('input_number.hallway_light_y1' ) | int - states('input_number.hallway_light_y2') | int ) / (states('input_number.hallway_light_x4') | int *60 - states('input_number.hallway_light_x3') | int *60 ) + (states('input_number.hallway_light_y1' ) | int - states('input_number.hallway_light_y2') | int ) / (states('input_number.hallway_light_x4') | int *60 - states('input_number.hallway_light_x3') | int *60 ) * -1 *states('input_number.hallway_light_x3') | int *60 + states('input_number.hallway_light_y2') | int ) | int }}
        {% elif ( ( states('input_number.hallway_light_x4') | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( 1440 ) %}
        {{ states('input_number.hallway_light_y1') | int }}
        {% else %}
        {{ ( 3 ) | int }}
        {% endif %}

The if state is selected based on which minute of the day it currently is, and according to that the brightness will be calculated.

Here the UI Lovelace YAML code to get the sliders:

views:
  - title: Preferences
    icon: mdi:tune-vertical
    cards:
    - type: vertical-stack
      cards:
      - type: entities
        title: Hallway
        show_header_toggle: false
        entities:
        - entity: input_number.hallway_light_y1
          name: Brightness night
          icon: mdi:brightness-3
        - entity: input_number.hallway_light_y2
          name: Brightness day
          icon: mdi:white-balance-sunny
        - entity: input_number.hallway_light_x1
          name: Fade in start
          icon: mdi:arrow-expand-up
        - entity: input_number.hallway_light_x2
          name: Fade in done
          icon: mdi:check
        - entity: input_number.hallway_light_x3
          name: Fade out start
          icon: mdi:arrow-expand-down
        - entity: input_number.hallway_light_x4
          name: Fade out done
          icon: mdi:check

If you used Desmos to generate your preferences, then note that [a-d] / x[1-4] are in minutes in Desmos, but in ‘mathematical hours’ in Lovelace. Thus 540 equals 09:00 and thus 9 in Lovelace. 570 equals 09:30 and thus 9,5 in Lovelace. 585 equals 09:45 and thus 9,75 in Lovelace.

Notes that shouldn’t matter but provide more detail in case you are going to customize:
This setup has been working without problems for 6 weeks now, except what seems to be an unrelated 1-2x per week minor issue with my Hue colour bulb. Wrong brightness from time in template if statement

Any minute of the day equal or greater than 1440 and smaller than 0 will result in brightness=3. This should not happen because ‘minute 1440’ does not exist; it will be 0 again. Let alone anything >1440 and <0. This is added for debugging/closing the if statement.

All the if states are exclusive. Even though the first one that is valid will be executed, there still is only one valid each time.

You can change the input_number details (min, max, step) of the sliders, but do note that weird things will happen when you: 1) Change the order of the x/y-points in the graph; e.g. have x1 after x3. 2) Use numbers outside the interval of 0-24 for min and max. These will not be converted to corresponding next cycle hours; e.g. 25 → 1 is not happening. This also means that there is a limitation for the fade out finish time (d / x4); 24 is the maximum in this setup. My suggestions is to keep this general shape when trying out via Desmos: low, increasing and reaching max before 12:00, going steady for a while, then decreasing.

Why input_number and not input_datetime? Because the latter one is ridiculous to adjust in Lovelace.

Brightness is set on minute precision. Minute 1440 however will never occur (because 24:00 does not exist, it will be 0:00 instead) and thus the corresponding brightness will never be set. This might result in brightness at 00:00 being off, but only in extremely weird unlogical use cases.

Currently brightness is only updated when the automation is triggered, although it is very easy to cause ‘retriggers’ for as long as something (motion maybe) is detected.

Future improvements?

  • I will try to covert it to a script ‘soon’ that uses the entity_id as variable, so that one instance can be easily used for multiple lights. Done.

Problems?
Let me know. In this topic I had to generalize some of my personal data, maybe I did something wrong so feedback is welcome.

13 Likes

Thank you very much! Just implemented it, and it seems to work! I will play with it in the next couple days to implement it everywhere :slight_smile:
Because I wanted the same brightness for every bulb with motion detection, I changed the input_number name from hallway to just brightness. Seems more suitable for my config.

Perhaps I missed the explanation but how is this different from https://www.home-assistant.io/components/flux/ ?
Does it do something the flux component can’t?

Didn’t try Flux myself but from reading the page my understanding was that it doesn’t do brightness how I am doing that now. It does have a brightness option, but what does it mean? That’s the max and it will fade to 0? Seems like you can’t set a lower bound to get a night light. Also, fade in and fade out durations are equal, while I for example want slow fade in, quick fade out. On the fly adjustments in Lovelace and ability to disable it by simply disabling an automation also seems not possible with Flux. Flux is not flexible enough for me.

Someone on another forum pointed me at Circadian Lighting [Custom Component]. That one seems much more flexible, it does include night light options and also complete brightness control. But I guess it will be synced with the sun always. Offsets for that are available, but if someone always goes to bed 23:00 despite which of the 365 days it is… Ja, I am not sure how that will work then. It will all depend on use case.

The thing I am running can very easily be converted to also do colour temperature, if that would be a miss for someone. Or maybe it runs fine next to other tools. Let me know what you all make of it. :ok_hand:

FWIW, You can specify sunrise and sunset times in Circadian Lighting, so it will always be the same. That’s not recommended because our bodies are accustomed to the sun changing with the season, but it doesn’t have to be synced with the sun if you don’t want.

Alright, so there are some options available. :ok_hand:

With some coding help the script version is running now, I will test it for a few days before updating the topic start.

1 Like

For the concept of use, please read the topic start.

The script version has been working well for 8 days.
Colour temperature has been added also in this update. I tested with Philips/Signify Hue. If other lights use a different colour temperature scale etc., let me know.

Thanks to RobertMe @ Tweakers.net for coding help.

Instead of updating the topic start I will post it here and will link to the latest update each time from the topic start.

Starting with the input_numbers again in configuration.yaml. Per light we need:

input_number:
  light_hallway_brightness_x1:
    name: Light hallway brightness x1
    min: 0
    max: 12
    step: 0.25
  light_hallway_brightness_x2:
    name: Light hallway brightness x2
    min: 0
    max: 12
    step: 0.25
  light_hallway_brightness_x3:
    name: Light hallway brightness x3
    min: 12
    max: 24
    step: 0.25
  light_hallway_brightness_x4:
    name: Light hallway brightness x4
    min: 12
    max: 24
    step: 0.25
  light_hallway_brightness_y1:
    name: Light hallway brightness y1
    min: 0
    max: 255
    step: 5
  light_hallway_brightness_y2:
    name: Light hallway brightness y2
    min: 0
    max: 255
    step: 5

  light_hallway_color_temp_x1:
    name: Light hallway color temp x1
    min: 0
    max: 12
    step: 0.25
  light_hallway_color_temp_x2:
    name: Light hallway color temp x2
    min: 0
    max: 12
    step: 0.25
  light_hallway_color_temp_x3:
    name: Light hallway color temp x3
    min: 12
    max: 24
    step: 0.25
  light_hallway_color_temp_x4:
    name: Light hallway color temp x4
    min: 12
    max: 24
    step: 0.25
  light_hallway_color_temp_y1:
    name: Light hallway color temp y1
    min: 153
    max: 500
    step: 25
  light_hallway_color_temp_y2:
    name: Light hallway color temp y2
    min: 153
    max: 500
    step: 25

An automation in automation.yaml could look like this:

- id: '002'
  alias: Hallway lamp dynamic brightness < Motion
  trigger:
  - platform: state
    entity_id: binary_sensor.motion_sensor
    to: 'on'
  condition:
  - condition: state
    entity_id: light.hallway
    state: 'off'
  action:
  - service: script.dlight
    data:
      entity_id: light.hallway

In the automation above, a script is called. Therefore we of course need a script in scripts.yaml:

  dlight:
    sequence:
    - service: light.turn_on
      data_template:
        entity_id: "{{entity_id}}"
        brightness: >-
          {% if ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x1'].state | float ) *60 ) > ( ( ( now().hour ) *60 ) + ( now().minute ) ) >= ( 0 ) %}
          {{ states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y1'].state | int }}
          {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x1'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x2'].state | float ) *60 ) %}
          {{ ( (now().hour*60 + now().minute) * (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y2'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y1'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x2'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x1'].state | float *60 ) + (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y2'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y1'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x2'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x1'].state | float *60 ) * -1 *states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x1'].state | float *60 + states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y1'].state | int ) | int }}
          {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x2'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x3'].state | float ) *60 ) %}
          {{ states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y2'].state | int }}
          {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x3'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x4'].state | float ) *60 ) %}
          {{ ( (now().hour*60 + now().minute) * (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y1'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y2'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x4'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x3'].state | float *60 ) + (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y1'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y2'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x4'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x3'].state | float *60 ) * -1 *states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x3'].state | float *60 + states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y2'].state | int ) | int }}
          {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_x4'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( 1440 ) %}
          {{ states.input_number['light_' ~ entity_id.split('.')[1] ~ '_brightness_y1'].state | int }}
          {% else %}
          {{ ( 255 ) | int }}
          {% endif %}
        color_temp: >-
          {% if states('input_number.light_' ~ entity_id.split('.')[1] ~ '_color_temp_x3') | int == 0 %}
          {{ ( 250 ) | int }}
          {% else %}
            {% if ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x1'].state | float ) *60 ) > ( ( ( now().hour ) *60 ) + ( now().minute ) ) >= ( 0 ) %}
            {{ states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y1'].state | int }}
            {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x1'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x2'].state | float ) *60 ) %}
            {{ ( (now().hour*60 + now().minute) * (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y2'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y1'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x2'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x1'].state | float *60 ) + (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y2'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y1'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x2'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x1'].state | float *60 ) * -1 *states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x1'].state | float *60 + states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y1'].state | int ) | int }}
            {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x2'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x3'].state | float ) *60 ) %}
            {{ states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y2'].state | int }}
            {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x3'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x4'].state | float ) *60 ) %}
            {{ ( (now().hour*60 + now().minute) * (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y1'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y2'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x4'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x3'].state | float *60 ) + (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y1'].state | int - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y2'].state | int ) / (states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x4'].state | float *60 - states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x3'].state | float *60 ) * -1 *states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x3'].state | float *60 + states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y2'].state | int ) | int }}
            {% elif ( ( states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_x4'].state | float ) *60 ) <= ( ( ( now().hour ) *60 ) + ( now().minute ) ) < ( 1440 ) %}
            {{ states.input_number['light_' ~ entity_id.split('.')[1] ~ '_color_temp_y1'].state | int }}
            {% else %}
            {{ ( 250 ) | int }}
            {% endif %}
          {% endif %}

Finally, the UI Lovelace preferences sliders:

  - title: Preferences
    icon: mdi:tune-vertical
    cards:
    - type: vertical-stack
      cards:
      - type: entities
        title: Light hallway
        show_header_toggle: false
        entities:
        - type: section
          label: Light hallway brightness
        - entity: input_number.light_hallway_brightness_y1
          name: Brightness night
          icon: mdi:brightness-3
        - entity: input_number.light_hallway_brightness_y2
          name: Brightness day
          icon: mdi:white-balance-sunny
        - entity: input_number.light_hallway_brightness_x1
          name: Fade in start
          icon: mdi:arrow-expand-up
        - entity: input_number.light_hallway_brightness_x2
          name: Fade in done
          icon: mdi:check
        - entity: input_number.light_hallway_brightness_x3
          name: Fade out start
          icon: mdi:arrow-expand-down
        - entity: input_number.light_hallway_brightness_x4
          name: Fade out done
          icon: mdi:check
        - type: section
          label: Light hallway color temp
        - entity: input_number.light_hallway_color_temp_y1
          name: Color temp night
          icon: mdi:brightness-3
        - entity: input_number.light_hallway_color_temp_y2
          name: Color temp day
          icon: mdi:white-balance-sunny
        - entity: input_number.light_hallway_color_temp_x1
          name: Fade in start
          icon: mdi:arrow-expand-up
        - entity: input_number.light_hallway_color_temp_x2
          name: Fade in done
          icon: mdi:check
        - entity: input_number.light_hallway_color_temp_x3
          name: Fade out start
          icon: mdi:arrow-expand-down
        - entity: input_number.light_hallway_color_temp_x4
          name: Fade out done
          icon: mdi:check

For each different light you will have to add another code block 1 (input_numbers), 2 (automation) and 4 (UI Lovelace). Block 3 (script) is only needed once.

In order to use your own light, change ‘hallway’ in blocks 1, 2 and 4 to for example ‘toilet’ or ‘bathroom’. Do not change anything else or it won’t work anymore.

Very important: If you have a light that does not support color_temp then don’t setup all the color_temp parts for that specific light. E.g. leave out light_hallway_color_temp_x1 etc. and the Lovelace parts.
But if you do use a light with the script that supports color_temp like the way above, then you MUST use it or accept that the scripts defaults to color_temp ‘250’. I was unable to come up with a better/safer way of the script to work, in order to prevent even weirder outcomes and errors. Likely impossible to have a better workaround than this in Jinja2.

Lovelace:

The graph on Desmos is updated as well: https://www.desmos.com/calculator/v0wwzucyum

5 Likes

This is almost exactly what I was looking for and gave me inspiration to implement my own concept. I just wanted to comment to share my appreciation of this post. I posted my version of this here if anyone is interested. My version uses one automation and no scripts for all lights that are on. It also actively transitions the lights over the time period selected.

2 Likes

Isn’t it possible to pour this into a blueprint?
@Saturnus ik ga er vanuit dat je NL’s bent, werkt lekker! Top!

Interesting. I hope to look into this soon.
Groeten. :wink: