Skip to main content
Selora Homes Selora Homes
Recipe · v1.0.0 · Released Aug 28, 2026

Circadian Adaptive Lighting

Installs a Home Assistant automation that walks the colour and brightness of your dimmable lights through the day, cool and bright around midday and warm and low in the evening. It only ever changes lights that are already on, so it never switches a light on by itself.

Fixed lighting is the same colour at breakfast and at bedtime, which is the one thing daylight never is. This recipe moves your dimmable lights along a daily curve instead. Around the middle of the day they sit cool and bright, and as the evening comes in they warm and drop down toward a low, restful light.

It runs every fifteen minutes and on every Home Assistant restart, so a light switched on at any point picks up the right setting within a few minutes. The important part is what it does not do. It only ever adjusts a light that is already on, so a room you left dark stays dark. Nothing in this recipe turns a light on.

Setting the curve

You set four points. The morning and evening times mark where the day begins and ends for the lights, and the recipe puts the coolest, brightest setting at the midpoint between them. The day and night colour temperatures set the two ends of the warm-to-cool range, and the day and night brightness levels do the same for how bright the lights sit. The defaults run from a warm, low evening to a bright, neutral midday, which suits most homes without changing anything.

Colour-temperature and colour bulbs get the full effect, both the warm-to-cool shift and the brightness change. Dimmable white lights that cannot change colour still follow the brightness curve.

Holding brightness on a dark afternoon

The clock does not know the weather. Add a light sensor as the optional illuminance role and the recipe leans on it to keep the brightness up when the room is genuinely dark in the middle of the day, rather than dimming to the schedule while a storm is overhead. Leave the sensor out and the curve follows the time of day alone.

Files Selora AI's pipeline reads to install this recipe. The manifest declares roles, inputs, and integrations; the package files are Jinja-templated YAML applied to Home Assistant.

# Circadian Adaptive Lighting — walk the colour temperature and brightness of
# the selected lights along a daily curve, only touching lights already on.
#
# One automation runs every 15 minutes and on start. It computes a target
# colour temperature and brightness from the time of day (coolest and
# brightest at the midpoint between the morning and evening times, warm and low
# at both ends) and applies them to each selected light that is currently on. A
# repeat/for_each with a per-light "is it on" guard is what keeps the recipe
# from ever switching a light on by itself.
slug: circadian-adaptive-lighting
version: 1.0.0
title: Circadian Adaptive Lighting
tagline: Cool and bright at midday, warm and low in the evening, applied only to the lights that are already on.
description: >-
  Moves the colour temperature and brightness of your dimmable lights along a
  daily curve, coolest and brightest around midday and warm and dim in the
  evening. Runs every 15 minutes and on restart, and only ever changes a light
  that is already on, so it never switches one on by itself.
author: Selora AI
released: "2026-08-28"
min_integration_version: "0.12.0"
tags: [lighting, circadian, color-temperature, brightness]
roles:
  # Dimmable lights the recipe tracks. min_count 1: with no lights there is
  # nothing to adjust. Colour-temp / colour lights get the warm-to-cool shift;
  # dimmable white lights follow the brightness curve only.
  - id: primary_lights
    title: Dimmable lights
    kind: light
    min_count: 1
    selection: required
    description: >-
      The dimmable lights to move through the day. Colour-temperature or colour
      bulbs also get the warm-to-cool shift.
  # Optional colour strips brought into the same curve.
  - id: strips
    title: Light strips
    kind: light
    min_count: 0
    selection: required
    description: >-
      Optional. Colour strips that follow the same curve as the main lights.
      Leave empty to skip.
  # Optional extra bulbs brought into the same curve.
  - id: bulbs
    title: Extra bulbs
    kind: light
    min_count: 0
    selection: required
    description: >-
      Optional. Additional colour or white-ambiance bulbs to move with the
      rest. Leave empty to skip.
  # Optional illuminance sensor. When picked, a dark daytime reading holds the
  # brightness at the day level instead of dimming on the clock. max_count 1.
  - id: illuminance
    title: Light level sensor
    kind: sensor
    device_class: illuminance
    min_count: 0
    max_count: 1
    selection: required
    description: >-
      Optional. A light sensor so a dark, overcast afternoon holds the
      brightness up rather than dimming to the schedule. Leave empty to follow
      the clock alone.
inputs:
  # Cool end of the range, applied around the midpoint of the day. 5000 K is a
  # neutral daylight white.
  - id: day_kelvin
    type: number
    label: Daytime colour temperature (K)
    description: >-
      The coolest colour temperature, used around the middle of the day.
    default: 5000
    min: 2000
    max: 6500
  # Warm end of the range, applied at the morning and evening edges. 2300 K is
  # a low, restful warm white.
  - id: night_kelvin
    type: number
    label: Evening colour temperature (K)
    description: >-
      The warmest colour temperature, used at the start and end of the day.
    default: 2300
    min: 1500
    max: 4000
  # Where the day begins for the lights. The coolest/brightest point sits at
  # the midpoint between this and the evening time.
  - id: morning_time
    type: string
    label: Morning start
    description: >-
      The time the daily curve starts warming up from its evening low.
    default: "06:30:00"
  - id: evening_time
    type: string
    label: Evening end
    description: >-
      The time the curve returns to its warmest, lowest setting.
    default: "20:30:00"
  # Brightness ceiling around midday.
  - id: day_brightness
    type: number
    label: Daytime brightness (percent)
    description: >-
      Brightness used around the middle of the day.
    default: 100
    min: 1
    max: 100
  # Brightness floor at the morning and evening edges.
  - id: night_brightness
    type: number
    label: Evening brightness (percent)
    description: >-
      Brightness used at the start and end of the day.
    default: 35
    min: 1
    max: 100
  # Darkness threshold in lux, used only when an illuminance sensor is picked.
  # Below this during the day, brightness is held at the day level.
  - id: dark_lux
    type: number
    label: Darkness threshold (lux)
    description: >-
      With a light sensor picked, a daytime reading below this holds the
      brightness at the daytime level.
    default: 150
    min: 1
    max: 2000
package_files:
  - package/automations/adaptive.yaml.j2
{# Circadian Adaptive Lighting.

   The colour temperature and brightness both follow a triangular curve across
   the day: warm and low (night values) at the morning and evening edges,
   cool and bright (day values) at the midpoint between them. f is the 0..1
   position on that curve, computed at runtime from the clock.

   The edge minutes (morning, midpoint, evening) and the day/night targets are
   rendered into a variables block as plain numbers at install time, so the
   runtime template only has to do the arithmetic. A second variables step
   builds the target templates from them.

   The repeat walks every selected light and only calls light.turn_on on the
   ones already on, checked per light, so the recipe never switches a light on.
#}
{# The two time inputs are free-form strings, so a homeowner can enter HH:MM as
   readily as HH:MM:SS. Pad with trailing zero fields before splitting: an
   unpack that expects exactly three parts raises at render time, which fails
   the install rather than degrading. Only hours and minutes are ever read. #}
{% set mparts = (inputs.morning_time ~ ':0:0').split(':') %}
{% set morning_min = mparts[0] | int * 60 + mparts[1] | int %}
{% set eparts = (inputs.evening_time ~ ':0:0').split(':') %}
{% set evening_min = eparts[0] | int * 60 + eparts[1] | int %}
{% set noon_min = (morning_min + evening_min) // 2 %}
{% set all_lights = roles.primary_lights + roles.strips + roles.bulbs %}
automation:
  - id: selora_recipe_{{ slug | replace('-', '_') }}_adjust
    alias: "Circadian Adaptive Lighting: adjust"
    description: >-
      Every 15 minutes and on restart, set the colour temperature and
      brightness of each selected light that is on to its point on the daily
      curve.
    mode: restart
    trigger:
      - platform: time_pattern
        minutes: "/15"
      - platform: homeassistant
        event: start
    action:
      - variables:
          mor: {{ morning_min }}
          noon: {{ noon_min }}
          eve: {{ evening_min }}
          day_k: {{ inputs.day_kelvin }}
          night_k: {{ inputs.night_kelvin }}
          day_b: {{ inputs.day_brightness }}
          night_b: {{ inputs.night_brightness }}
{% if roles.illuminance %}
          dark_lux: {{ inputs.dark_lux }}
          lux: "{% raw %}{{ states('{% endraw %}{{ roles.illuminance[0] }}{% raw %}') | float(99999) }}{% endraw %}"
{% endif %}
      - variables:
          target_kelvin: "{% raw %}{% set m = now().hour * 60 + now().minute %}{% set f = 0 %}{% if mor < m < eve %}{% if m <= noon %}{% set f = (m - mor) / (noon - mor) %}{% else %}{% set f = (eve - m) / (eve - noon) %}{% endif %}{% endif %}{{ (night_k + (day_k - night_k) * f) | round | int }}{% endraw %}"
          target_brightness: "{% raw %}{% set m = now().hour * 60 + now().minute %}{% set f = 0 %}{% if mor < m < eve %}{% if m <= noon %}{% set f = (m - mor) / (noon - mor) %}{% else %}{% set f = (eve - m) / (eve - noon) %}{% endif %}{% endif %}{% endraw %}{% if roles.illuminance %}{% raw %}{% if mor < m < eve and lux < dark_lux %}{% set f = 1 %}{% endif %}{% endraw %}{% endif %}{% raw %}{{ (night_b + (day_b - night_b) * f) | round | int }}{% endraw %}"
      - repeat:
          for_each:
{% for light in all_lights %}
            - {{ light }}
{% endfor %}
          sequence:
            - if:
                - condition: template
                  value_template: "{% raw %}{{ is_state(repeat.item, 'on') }}{% endraw %}"
              then:
                - service: light.turn_on
                  target:
                    entity_id: "{% raw %}{{ repeat.item }}{% endraw %}"
                  data:
                    color_temp_kelvin: "{% raw %}{{ target_kelvin | int }}{% endraw %}"
                    brightness_pct: "{% raw %}{{ target_brightness | int }}{% endraw %}"

Type to search across

↑↓ navigate ↵ open
⌘K search