Last active
January 10, 2026 03:24
-
-
Save yougotborked/24c5384ed4a2a28e1151f1f26d98b2e6 to your computer and use it in GitHub Desktop.
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: Kids Room Sleep/Wake (RGB Status) + Overhead Early-On Monitor (v3.1-hardened) | |
| description: > | |
| Shows child-friendly status with an RGB lamp **only at phase-change moments** | |
| (Stay → Play → Get Up) and during **overhead violations**. | |
| Hardening changes (aimed at stopping early-morning surprise ON events): | |
| - Quiet Play is **disabled by default** and no longer defaults to midnight. | |
| - Overhead "off→on" trigger requires a short stable-on period. | |
| - HA start behavior is now opt-in via `sync_on_ha_start`. | |
| - Optional Logbook debug entry on every run to diagnose exact triggers. | |
| Respects manual "off" on the status lamp and does **not** keep turning it back on | |
| via periodic enforcement. Audio cues are only reactions to the child's action | |
| (overhead on) until the Get Up alarm. | |
| Handles cross-midnight schedules; optional Quiet Play; optional auto-off of overhead | |
| if still too early. On Get Up, optionally turns on overhead and announces to the child. | |
| Temporarily bumps child TTS volume, then restores. | |
| domain: automation | |
| input: | |
| child_name: | |
| name: Child name | |
| selector: { text: {} } | |
| status_rgb_light: | |
| name: Status RGB light (lamp/sconce) | |
| selector: | |
| entity: { domain: light } | |
| overhead_entity: | |
| name: Overhead light/switch to monitor | |
| selector: | |
| entity: | |
| multiple: false | |
| domain: [light, switch] | |
| overhead_stability_seconds: | |
| name: Overhead stable-on seconds | |
| description: Require overhead entity to remain ON this many seconds before reacting. | |
| default: 2 | |
| selector: { number: { min: 0, max: 30, step: 1, mode: slider } } | |
| # Times | |
| stay_in_bed_time: | |
| name: Stay-in-Bed time | |
| selector: { time: {} } | |
| quiet_play_enabled: | |
| name: Enable Quiet Play phase | |
| default: false | |
| selector: { boolean: {} } | |
| quiet_play_time: | |
| name: Quiet Play time (ignored if disabled) | |
| default: "05:30:00" | |
| selector: { time: {} } | |
| get_up_time: | |
| name: Get Up time | |
| selector: { time: {} } | |
| # Colors/brightness | |
| stay_color: | |
| name: Stay-in-Bed color | |
| default: [255, 40, 0] | |
| selector: { color_rgb: {} } | |
| stay_brightness: | |
| name: Stay-in-Bed brightness (1-255) | |
| default: 25 | |
| selector: { number: { min: 1, max: 255, mode: slider } } | |
| play_color: | |
| name: Quiet Play color | |
| default: [255, 180, 50] | |
| selector: { color_rgb: {} } | |
| play_brightness: | |
| name: Quiet Play brightness (1-255) | |
| default: 60 | |
| selector: { number: { min: 1, max: 255, mode: slider } } | |
| go_color: | |
| name: Get Up color | |
| default: [40, 255, 40] | |
| selector: { color_rgb: {} } | |
| go_brightness: | |
| name: Get Up brightness (1-255) | |
| default: 180 | |
| selector: { number: { min: 1, max: 255, mode: slider } } | |
| # Overhead monitoring (seconds) | |
| reminder_delay_seconds: | |
| name: Early reminder delay (seconds) | |
| description: Speak to child this many seconds after overhead turns on (during Stay/Play). | |
| default: 20 | |
| selector: { number: { min: 0, max: 300, step: 1, mode: slider } } | |
| auto_off_after_seconds: | |
| name: Auto-off cutoff (seconds) | |
| description: Turn overhead off after this many seconds if still too early (0 = never). | |
| default: 90 | |
| selector: { number: { min: 0, max: 900, step: 5, mode: slider } } | |
| auto_turn_off_overhead: | |
| name: Turn overhead off at cutoff if still too early | |
| default: true | |
| selector: { boolean: {} } | |
| notify_parent_immediately: | |
| name: Announce to bedside as soon as overhead turns on (during night window) | |
| default: true | |
| selector: { boolean: {} } | |
| # On-Get-Up actions | |
| turn_on_overhead_at_get_up: | |
| name: Turn overhead on at Get Up | |
| default: true | |
| selector: { boolean: {} } | |
| announce_get_up_to_child: | |
| name: Announce Get Up to child | |
| default: true | |
| selector: { boolean: {} } | |
| # Stay phase visual policy | |
| keep_stay_light_off_by_default: | |
| name: Keep status light OFF during Stay-in-Bed (only flash on violations) | |
| default: true | |
| selector: { boolean: {} } | |
| stay_flash_brightness: | |
| name: Stay-in-Bed flash brightness (1-255) | |
| default: 25 | |
| selector: { number: { min: 1, max: 255, mode: slider } } | |
| # Speakers & TTS | |
| tts_engine: | |
| name: TTS engine (e.g., tts.cloud_say / tts.piper) | |
| selector: | |
| entity: { domain: tts } | |
| parent_speaker: | |
| name: Parent bedside speaker | |
| selector: | |
| entity: { domain: media_player } | |
| child_speaker: | |
| name: Child room speaker | |
| selector: | |
| entity: { domain: media_player } | |
| # Child TTS volume bump | |
| child_tts_volume: | |
| name: Child TTS volume (0.0–1.0) | |
| description: Temporary volume used for child TTS lines. | |
| default: 0.65 | |
| selector: { number: { min: 0.0, max: 1.0, step: 0.05, mode: slider } } | |
| restore_child_volume: | |
| name: Restore child speaker’s previous volume after TTS | |
| default: true | |
| selector: { boolean: {} } | |
| sync_on_ha_start: | |
| name: Sync cues on Home Assistant start | |
| description: If enabled, HA restart will re-apply the appropriate phase cue. | |
| default: true | |
| selector: { boolean: {} } | |
| debug_log: | |
| name: Debug logging | |
| description: Log trigger/phase/window decisions to Logbook for diagnosis. | |
| default: true | |
| selector: { boolean: {} } | |
| # Messaging | |
| speak_to_child_on_violation: | |
| name: Speak to child (reminder + cutoff if still too early) | |
| default: true | |
| selector: { boolean: {} } | |
| potty_clause: | |
| name: Potty clause (always appended) | |
| default: "It's always okay to use the potty." | |
| selector: { text: {} } | |
| msg_back_to_bed: | |
| name: Message - Too early (before Quiet Play) | |
| default: "It's still sleep time. | |
| Please get back in bed." | |
| selector: { text: {} } | |
| msg_quiet_play: | |
| name: Message - Quiet Play window | |
| default: "You may play quietly in your room." | |
| selector: { text: {} } | |
| msg_get_dressed: | |
| name: Message - Get Up window | |
| default: "It's okay to get up and get dressed." | |
| selector: { text: {} } | |
| mode: restart | |
| max_exceeded: silent | |
| variables: | |
| child_name: !input child_name | |
| status_rgb_light: !input status_rgb_light | |
| overhead_entity: !input overhead_entity | |
| overhead_stability_seconds: !input overhead_stability_seconds | |
| stay_in_bed_time: !input stay_in_bed_time | |
| quiet_play_enabled: !input quiet_play_enabled | |
| quiet_play_time: !input quiet_play_time | |
| get_up_time: !input get_up_time | |
| stay_color: !input stay_color | |
| stay_brightness: !input stay_brightness | |
| play_color: !input play_color | |
| play_brightness: !input play_brightness | |
| go_color: !input go_color | |
| go_brightness: !input go_brightness | |
| reminder_delay_seconds: !input reminder_delay_seconds | |
| auto_off_after_seconds: !input auto_off_after_seconds | |
| auto_turn_off_overhead: !input auto_turn_off_overhead | |
| turn_on_overhead_at_get_up: !input turn_on_overhead_at_get_up | |
| announce_get_up_to_child: !input announce_get_up_to_child | |
| keep_stay_light_off_by_default: !input keep_stay_light_off_by_default | |
| stay_flash_brightness: !input stay_flash_brightness | |
| tts_engine: !input tts_engine | |
| parent_speaker: !input parent_speaker | |
| child_speaker: !input child_speaker | |
| child_tts_volume: !input child_tts_volume | |
| restore_child_volume: !input restore_child_volume | |
| sync_on_ha_start: !input sync_on_ha_start | |
| debug_log: !input debug_log | |
| notify_parent_immediately: !input notify_parent_immediately | |
| speak_to_child_on_violation: !input speak_to_child_on_violation | |
| potty_clause: !input potty_clause | |
| msg_back_to_bed: !input msg_back_to_bed | |
| msg_quiet_play: !input msg_quiet_play | |
| msg_get_dressed: !input msg_get_dressed | |
| # --- Restart-safe time math (timestamp-based; no timedelta concat) --- | |
| day: 86400 | |
| now_ts: "{{ as_timestamp(now()) }}" | |
| stay_ts: "{{ as_timestamp(today_at(stay_in_bed_time)) }}" | |
| up_ts_raw: "{{ as_timestamp(today_at(get_up_time)) }}" | |
| has_qp: "{{ quiet_play_enabled and (quiet_play_time != stay_in_bed_time) }}" | |
| qp_ts_raw: "{{ as_timestamp(today_at(quiet_play_time)) if has_qp else None }}" | |
| crosses_midnight: "{{ up_ts_raw <= stay_ts }}" | |
| up_ts: "{{ up_ts_raw if not crosses_midnight else (up_ts_raw + day) }}" | |
| qp_ts: > | |
| {% if has_qp %} | |
| {% if not crosses_midnight %} | |
| {{ qp_ts_raw }} | |
| {% else %} | |
| {{ (qp_ts_raw + day) if qp_ts_raw < stay_ts else qp_ts_raw }} | |
| {% endif %} | |
| {% else %} | |
| {{ None }} | |
| {% endif %} | |
| now_norm_ts: "{{ now_ts if now_ts >= stay_ts else (now_ts + day) }}" | |
| in_window: "{{ (now_norm_ts >= stay_ts) and (now_norm_ts < up_ts) }}" | |
| phase: > | |
| {% if not in_window %} | |
| outside | |
| {% else %} | |
| {% if not has_qp %} | |
| stay | |
| {% else %} | |
| {% if now_norm_ts < qp_ts %} | |
| stay | |
| {% elif now_norm_ts < up_ts %} | |
| play | |
| {% else %} | |
| outside | |
| {% endif %} | |
| {% endif %} | |
| {% endif %} | |
| triggers: | |
| - id: at_stay | |
| platform: time | |
| at: !input stay_in_bed_time | |
| - id: at_quiet_play | |
| platform: time | |
| at: !input quiet_play_time | |
| - id: at_get_up | |
| platform: time | |
| at: !input get_up_time | |
| - id: overhead_on_immediate | |
| platform: state | |
| entity_id: !input overhead_entity | |
| from: "off" | |
| to: "on" | |
| for: | |
| seconds: !input overhead_stability_seconds | |
| - id: ha_started | |
| platform: homeassistant | |
| event: start | |
| conditions: [] | |
| actions: | |
| - if: | |
| - condition: template | |
| value_template: "{{ debug_log }}" | |
| then: | |
| - service: logbook.log | |
| data: | |
| name: Kids Sleep/Wake | |
| entity_id: !input status_rgb_light | |
| message: > | |
| Trigger={{ trigger.id }} phase={{ phase }} in_window={{ in_window }} | |
| has_qp={{ has_qp }} crosses_midnight={{ crosses_midnight }} | |
| else: [] | |
| - choose: | |
| # --- GET UP ALARM --- | |
| - conditions: | |
| - condition: trigger | |
| id: at_get_up | |
| sequence: | |
| - variables: | |
| _modes: "{{ state_attr(status_rgb_light, 'supported_color_modes') or [] }}" | |
| _rgb_ok: "{{ 'rgb' in _modes or 'rgb_color' in _modes or 'hs' in _modes or 'xy' in _modes }}" | |
| - choose: | |
| - conditions: "{{ _rgb_ok }}" | |
| sequence: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| rgb_color: "{{ go_color }}" | |
| brightness: "{{ go_brightness }}" | |
| default: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| brightness: "{{ go_brightness }}" | |
| - if: | |
| - condition: template | |
| value_template: "{{ turn_on_overhead_at_get_up }}" | |
| then: | |
| - service: homeassistant.turn_on | |
| entity_id: !input overhead_entity | |
| - if: | |
| - condition: template | |
| value_template: "{{ announce_get_up_to_child }}" | |
| then: | |
| - variables: | |
| __prev_child_vol: "{{ state_attr(child_speaker, 'volume_level') }}" | |
| - service: media_player.volume_set | |
| target: { entity_id: !input child_speaker } | |
| data: | |
| volume_level: "{{ child_tts_volume|float }}" | |
| - service: tts.speak | |
| target: { entity_id: !input tts_engine } | |
| data: | |
| media_player_entity_id: !input child_speaker | |
| message: "{{ msg_get_dressed }} {{ potty_clause }}" | |
| - if: | |
| - condition: template | |
| value_template: "{{ restore_child_volume and (__prev_child_vol is not none) }}" | |
| then: | |
| - delay: "00:00:02" | |
| - service: media_player.volume_set | |
| target: { entity_id: !input child_speaker } | |
| data: | |
| volume_level: "{{ __prev_child_vol }}" | |
| # --- PHASE CUES (stay / quiet play / HA start) --- | |
| - conditions: | |
| - condition: or | |
| conditions: | |
| - condition: trigger | |
| id: at_stay | |
| - condition: trigger | |
| id: at_quiet_play | |
| - condition: template | |
| value_template: "{{ trigger.id == 'ha_started' and sync_on_ha_start }}" | |
| sequence: | |
| - variables: | |
| _modes: "{{ state_attr(status_rgb_light, 'supported_color_modes') or [] }}" | |
| _rgb_ok: "{{ 'rgb' in _modes or 'rgb_color' in _modes or 'hs' in _modes or 'xy' in _modes }}" | |
| - choose: | |
| # Stay-in-Bed | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ trigger.id == 'ha_started' or trigger.id == 'at_stay' }}" | |
| sequence: | |
| - if: | |
| - condition: template | |
| value_template: "{{ keep_stay_light_off_by_default }}" | |
| then: | |
| - service: light.turn_off | |
| entity_id: !input status_rgb_light | |
| else: | |
| - choose: | |
| - conditions: "{{ _rgb_ok }}" | |
| sequence: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| rgb_color: "{{ stay_color }}" | |
| brightness: "{{ stay_brightness }}" | |
| default: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| brightness: "{{ stay_brightness }}" | |
| # Quiet Play (hardened: only if it's actually in-window AND phase=play) | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ trigger.id == 'at_quiet_play' and has_qp and in_window and phase == 'play' }}" | |
| sequence: | |
| - choose: | |
| - conditions: "{{ _rgb_ok }}" | |
| sequence: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| rgb_color: "{{ play_color }}" | |
| brightness: "{{ play_brightness }}" | |
| default: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| brightness: "{{ play_brightness }}" | |
| default: [] | |
| # --- OVERHEAD TURNED ON DURING NIGHT WINDOW --- | |
| - conditions: | |
| - condition: trigger | |
| id: overhead_on_immediate | |
| - condition: template | |
| value_template: "{{ in_window }}" | |
| sequence: | |
| # Parent heads-up | |
| - if: | |
| - condition: template | |
| value_template: "{{ notify_parent_immediately }}" | |
| then: | |
| - service: tts.speak | |
| target: { entity_id: !input tts_engine } | |
| data: | |
| media_player_entity_id: !input parent_speaker | |
| message: "{{ child_name }} turned on the overhead light." | |
| # If Stay + keep-off policy, flash status light now (capability-aware) | |
| - variables: | |
| _modes: "{{ state_attr(status_rgb_light, 'supported_color_modes') or [] }}" | |
| _rgb_ok: "{{ 'rgb' in _modes or 'rgb_color' in _modes or 'hs' in _modes or 'xy' in _modes }}" | |
| - if: | |
| - condition: template | |
| value_template: "{{ phase == 'stay' and keep_stay_light_off_by_default }}" | |
| then: | |
| - choose: | |
| - conditions: "{{ _rgb_ok }}" | |
| sequence: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| rgb_color: "{{ stay_color }}" | |
| brightness: "{{ stay_flash_brightness }}" | |
| default: | |
| - service: light.turn_on | |
| entity_id: !input status_rgb_light | |
| data: | |
| brightness: "{{ stay_flash_brightness }}" | |
| # Reminder after delay (re-evaluate phase/time AFTER delay) | |
| - delay: | |
| seconds: "{{ reminder_delay_seconds|int }}" | |
| - variables: | |
| _now: "{{ as_timestamp(now()) }}" | |
| _now_norm: "{{ _now if _now >= stay_ts else (_now + day) }}" | |
| _in_window_now: "{{ (_now_norm >= stay_ts) and (_now_norm < up_ts) }}" | |
| _phase_now: > | |
| {% if not _in_window_now %} | |
| outside | |
| {% else %} | |
| {% if not has_qp %} | |
| stay | |
| {% else %} | |
| {% if _now_norm < qp_ts %} | |
| stay | |
| {% elif _now_norm < up_ts %} | |
| play | |
| {% else %} | |
| outside | |
| {% endif %} | |
| {% endif %} | |
| {% endif %} | |
| _enforce_early_now: "{{ _phase_now in ['stay','play'] }}" | |
| _guidance_now: > | |
| {% if _phase_now == 'stay' %} | |
| {{ msg_back_to_bed }} | |
| {% elif _phase_now == 'play' %} | |
| {{ msg_quiet_play }} | |
| {% else %} | |
| {{ msg_get_dressed }} | |
| {% endif %} | |
| - if: | |
| - condition: state | |
| entity_id: !input overhead_entity | |
| state: "on" | |
| - condition: template | |
| value_template: "{{ _in_window_now and _enforce_early_now }}" | |
| then: | |
| - if: | |
| - condition: template | |
| value_template: "{{ speak_to_child_on_violation }}" | |
| then: | |
| - variables: | |
| __prev_child_vol2: "{{ state_attr(child_speaker, 'volume_level') }}" | |
| - service: media_player.volume_set | |
| target: { entity_id: !input child_speaker } | |
| data: | |
| volume_level: "{{ child_tts_volume|float }}" | |
| - service: tts.speak | |
| target: { entity_id: !input tts_engine } | |
| data: | |
| media_player_entity_id: !input child_speaker | |
| message: "{{ _guidance_now }} {{ potty_clause }}" | |
| - if: | |
| - condition: template | |
| value_template: "{{ restore_child_volume and (__prev_child_vol2 is not none) }}" | |
| then: | |
| - delay: "00:00:02" | |
| - service: media_player.volume_set | |
| target: { entity_id: !input child_speaker } | |
| data: | |
| volume_level: "{{ __prev_child_vol2 }}" | |
| - if: | |
| - condition: template | |
| value_template: "{{ phase == 'stay' and keep_stay_light_off_by_default }}" | |
| then: | |
| - service: light.turn_off | |
| entity_id: !input status_rgb_light | |
| # Auto-off at cutoff (re-evaluate right before action) | |
| - if: | |
| - condition: template | |
| value_template: > | |
| {{ auto_turn_off_overhead and (auto_off_after_seconds|int > 0) }} | |
| then: | |
| - delay: | |
| seconds: > | |
| {{ [ (auto_off_after_seconds|int - reminder_delay_seconds|int) , 0 ] | max }} | |
| - variables: | |
| __now: "{{ as_timestamp(now()) }}" | |
| __now_norm: "{{ __now if __now >= stay_ts else (__now + day) }}" | |
| __in_window_now: "{{ (__now_norm >= stay_ts) and (__now_norm < up_ts) }}" | |
| __phase_now: > | |
| {% if not __in_window_now %} | |
| outside | |
| {% else %} | |
| {% if not has_qp %} | |
| stay | |
| {% else %} | |
| {% if __now_norm < qp_ts %} | |
| stay | |
| {% elif __now_norm < up_ts %} | |
| play | |
| {% else %} | |
| outside | |
| {% endif %} | |
| {% endif %} | |
| {% endif %} | |
| - if: | |
| - condition: state | |
| entity_id: !input overhead_entity | |
| state: "on" | |
| - condition: template | |
| value_template: "{{ __in_window_now and (__phase_now in ['stay','play']) }}" | |
| then: | |
| - service: homeassistant.turn_off | |
| entity_id: !input overhead_entity | |
| default: [] | |
| # Notes | |
| # - v3.1-hardened keeps the original behavior but changes the defaults and adds guards | |
| # to prevent accidental early-morning status lamp turn-ons. | |
| # - Enable Quiet Play only if you explicitly want it, and set a non-midnight time. | |
| # - Use Logbook/Traces to identify the exact trigger when something unexpected happens. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment