Last active
October 14, 2025 19:33
-
-
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) | |
| description: > | |
| Shows child-friendly status with an RGB lamp **only at phase-change moments** | |
| (Stay → Play → Get Up) and during **overhead violations**. 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] | |
| # Times | |
| stay_in_bed_time: | |
| name: Stay-in-Bed time | |
| selector: { time: {} } | |
| quiet_play_enabled: | |
| name: Enable Quiet Play phase | |
| default: true | |
| selector: { boolean: {} } | |
| quiet_play_time: | |
| name: Quiet Play time (ignored if disabled) | |
| default: "00:00: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: {} } | |
| # 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 | |
| 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 | |
| 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" | |
| - id: ha_started | |
| platform: homeassistant | |
| event: start | |
| conditions: [] | |
| actions: | |
| - 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: trigger | |
| id: ha_started | |
| 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 | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ trigger.id == 'at_quiet_play' and has_qp }}" | |
| 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 adds a temporary child TTS volume bump with optional restore. | |
| # - Uses !input for targets (no templated entity_id) to avoid silent no-ops. | |
| # - Capability-aware light.turn_on: RGB if supported, otherwise brightness-only. | |
| # - Recomputes phase after delays to avoid stale decisions around boundaries. | |
| # - Guards Quiet Play timing across midnight and odd equal-times cases. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment