Skip to content

Instantly share code, notes, and snippets.

@larkov
Last active September 24, 2026 07:02
Show Gist options
  • Select an option

  • Save larkov/1c197a04d06c6df6f91137435234d171 to your computer and use it in GitHub Desktop.

Select an option

Save larkov/1c197a04d06c6df6f91137435234d171 to your computer and use it in GitHub Desktop.
Wake-up light alarm with sunrise effect (Kelvin + curve)
blueprint:
name: Wake-up light alarm with sunrise effect (Kelvin + curve)
description: >-
A wake-up light alarm with a brightness and color temperature sunrise effect.
Brightness follows a configurable curve so the sunrise looks even to the eye,
color temperature uses Kelvin, and lights that are already on are left alone.
Note: Requires the date_time_iso sensor (Time & Date integration), not manually executable!
domain: automation
input:
light_entity:
name: Wake-up light(s)
description: >-
The light(s) to control. Turning a light off during the sunrise keeps it off.
Color temperature range is auto-detected per light.
selector:
entity:
filter:
- domain: light
multiple: true
timestamp_sensor:
name: Alarm timestamp sensor
description: "Sensor with timestamp of next alarm with device_class: timestamp (set to 'none' for manual alarm time)"
default: none
selector:
entity:
filter:
- device_class: timestamp
manual_time:
name: Manual alarm time
description: >-
Time to trigger alarm every day if timestamp sensor is not set.
Settings at or shortly after midnight will not work as expected!
default: "7:00:00"
selector:
time: {}
check_entity:
name: Additional entity to check before sunrise is triggered
description: >-
If set, checks if entity is 'on' or 'home' before triggering.
Use e.g. a (workday) sensor, device_tracker or person entity.
default: none
selector:
entity: {}
sunrise_duration:
name: Sunrise duration
description: The sunrise will start the configured number of minutes before the alarm time.
default: 25
selector:
number:
min: 5
max: 60
step: 5
unit_of_measurement: min
mode: slider
start_brightness:
name: Minimum brightness
description: >-
The brightness to start with. Some lights ignore very low values and may
turn on with full brightness instead!
default: 1
selector:
number:
min: 1
max: 255
step: 1
mode: slider
end_brightness:
name: Maximum brightness
description: The brightness will be transitioned from the minimum to this value.
default: 254
selector:
number:
min: 5
max: 255
step: 1
mode: slider
brightness_curve:
name: Brightness curve
description: >-
1 = linear (looks too fast to the eye). Higher values keep the light dim
longer and put most of the increase near the end. 3 is a good start;
try 2 if the beginning feels too slow.
default: 3
selector:
number:
min: 1
max: 4
step: 0.5
mode: slider
min_kelvin:
name: Start color temperature (warm)
description: "Color temperature at the start of the sunrise. (0: warmest supported)"
default: 0
selector:
number:
min: 0
max: 6500
step: 100
mode: slider
unit_of_measurement: K
max_kelvin:
name: End color temperature (cool)
description: "Color temperature at the alarm time. (0: coolest supported)"
default: 0
selector:
number:
min: 0
max: 6500
step: 100
mode: slider
unit_of_measurement: K
skip_lit_lights:
name: Leave lights that are already on
description: >-
If a light was already on before the sunrise window started, it is not
touched. Lights that were off still get the sunrise. Light groups are
expanded to their member lights.
default: true
selector:
boolean: {}
pre_sunrise_actions:
name: Pre-sunrise actions
description: Optional actions to run before sunrise starts.
default: []
selector:
action: {}
post_sunrise_actions:
name: Post-sunrise actions
description: Optional actions to run after sunrise ends (around the alarm time).
default: []
selector:
action: {}
source_url: https://gist.github.com/sbyx/96c43b13b90ae1c35b872313ba1d2d2d
variables:
light_input: !input light_entity
lights: "{{ [light_input] if light_input is string else light_input | list }}"
sensor: !input timestamp_sensor
manual_time: !input manual_time
check_entity: !input check_entity
sunrise_duration: !input sunrise_duration
seconds: "{{ float(sunrise_duration) * 60 }}"
start_brightness: !input start_brightness
end_brightness: !input end_brightness
range_brightness: "{{ float(end_brightness) - float(start_brightness) }}"
curve: !input brightness_curve
min_kelvin: !input min_kelvin
max_kelvin: !input max_kelvin
skip_lit: !input skip_lit_lights
# Update roughly once per brightness step, but never faster than 3 s or slower than 30 s
tick_time: "{{ [[float(seconds) / ([float(range_brightness), 1] | max), 3] | max, 30] | min }}"
triggers:
- trigger: time_pattern
minutes: "*"
conditions: []
actions:
- wait_template: "{{ sensor == 'none' or as_timestamp(states(sensor), None) != None }}"
- wait_template: >-
{% set alarm = as_timestamp(states(sensor) if sensor != 'none'
else states('sensor.date') ~ ' ' ~ manual_time, 0) %}
{{ 0 < alarm - as_timestamp(states('sensor.date_time_iso')) <= float(seconds)
and states(check_entity) in ['unknown', 'on', 'home'] }}
# Decide which lights get the sunrise, based on what they looked like when the
# sunrise window STARTED (not right now). This lets a restarted run (after an
# HA restart, automation reload or error) resume instead of skipping lights
# the sunrise itself turned on.
# - off since before the window -> sunrise
# - on, turned on in the window -> sunrise (we turned it on; resume)
# - on since before the window -> leave alone (unless skip_lit is off)
# - off, turned off in window -> leave off (user turned it off)
- variables:
window_start: >-
{{ as_timestamp(states(sensor) if sensor != 'none'
else states('sensor.date') ~ ' ' ~ manual_time, 0) - float(seconds) }}
light_plan: >-
{%- set ns = namespace(use=[], resume=false) -%}
{%- for s in expand(lights) if s.state in ['on', 'off'] -%}
{%- set in_window = as_timestamp(s.last_changed) >= window_start -%}
{%- if s.state == 'off' and not in_window -%}
{%- set ns.use = ns.use + [s.entity_id] -%}
{%- elif s.state == 'on' and in_window -%}
{%- set ns.use = ns.use + [s.entity_id] -%}
{%- set ns.resume = true -%}
{%- elif s.state == 'on' and not skip_lit -%}
{%- set ns.use = ns.use + [s.entity_id] -%}
{%- endif -%}
{%- endfor -%}
{{ {'lights': ns.use, 'resume': ns.resume} }}
sunrise_lights: "{{ light_plan.lights }}"
resuming: "{{ light_plan.resume }}"
# Pre-sunrise actions only on a fresh start, not when resuming
- if:
- "{{ not resuming }}"
then:
- choose: []
default: !input pre_sunrise_actions
- condition: template
value_template: "{{ sensor == 'none' or as_timestamp(states(sensor), None) != None }}"
- condition: template
value_template: >-
{% set alarm = as_timestamp(states(sensor) if sensor != 'none'
else states('sensor.date') ~ ' ' ~ manual_time, 0) %}
{{ 0 < alarm - as_timestamp(now()) <= float(seconds)
and states(check_entity) in ['unknown', 'on', 'home'] }}
- if:
- "{{ sunrise_lights | count > 0 }}"
then:
# Turn on each light that is still off at minimum brightness and warmest
# color (lights already on from an earlier run just continue the curve)
- repeat:
for_each: "{{ sunrise_lights | select('is_state', 'off') | list }}"
sequence:
- variables:
lo: "{{ state_attr(repeat.item, 'min_color_temp_kelvin') | int(0) }}"
supports_ct: "{{ lo > 0 and 'color_temp' in (state_attr(repeat.item, 'supported_color_modes') or []) }}"
start_k: "{{ [lo, min_kelvin | int] | max if min_kelvin | int > 0 else lo }}"
- if:
- "{{ supports_ct }}"
then:
- action: light.turn_on
target:
entity_id: "{{ repeat.item }}"
data:
brightness: "{{ start_brightness | int }}"
color_temp_kelvin: "{{ start_k }}"
else:
- action: light.turn_on
target:
entity_id: "{{ repeat.item }}"
data:
brightness: "{{ start_brightness | int }}"
# Sunrise loop
- repeat:
while:
- "{{ sensor == 'none' or as_timestamp(states(sensor), None) != None }}"
- >-
{% set alarm = as_timestamp(states(sensor) if sensor != 'none'
else states('sensor.date') ~ ' ' ~ manual_time, 0) %}
{{ 0 < alarm - as_timestamp(now()) <= float(seconds) }}
sequence:
- delay: "{{ tick_time }}"
- variables:
alarm: >-
{{ as_timestamp(states(sensor) if sensor != 'none'
else states('sensor.date') ~ ' ' ~ manual_time, 0) }}
remaining: "{{ alarm - as_timestamp(now()) }}"
# Linear progress 0 -> 1, then shaped by the curve
progress: "{{ [0, [1, 1 - remaining / float(seconds)] | min] | max }}"
shaped: "{{ progress ** float(curve) }}"
target_brightness: >-
{{ (float(start_brightness) + float(range_brightness) * shaped) | round(0) | int }}
- if:
- "{{ 0 < remaining <= float(seconds) }}"
then:
- repeat:
for_each: "{{ sunrise_lights }}"
sequence:
- variables:
current: "{{ state_attr(repeat.item, 'brightness') | int(0) }}"
lo: "{{ state_attr(repeat.item, 'min_color_temp_kelvin') | int(0) }}"
hi: "{{ state_attr(repeat.item, 'max_color_temp_kelvin') | int(0) }}"
supports_ct: "{{ lo > 0 and hi > 0 and 'color_temp' in (state_attr(repeat.item, 'supported_color_modes') or []) }}"
start_k: "{{ [lo, min_kelvin | int] | max if min_kelvin | int > 0 else lo }}"
end_k: "{{ [hi, max_kelvin | int] | min if max_kelvin | int > 0 else hi }}"
# Interpolate in mired space (perceptually even), output Kelvin
target_k: >-
{% if supports_ct %}
{% set m_start = 1000000 / start_k %}
{% set m_end = 1000000 / end_k %}
{{ (1000000 / (m_start + (m_end - m_start) * shaped)) | round(0) | int }}
{% else %}0{% endif %}
# Skip lights that were turned off (keep off) or are already at max
- if:
- "{{ 0 < current < end_brightness | int }}"
then:
- if:
- "{{ supports_ct }}"
then:
- action: light.turn_on
target:
entity_id: "{{ repeat.item }}"
data:
brightness: "{{ target_brightness }}"
color_temp_kelvin: "{{ target_k }}"
else:
- action: light.turn_on
target:
entity_id: "{{ repeat.item }}"
data:
brightness: "{{ target_brightness }}"
- choose: []
default: !input post_sunrise_actions
mode: single
max_exceeded: silent
@larkov

larkov commented Sep 24, 2026

Copy link
Copy Markdown
Author

Modified version of https://gist.github.com/sbyx/96c43b13b90ae1c35b872313ba1d2d2d with help from uncle Claude.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment