Last active
September 24, 2026 07:02
-
-
Save larkov/1c197a04d06c6df6f91137435234d171 to your computer and use it in GitHub Desktop.
Wake-up light alarm with sunrise effect (Kelvin + curve)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Modified version of https://gist.github.com/sbyx/96c43b13b90ae1c35b872313ba1d2d2d with help from uncle Claude.