A blueprint to adjust lights throughout the day based on a time range

Hey, everybody. I made a blueprint that allows you to set a circadian‑rhythm‑type lighting based on a time range. The idea is to change the lights over the course of the day and also change the color throughout the day. I’m not sure if something like this exists, but I felt I needed to share it with people.

The reason I made this is because I have ADHD and trouble getting to bed, so I decided to create a light schedule for my office lights. I hope it helps anyone.

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

The code is below

blueprint:
  name: Circadian Kelvin Ramp
  description: >
    Gradually ramps selected lights across a color temperature range over a
    configurable time window.


    Defaults ramp from 6535K (cool) at 4:00 AM down to 2000K (warm) at 5:00 PM.
    Swap the start/end kelvin values to invert the direction.


    Safety guard: if ANY selected light that is currently ON has a color outside
    the guard range — including any light in an RGB/HS/XY color mode, which has
    no kelvin value at all — the entire run is skipped and no light is touched.
    Lights that are OFF are ignored by the guard and are never turned on by this
    automation.


    Outside the time window the automation does nothing; lights hold whatever
    value they last had.
  domain: automation

  input:
    lights:
      name: Lights
      description: The lights to ramp. Only lights that are already on will be adjusted.
      selector:
        entity:
          multiple: true
          filter:
            - domain: light

    start_time:
      name: Start time
      description: When the ramp begins.
      default: "04:00:00"
      selector:
        time: {}

    end_time:
      name: End time
      description: >
        When the ramp finishes. If this is earlier than the start time, the
        window is treated as crossing midnight (e.g. 18:00 to 06:00).
      default: "17:00:00"
      selector:
        time: {}

    start_kelvin:
      name: Start kelvin
      description: Color temperature at the start of the window.
      default: 6535
      selector:
        number:
          min: 2000
          max: 6535
          step: 1
          unit_of_measurement: K
          mode: slider

    end_kelvin:
      name: End kelvin
      description: Color temperature at the end of the window.
      default: 2000
      selector:
        number:
          min: 2000
          max: 6535
          step: 1
          unit_of_measurement: K
          mode: slider

    guard_min_kelvin:
      name: Guard minimum kelvin
      description: >
        If any selected light is on and below this value, the whole run is
        skipped.
      default: 2000
      selector:
        number:
          min: 1000
          max: 10000
          step: 1
          unit_of_measurement: K
          mode: box

    guard_max_kelvin:
      name: Guard maximum kelvin
      description: >
        If any selected light is on and above this value, the whole run is
        skipped.
      default: 6535
      selector:
        number:
          min: 1000
          max: 10000
          step: 1
          unit_of_measurement: K
          mode: box

    update_interval:
      name: Update interval
      description: >
        How often the lights are recalculated. Shorter is smoother but adds
        more network traffic.
      default: "/5"
      selector:
        select:
          options:
            - label: Every minute
              value: "/1"
            - label: Every 5 minutes
              value: "/5"
            - label: Every 10 minutes
              value: "/10"
            - label: Every 15 minutes
              value: "/15"

    transition:
      name: Transition
      description: >
        Fade duration in seconds for each update. Setting this at or near the
        update interval makes the change continuous rather than stepped. Set to
        0 for lights that do not support transitions.
      default: 300
      selector:
        number:
          min: 0
          max: 900
          step: 1
          unit_of_measurement: s
          mode: box

mode: single
max_exceeded: silent

variables:
  lights: !input lights
  start_t: !input start_time
  end_t: !input end_time
  start_k: !input start_kelvin
  end_k: !input end_kelvin
  guard_min: !input guard_min_kelvin
  guard_max: !input guard_max_kelvin
  ents: >
    {{ lights if lights is not string else [lights] }}
  lights_on: >
    {{ expand(ents)
       | selectattr('state', 'eq', 'on')
       | map(attribute='entity_id')
       | list }}

triggers:
  - trigger: time_pattern
    minutes: !input update_interval

conditions:
  # 1. Only run inside the configured window. This condition handles
  #    cross-midnight windows on its own.
  - condition: time
    after: !input start_time
    before: !input end_time

  # 2. At least one selected light is on. Nothing to do otherwise.
  - condition: template
    value_template: "{{ lights_on | count > 0 }}"

  # 3. Guard. Abort the whole run if any light that is on sits outside the
  #    kelvin range, or is in a color mode that has no kelvin value at all.
  - condition: template
    value_template: >
      {% set ns = namespace(ok = true) %}
      {% for e in lights_on %}
        {% set k = state_attr(e, 'color_temp_kelvin') %}
        {% if k is none or k < guard_min or k > guard_max %}
          {% set ns.ok = false %}
        {% endif %}
      {% endfor %}
      {{ ns.ok }}

actions:
  - variables:
      target_kelvin: >
        {% set t = namespace(s = today_at(start_t), e = today_at(end_t)) %}
        {% if t.e <= t.s %}
          {# Window crosses midnight. Decide which side of it we are on. #}
          {% if now() < t.e %}
            {% set t.s = t.s - timedelta(days=1) %}
          {% else %}
            {% set t.e = t.e + timedelta(days=1) %}
          {% endif %}
        {% endif %}
        {% set total = (t.e - t.s).total_seconds() %}
        {% set raw = (now() - t.s).total_seconds() / total %}
        {% set frac = [[raw, 0] | max, 1] | min %}
        {{ (start_k + (end_k - start_k) * frac) | round | int }}

  - action: light.turn_on
    target:
      entity_id: "{{ lights_on }}"
    data:
      color_temp_kelvin: "{{ target_kelvin }}"
      transition: !input transition```

A search would solve that…

Flux core integration
Circadian Lighting custom integration
Multiple Blueprints

Why are you expanding the entities into full state objects instead of just selecting using the is_state test?

If you’re going to use an unnecessary loop, why not at least break the loop as soon as you have a false result instead of processing all the remaining entities to no purpose?

1 Like