Automations¶
All automations live exclusively in automations.yaml. There are currently 66 automations across 22 sections.
Editing automations
Edit automations.yaml locally, then commit and push to main — CI hot-reloads the automations (no HA restart needed) once the test suite passes.
Section 1 — Leak Detection¶
Critical push notifications (bypass DND) sent to both phones immediately when any leak sensor reports water. The notification names the exact wet room(s).
| Automation | Trigger | Action |
|---|---|---|
| Leak detected | sensor.leak_status changes to a non-dry value (a room wets, or another joins) | Critical push to both phones naming the wet room(s); persistent HA notification |
| Leak cleared | sensor.leak_status → "All dry" (after being wet) | All-clear push; dismisses the persistent notification |
Architecture: all 13 sensors live in one container, group.leak_sensors (configuration.yaml). Everything derives from it — sensor.leak_status (the wet rooms, by friendly_name), binary_sensor.any_leak_wet (drives the night-light blue leak alert, #76), and the alerts (which trigger on leak_status). To add a leak sensor: one line in group.leak_sensors + a clean friendly_name (the alert uses it as the room name). The notifications consolidate into a single updating alert listing all currently-wet rooms.
13 sensors monitored: Kitchen sink, laundry room, master bathroom (×2), powder room, office sink, bedroom 3 sink, 2nd floor hall bathroom (×2), butler's kitchen, 3rd floor furnace, attic air handler, crawl space furnace. All the same model (Third Reality) with a built-in siren.
Critical alert
Leak notifications use push_type: critical — they override silent mode and Do Not Disturb on both iPhones.
Section 1b — Condensate Safety¶
#71. Generalized across all 3 HVAC zones via a single zone_map template, resolved from whichever sensor/thermostat triggered:
| Leak sensor | Thermostat | Zone name |
|---|---|---|
binary_sensor.attic_air_handler_leak_detector | climate.office | Office |
binary_sensor.3rd_floor_furnace_leak_detector | climate.bedroom | Master Bedroom |
binary_sensor.crawl_space_furnace_leak_detector | climate.dining_room | Dining Room |
| Automation | Trigger | Action |
|---|---|---|
Condensate Leak — HVAC Shutoff (condensate_leak_hvac_shutoff, mode: queued) | Any of the 3 sensors → on | climate.set_hvac_mode: off on the mapped zone; if not already latched, turn on input_boolean.<slug>_ac_condensate_shutoff and push both phones once per episode |
Condensate Leak — HVAC Restore (condensate_leak_hvac_restore, mode: parallel) | Any of the 3 sensors → off for 10 minutes, and that zone's latch is on | Wait until ≥2h since input_datetime.<slug>_ac_condensate_last_restore; re-check the pan is still dry and the latch still on; clear the latch, stamp the restore time, climate.set_hvac_mode: heat_cool, push both phones |
Condensate Leak — Owner Override Clear (condensate_leak_hvac_override, mode: queued) | Any of climate.office/climate.bedroom/climate.dining_room changes state, new state not off/unavailable/unknown, and that zone's latch is on | Clear the latch (cancels the pending auto-restore) + push both phones |
Flow: shutoff → restore (after a 10-min dry debounce + a 2h per-zone cooldown) → or owner override (manual turn-on) cancels the pending restore early. Zones are fully independent — the zone is always resolved from the trigger, never hardcoded, so one zone's leak/restore/override never touches another zone's latch or thermostat. Restore clears the latch and stamps the timestamp before setting heat_cool, so its own write doesn't get mistaken for a manual override.
This is the 2nd sanctioned HVAC-write exception (after §9b pre-cool) — everywhere else the Ecobees' Eco+ TOU owns heating/cooling. This one writes hvac_mode (off/heat_cool), not a setpoint. The 3 sensors stay in group.leak_sensors, so the generic §1 critical water alert still fires independently of this HVAC-safety layer. User-facing detail: Condensate Safety Alerts. Full design: projects/climate_system.md.
Section 2 — Garage Doors¶
| Automation | Trigger | Action |
|---|---|---|
| Garage door opened | Either door → open | Push notification to both phones with door name |
| Garage door closed | Either door → closed | Push notification to both phones |
| Garage open when away | Door open + both people away | Push notification with actionable "Close It" button |
| Left Garage — Open on Arrival | Louis or Lindsay not_home → home (door closed, 6 AM–11 PM, away 2+ min) | Opens the left garage — only if that person's auto-open toggle is on |
Per-person auto-open toggles (#64). Two switches gate the arrival auto-open independently: input_boolean.garage_auto_open_louis and input_boolean.garage_auto_open_lindsay (both default on). Flip one off to stop the left garage opening when that person gets home (e.g. Lindsay's away, or you just don't want it). They're on the Command dashboard → Garage pop-up (tap the Garage tile) and persist across restarts.
Section 3 — Salt Lamp¶
Sun-elevation on/off for the office salt lamp plug (switch.office_plug_1): on at civil dusk (sun −6°) when the office is occupied, off at civil dawn — so it burns only through the dark hours. Presence-gated by Section 64 (vacant → off, occupied → restore).
Section 4 — Air Quality (Bedroom) — Purifier Control¶
CO₂/VOC push-notification alerts were removed (2026-06-15) — too noisy and not actionable. The bedroom and office air-quality notifications no longer exist. What remains is silent purifier control (no phone alerts).
| Automation | Trigger | Action |
|---|---|---|
| Bedroom VOC Boost Purifier Medium | sensor.bedroom_volatile_organic_compounds > 400 µg/m³ for 1 min | Winix → Manual, medium (50%) |
| Bedroom VOC Boost Purifier High | VOCs climb above ~700 µg/m³ | Winix → Manual, high |
| Bedroom VOC Purifier Return to Auto | VOCs settle back down | Winix → Auto |
Purifier ramping respects the PlasmaWave bedtime schedule — it won't re-enable PlasmaWave overnight. See Climate → Automatic VOC Response.
Section 6 — Battery Alerts¶
Monitors all ZHA devices. Sends a push notification when any device battery drops below threshold. Covers all ~15 Zigbee devices including leak sensors, remote sensors, etc.
Section 7 — Thermostat Presence¶
Uses zone.home person count (not individual person entities) to transition Ecobee presets.
| Automation | Trigger | Action |
|---|---|---|
| Everyone leaves | zone.home count → 0 | Set both Ecobee thermostats to away preset |
| Someone arrives home | zone.home count → ≥1 | Set both Ecobee thermostats to home preset |
Why zone.home?
Using the zone count rather than person.louis or person.lindsay individually means "home" means anyone is home, and "away" means everyone has left.
Section 8 — Garage Open Too Long¶
| Automation | Trigger | Action |
|---|---|---|
| Garage open 30 min | Either door open for 30+ minutes | Push notification to both phones with actionable "Close It" button |
Section 9 — TOU Peak Mode¶
| Automation | Trigger | Action |
|---|---|---|
| Peak starts | sensor.tou_period → peak | input_boolean.peak_mode → on |
| Peak ends | sensor.tou_period → anything else | input_boolean.peak_mode → off |
Section 9b — TOU Pre-cool Before Peak (hot days)¶
| Automation | Trigger | Action |
|---|---|---|
| Pre-cool Before Peak (hot days) | 13:00 on a summer (May–Sep) weekday and sensor.weather_forecast_daily > input_number.precool_high_threshold (default 90 °F, tunable from the dashboard) | Set all three Ecobees (bedroom/office/dining_room) to a 70° cool hold (heat_cool low 65 / high 70) and raise input_boolean.precool_active |
| Resume Ecobee Control Before Peak | 14:59 (or HA start if already past 14:59) while input_boolean.precool_active is on | ecobee.resume_program (resume_all) on all three → releases the hold so Eco+ TOU takes over through peak; clears input_boolean.precool_active |
This is the one sanctioned case of HA writing HVAC setpoints — everywhere else the Ecobees' Eco+ TOU owns heating/cooling. input_boolean.precool_active bridges the two automations and guarantees the hold is always released: the resume automation fires only while it's on and re-checks on HA start, so a restart mid-window still hands control back before peak. The 13:00/14:59 times are fixed to the summer 3–6 PM peak (the only time a 90°+ high occurs), and the summer-month + weekday conditions keep those fixed times aligned with the schedule. The trip point lives in the input_number.precool_high_threshold helper (default 90 °F) so you can raise or lower it from the dashboard without editing the automation. User-facing detail: Extra Pre-cool on Hot Days.
Section 10 — Exterior Lights (unified renderer)¶
| Automation | Trigger | Action |
|---|---|---|
| Exterior Lights — Render Triggers | Civil dusk / civil dawn (sun elevation ±6°) · input_boolean.sleeping changes · 30-min tick (gated by input_boolean.exterior_lights_self_heal) · HA start | maintain the backyard latch; clear the manual override at dusk/dawn; stand down while input_boolean.exterior_lights_manual is on (except dusk/dawn); then script.exterior_lights_render |
All outdoor lights are owned by one area-driven priority renderer, script.exterior_lights_render (replaces the retired per-fixture String Lights and Front Exterior renderers). The automation above never lists individual lights — it re-runs the renderer, which figures out the target set at runtime from area membership: every light.* in the front_exterior, side_exterior, and backyard areas, plus an allowlist of switch-based light loads (today just switch.string_lights). Add a light to one of those areas and it's picked up automatically — no edit here. (The ~21 Inovelli config sub-switches and the plug's status LED are excluded; they aren't lights.)
Two schedules:
| Group | Lights | On | Off |
|---|---|---|---|
| Security | front + side exterior (porch, watch, flood) | civil dusk | civil dawn — all night |
| Backyard | backyard string lights | civil dusk | when you go to bed |
- Civil dusk / dawn is the sun reaching 6° below the horizon (
sun.sunelevation crossing −6°) — darker than sunset, roughly 20–30 minutes later. Triggering on the settled elevation avoids the sun-event race that once left the watch light off at sunset. - "When you go to bed" =
input_boolean.sleepingturns on (set by Good Night). A latch (input_boolean.backyard_bedtime_latch) keeps the backyard off for the rest of the night even if you get up before dawn; it clears at the next civil dusk. - The 30-minute tick re-checks and re-commands only lights in the wrong state, so a switch that briefly dropped offline heals itself instead of staying wrong until the next dusk/dawn. This poll is gated by the
input_boolean.exterior_lights_self_healtoggle (on by default) — turn it off from the dashboard/Dev Tools to disable the tick without touching the other triggers, and your choice persists across restarts (#89). - Manual override (2026-07-21). Three template switches —
switch.outside_lights(string + flood + porch),switch.backyard_lights(string),switch.front_flood_lights(flood) — let you toggle the outdoor fixtures by voice/dashboard. Each verb setsinput_boolean.exterior_lights_manual, which makes this automation stand the renderer down on every trigger except the civil dusk/dawn edges. Those edges clear the flag and reclaim the schedule, so a manual on/off sticks until the next day/night boundary (Good Morning also clears it). These switches replaced the removedscene.outdoor_lights_on/offand deliver #90. The switch state reflects the real fixtures (Alexa shows accurate on/off).
None of these lights are in group.lights — the renderer is the only thing that writes to them. Good Night still turns the backyard off (it sets input_boolean.sleeping, which trips the render). Full design: docs/superpowers/specs/2026-07-13-exterior-lights-renderer-design.md + docs/superpowers/specs/2026-07-21-outside-lights-alexa-switches-design.md.
Section 11 — Upstairs Hallway¶
| Automation | Trigger | Action |
|---|---|---|
| Hallway on | Sunset | switch.upstairs_hallway_1 → on |
| Hallway off (weeknights) | 9:30 PM Sunday–Thursday | switch.upstairs_hallway_1 → off |
| Hallway off (weekends) | 11:00 PM Friday–Saturday | switch.upstairs_hallway_1 → off |
Section 13 — Ring Camera Snapshot Refresh¶
Prevents stale thumbnails on Ring camera dashboard tiles during quiet periods (no motion detected).
| Automation | Trigger | Action |
|---|---|---|
| Ring snapshot refresh | Every 2 minutes (time_pattern) | homeassistant.update_entity on both Ring camera entities |
Section 14 — Cat Camera Privacy Mode¶
Webhook-triggered automations that toggle the Cat Camera privacy mode (blocks the RTSP stream). The entity and webhook ids are kept as office_cam_privacy* so the existing Siri Shortcuts keep working after the camera was renamed.
| Automation | Trigger | Action |
|---|---|---|
| Privacy on | Webhook | input_boolean.office_cam_privacy → on |
| Privacy off | Webhook | input_boolean.office_cam_privacy → off |
Section 16 — Internet Speed Monitoring¶
| Automation | Trigger | Action |
|---|---|---|
| Internet Speed — Degraded | sensor.speedtest_download below 100 Mbit/s for 90 min, AND input_boolean.internet_speed_degraded off | Latch helper on, notify Louis |
| Internet Speed — Recovered | sensor.speedtest_download above 150 Mbit/s — or returns from unavailable, or on HA start — AND helper on AND the current value is genuinely >150 | Latch helper off, notify Louis |
| Internet Speed — Speedtest Auto-Recover | sensor.speedtest_ping unavailable for 3 h | Reload the Speedtest config entry, notify Louis |
Detects the Eero WAN link degrading to "100mb mode" (auto-negotiating 100BASE-TX instead of gigabit). Healthy download is 300+ Mbit/s; degraded reads ~94. The 90-minute sustained window rides through a single congested test run (default Speedtest interval is ~60 min, so the window spans at least one confirming reading). input_boolean.internet_speed_degraded latches the alerted state so "Recovered" only fires after a real "Degraded" — and the 100/150 hysteresis gap prevents flapping at the threshold. Because numeric_state doesn't fire on a transition from unavailable, "Recovered" also re-checks when the sensor returns from unavailable and on HA start (gated by a value check) — otherwise an Ookla wedge-then-recover would leave the degraded flag stuck on. Regular (non-critical) notifications to Louis only.
Auto-Recover handles the integration's other failure mode: the Speedtest.net (Ookla) integration periodically wedges — after a failed hourly run its sensors go unavailable and never recover on their own, which silently empties the Grafana 🌐 Internet panels (they read from InfluxDB, and unavailable states aren't written). After 3 h stuck (well past the ~hourly cadence, so normal runs and HA restarts don't trip it), the automation calls homeassistant.reload_config_entry to revive it. One reload fixes the common single-bad-run case.
Remaining limitation: the degraded detector still catches slow, not a full outage — if the line is fully down, Speedtest can't run and sensor.speedtest_download just holds its last value (until it eventually goes unavailable, at which point Auto-Recover kicks in). A true real-time outage detector would still want a separate ping/connectivity check.
Section 15 — Energy Monitoring (Refoss EM16P)¶
The EM16P energy meter is installed and live (whole-home + per-circuit power, with cost/TOU sensors). These automations support it.
| Automation | Trigger | Action |
|---|---|---|
| Sync utility meter tariff to TOU period | sensor.tou_period changes (or HA start) | Set select.whole_home_energy to the matching tariff bucket so today's kWh splits correctly across peak / off-peak / super-off-peak |
| HVAC — 1st/2nd/3rd Floor Condenser short cycle | Each binary_sensor.*_condenser_running → off after running < 8 minutes | Persistent HA notification only (no phone alert) — data-collection during baseline |
The tariff sync is foundational, not load-shifting (load-shift automations wait for the 30-day baseline). Short-cycle alerts are HA-dashboard-only by design so they don't spam phones.
Section 17 — Food Temperature¶
One consolidated automation per garage appliance, each with five trigger branches (soft / hard / sensor-offline / battery / recover) selected via trigger.id. The for: duration on each numeric_state trigger is the debounce — a door-open/loading spike clears before the timer elapses, so it never alerts.
| Automation | Soft (warm → Louis) | Hard (critical → both, bypasses DND) | Recover (all-clear) |
|---|---|---|---|
| Food Temp — Refrigerator | > 42°F for 45 min | > 52°F for 10 min | < 40°F for 15 min |
| Food Temp — Refrigerator Freezer | > 10°F for 60 min | > 25°F for 15 min | < 10°F for 15 min |
| Food Temp — Deep Freezer | > 5°F for 60 min | > 20°F for 20 min | < 5°F for 15 min |
Each also fires a health alert (sensor unavailable 30 min, or battery < 15%) to Louis.
State latches: input_boolean.food_alert_<appliance> marks an active episode (so the all-clear only fires after a real alert and the soft tier doesn't spam); input_boolean.food_critical_<appliance> records that the critical tier fired (so the all-clear reaches both phones only when both were alerted, and a "warm" heads-up is suppressed once "FAILING" has fired). Sensors: sensor.refrigerator_temperature_sensor_temperature, sensor.refrigerator_freezer_temperature_sensor_temperature, sensor.freezer_temperature_sensor_temperature (+ matching _battery). User-facing detail: Food Temperature Alerts.
Section 18 — Garden / Plants¶
| Automation | Trigger | Action |
|---|---|---|
| Garden — Low Plant Moisture Alert | Any of the 7 indoor plant *_moisture sensors below its own input_number.plant_<name>_red threshold for 30 min, or daily at 9:00 | Notify Lindsay with the list of plants currently under their own threshold + readings |
One automation covers all seven indoor plants (Maury River, Fiddle Leaf 1-2, Monstera 1-3, Bird of Paradise). Each plant alerts against its own threshold — the input_number.plant_<name>_red "water below" helper Lindsay sets on the Plants dashboard — not a global cutoff (#56). There's a per-plant below: <helper> trigger, and the message list compares each sensor to its paired _red helper, so changing a plant's setpoint changes when (or whether) it alerts. The 30-minute for: debounce rides out the moisture swing right after watering; the daily 9am re-check means a persistently-dry plant keeps nudging until watered (a numeric_state edge trigger alone won't re-fire while a plant sits below its threshold). The message picks a random quip from a 50-phrase pool ({{ [...] | random }}) and appends the live thirsty-plant list (with each plant's reading and its threshold). Uses the plant_moisture notification tag so repeats replace rather than stack. User-facing detail: Plant Needs Water.
Section 19 — Energy / Peak-Hour Appliance Alerts¶
| Automation | Trigger | Action |
|---|---|---|
| Energy — Peak-Hour Appliance Alert | (A) any watched appliance circuit above its running threshold for 1 min while input_boolean.peak_mode is on, or (B) peak_mode → on while one is already running | Notify both Louis and Lindsay, naming the appliance(s) + the live rate and its multiple vs off-peak |
One automation, two paths selected via choose. Path A (started-during-peak) uses five per-circuit numeric_state triggers carrying a trigger.id; the action names that appliance. Path B (running-as-peak-begins) triggers on peak_mode → on and a running template variable scans all five circuits for whatever is currently above threshold. Path B is listed first in the choose so the peak-start event reports the full running list rather than mis-firing as a single-appliance Path-A alert.
Watched circuits + thresholds: Oven/Stove sensor.stove_power >500 W, Dryer sensor.dryer_power >500 W, Dishwasher sensor.em_dishwasher_power >150 W, Washer sensor.em_washer_power >120 W, Whirlpool tub sensor.em_whirlpool_power >300 W. (Stove and Dryer are the 240V leg-sum template sensors; the rest are raw single-CT channels.) The 1-minute for: debounce rejects motor inrush / transient blips.
Excluded by design: microwave (too brief to defer), fridge/freezers (not shiftable), and all HVAC — the Ecobee thermostats own heating/cooling and their Eco+ TOU logic already manages peak (the one exception is the §9b hot-day pre-cool, which writes setpoints only in the early-afternoon pre-peak window and hands control back before peak). The premium variable computes the live peak÷off-peak rate ratio (season-aware) and peak_end prints the window end (6 PM summer; 9 AM / 8 PM winter). Tag peak_appliance so repeats replace. mode: queued. This is Workstream 3 of the energy plan (projects/house_energy_strategy.md); it's rule-based (peak window + is-it-running), so it doesn't need the 30-day baseline that the threshold-based peak-budget alert does. User-facing detail: High-Cost Appliance During Peak.
Section 20 — Energy Project Reminders¶
Two one-shot, self-expiring reminders (admin-facing, to Louis) that nudge a revisit of the house-energy project once enough EM16P baseline data has accumulated. Each fires at 09:07 on its target date via a year-pinned date condition ({{ now().strftime('%Y-%m-%d') == '…' }}), so it fires exactly once and never matches again.
| Automation | Fires | Reminds to |
|---|---|---|
| Reminder: 14-day baseline ready | 2026-06-27 | Run the WS1 baseline — phantom-load audit, whirlpool heater check, baseline floor |
| Reminder: 30-day baseline ready | 2026-07-13 | Compute the peak/off-peak cost split + build the remaining WS3 TOU load-shift automations |
Delete both after they fire (data baseline started ~2026-06-13). Plan: projects/house_energy_strategy.md.
Section 21 — Desk Kiosk & Office TOU Bulb¶
| Automation | Trigger | Action |
|---|---|---|
| Kiosk Mode — On (webhook) | Webhook (Siri Shortcut "kiosk mode on") | input_boolean.kiosk_mode → on — hides the HA header/sidebar on the Desk iPad |
| Kiosk Mode — Off (webhook) | Webhook (Siri Shortcut "kiosk mode off") | input_boolean.kiosk_mode → off — shows the header/sidebar so you can navigate |
| Office Bulb Color by TOU Period | sensor.tou_period changes, or the bulb turns on | Tints light.louis_office_sengled_a19_bulb_1 to signal the price tier — blue = super off-peak (cheapest), green = off-peak, red = peak (most expensive). Only acts while the bulb is on |
Both webhooks use local_only: false so the Siri Shortcuts can trigger them from anywhere. The office bulb is a passive at-a-glance price indicator; it re-applies the color whenever the TOU period changes or the bulb is switched on.
Section 22 — Cat Detection (Cat Camera)¶
| Automation | Trigger | Action |
|---|---|---|
| Cat Detected — Laundry Room | binary_sensor.cat_camera_cat → on | Critical push to both phones ("🐱 Cat Detected") with a live snapshot |
| Cat Camera Motion — Person Detected (§38b) | binary_sensor.cat_camera_person → on | Critical push to both phones ("👁️ Motion — Cat Camera") with a live snapshot |
Both are interruption-level: critical (bypass silent/DND), attach /api/camera_proxy/camera.wyze_cam_v3_1, are suppressed while input_boolean.office_cam_privacy is on, and carry a 5-minute cooldown (mode: single + a trailing delay: "00:05:00" so a lingering subject can't re-alert). They discriminate: a cat fires only the cat alert (.../cat), a person only the motion alert (.../person) — never both.
The sensors are MQTT binary sensors (configuration.yaml) fed by the Frigate add-on, which runs object detection on the Cat Camera's Wyze/Thingino RTSP stream (Frigate tracks cat + person on this camera) and publishes per-object counts on frigate/cat_camera/<label> plus a total on frigate/cat_camera/all. See Cameras → Object Detection.
Section 23 — Night Lights¶
8× ThirdReality 3RSNL02043Z (light.night_light_1…_8), each with a motion (binary_sensor.night_light_N_motion) and lux (sensor.night_light_N_illuminance) sensor. All behavior funnels through one renderer.
script.night_lights_render — the single source of truth. Sets the lights (or a targets subset) by priority:
| Priority | Condition | Output |
|---|---|---|
| 1 | binary_sensor.any_leak_wet on | Blue (RGB 0/90/255) @ 80% — all lights (#76: water, distinct from alarm red) |
| 2 | input_boolean.good_night_error on | Red @ 50% — bedroom light NL7 only (others render normally) |
| 3 | sun.sun below horizon and a garage cover open | Amber (RGB 255/140/0) @ 40% |
| 4 | sun.sun above horizon | Off |
| 5 | input_boolean.sleeping on | Ember 2200 K @ 3% |
| 6 | (else: evening, awake, dark) | Warm 2700 K @ 10% (raised from 5% — at ~5% these bulbs render red in dark rooms, #98) |
| Automation | Trigger | Action |
|---|---|---|
| Night Lights — Re-render on state change | sun.sun up/down, either garage cover → open/closed, any_leak_wet → on/off | Call script.night_lights_render (all 8) |
| Night Lights — Motion boost | Any night_light_N_motion → on (cond: dark, no leak, garages closed) | Boost that light 2700 K @ 45% evening / 15% 11pm–6am / 8% asleep |
| ″ | Same motion → off for 2 min | script.night_lights_render with targets = just that light (restores its baseline) |
Sleep-aware: script.good_night sets input_boolean.sleeping (→ ember + dim motion) and script.good_morning clears it. Device defaults (set via ZHA on all 8): 2700 K startup, 4 s on/off fade, power-on = restore-previous.
Fade timing: each scene specifies a transition (seconds) so ramps are gentle but alerts snap — motion illuminate 2 s, fade back to floor/ember 8 s, daylight-off 4 s, garage amber 2 s, leak blue 0 s (instant). Tune these in script.night_lights_render + the motion automation.
Dark-gating uses sun.sun rather than the per-light lux sensors to avoid a self-feedback loop (a light's own glow raising its own lux reading). Full design + idea backlog: projects/smart_lighting_scenes.md.
Section 20 — Good Morning Routine¶
script.good_morning = script.wake_house + script.speak_morning_briefing — the full routine that the dashboard button and "Alexa, good morning" invoke (wakes and briefs).
script.wake_house(the wake primitive) — clearsinput_boolean.sleeping+input_boolean.good_night_error, re-renders the night lights, turns on the office TOU bulb + Corner Lamp (plain on, last state — the physical Govee one-touch scenes stay in charge), and disarms the Ring alarm. Call this to wake the house without speaking.script.speak_morning_briefing— the spoken briefing on the kitchen Echo (see below), also runnable on its own. It's a thin wrapper overscript.speak_briefing, the single source of the briefing text.
The auto kitchen-motion path also calls script.good_morning (one composition).
Auto-trigger + spoken briefing:
| Automation | Trigger | Action |
|---|---|---|
| Good Morning — Arm on Phone Off Charger | sensor.louis_iphone_battery_state → Not Charging (cond: 5:00–10:30 AM, sleeping on, not already armed) | input_boolean.morning_armed → on (nothing visible — just armed); logs the arm; if the kitchen Echo is unavailable, reloads alexa_devices now so the walk to the kitchen is its recovery window and the briefing speaks to a live Echo |
| Good Morning — Kitchen Briefing | binary_sensor.night_light_2_motion → on (cond: morning_armed on, after 5 AM) | Wait 1 s → disarm → run script.good_morning → script.speak_morning_briefing (logs the Echo's state) |
| Good Morning — Disarm if Unused | 10:30 AM (cond: morning_armed on) | morning_armed → off (so it can't fire at lunch); logs the unused-disarm |
| Good Morning — Debug Motion Not Armed | binary_sensor.night_light_2_motion → on (cond: 5:00–10:30 AM, sleeping on, morning_armed off) | Logbook only — records "in the kitchen but not armed" (the order-of-ops miss) |
| Energy — Snapshot Yesterday Cost | 11:59:30 PM | input_number.energy_cost_yesterday ← sensor.energy_cost_today |
On-demand briefing (#48, #58). The briefing text is factored into one shared primitive, script.speak_briefing (notify_service, media_player, set_outcome), so it can run independent of armed/motion. Two callers use it:
script.speak_morning_briefing→ kitchen Echo,set_outcome: true(the morning path + "good morning briefing" routine).script.play_daily_briefing→ "Alexa, play my daily briefing" on any Echo, played back on that same Echo. It targets whichevermedia_player.*_echohas alexa_media'slast_calledattribute set (the Echo you just spoke to), falling back to the kitchen Echo when none is flagged (e.g. right after a restart). It runs withset_outcome: falseso an on-demand call never overwrites the morning outcome tile. Exposed to Alexa as a scene; in the Alexa app add a custom routine — "play my daily briefing" → Activate scene → Play Daily Briefing.
The greeting is time-aware (Good morning / afternoon / evening from the ET clock), so an afternoon on-demand briefing greets you correctly.
Debug logging (#48). Every gate writes to the logbook (filter on input_boolean.morning_armed, name "Good Morning"): armed, auto-briefing-fired + Echo availability, motion-while-asleep-but-not-armed, and disarmed-unused. A few mornings of history pinpoints exactly where a missed briefing dropped — including the known dead-Echo case (the briefing "speaks" to an unavailable kitchen Echo with no sound).
Grafana tile (#48). input_text.morning_briefing_outcome (pending → armed → briefed, or missed_unarmed / missed_unused / echo_unavailable; reset to pending nightly by Good Night) flows to InfluxDB. The Home Intelligence → 🌅 Morning Briefing row shows a per-morning state-timeline (last 14 days) + a last-outcome stat — briefing reliability at a glance.
Echo-down resilience (#57). The kitchen Echo's alexa_devices integration goes unavailable periodically (Amazon throttling), and the briefing would otherwise speak into the void and falsely log briefed. So script.speak_morning_briefing: (1) if the Echo is offline, reloads alexa_devices and waits up to 45 s before speaking — but only a single reload, and only if the §46 heal automation hasn't reloaded in the last 20 min (aggressive reloads prolong Amazon's throttle); (2) sets the outcome honestly — briefed only if the Echo was reachable, else echo_unavailable (🔇 on the tile). This is the single source of the briefed/echo_unavailable outcome (the §20 automation no longer assumes success).
The briefing is built from a modular list (add a line = one entry to parts in script.speak_morning_briefing) and spoken via notify.send_message → notify.kitchen_echo_show_speak:
"Good morning. It is 72 degrees and partly cloudy, with a high near 85 degrees. Your sleep score was 97. SoFi closed at 17 dollars and 30 cents, down 0.1 percent. In premarket, SoFi is up 0.4 percent. Energy cost yesterday was 5 dollars and 60 cents."
- Weather — current temp + condition + today's forecast high (
weather.forecast_home). - Sleep score — last night's ResMed myAir score (
sensor.bedroom_airsense11_autoset_cpap_total_myair_score); only spoken when last night's data has synced, never a stale night. - SoFi close — price spoken as "dollars and cents" + up/down %.
- Premarket — only spoken during premarket hours (4:00–9:30 AM); omitted otherwise.
- Yesterday's energy cost — "dollars and cents".
Stock data comes from sensor.sofi_quote, a command_line: sensor that does Yahoo's v7 cookie→crumb→quote handshake in pure Python (the only free source with reliable extended-hours / premarket data). If the feed hiccups, the affected line is simply dropped.
Why kitchen motion, not the Echo's own motion
The kitchen Echo Show can speak but does not expose a motion sensor to HA (its detection is camera-based). So the briefing fires off the kitchen night light's motion (NL2) instead.
Section 21 — Good Night Routine¶
script.good_night runs this sequence (also via the dashboard button / "Alexa, good night"):
- Clear the error flag (
input_boolean.good_night_error). - All lights off — every bulb/lamp (
light.turn_offall) plusgroup.lights(the switch/plug lights the bulb sweep can't reach: back-yard string lights, office salt lamp, ramen sign). - Close both garage doors.
input_boolean.sleeping→ on; night lights drop to ember.- Verify the garage — wait 30 s; if either door is still open, raise
input_boolean.good_night_error(no alert — the red bedroom light is the signal). - Bedtime report — one quiet informational push to Louis's phone (anything notable still on, or "All secure").
- Re-render — if the error flag was raised, the bedroom night light (NL7) turns red.
Auto-trigger + error clearing:
| Automation | Trigger | Action |
|---|---|---|
| Good Night — Auto on Bed Presence + Phone Charging | sensor.louis_iphone_battery_state → Charging or binary_sensor.bedroom_occupancy → on | Run script.good_night — cond: 10 PM–4 AM, both bedroom occupied (Ecobee) and phone on charger, not already asleep |
| Good Night Error — Clear When Resolved | Either garage cover changes state (cond: good_night_error on) | If nothing's still wrong (both doors closed) → clear the flag + re-render (NL7 back to ember) |
Requiring bedroom occupancy AND phone-on-charger means a living-room top-up charge won't trigger bedtime. The red error indicator is deliberately extensible: any future Good Night step (lock the house, arm "home") sets the same good_night_error flag on failure, and the clear automation's "still wrong?" template is the single place to extend.
Section 22 — Ring Alarm Intrusion Alert¶
The Ring Alarm is the brain and dispatches natively (Ring Protect Pro). HA only reacts to its state via ring-mqtt, and never arms or dispatches.
| Automation | Trigger | Action |
|---|---|---|
| Alarm — Ring Triggered Critical Alert | alarm_control_panel.blairmont_alarm → triggered | Critical push to both phones (front-door snapshot + Security deep link) → persistent HA notification. The spoken announcement itself moved to the Alarm Audio Trigger Loop (#52) below — this automation no longer speaks |
| Alarm — Night Lights Red Beacon | alarm_control_panel.blairmont_alarm → triggered | All 8 night lights slowly breathe red (bright deep-red ↔ dim ember, ~5 s cycle) as a silent house-wide indicator until disarm, then restore the normal scene |
The {{ zones }} line in the message names any Ring contact/motion binary_sensor currently open (binary_sensor.personnel_door, binary_sensor.foyer_motion_sensor; extend the list as Retrofit Kit zones are added). If none can be identified it falls back to "check the Ring app." See Notifications.
The Red Beacon is a deliberately smooth pulse (2 s hardware fades), not a hard strobe — gentler on the eyes and on the Zigbee mesh (2 commands per cycle, not a per-bulb flood). It's the top priority in the script.night_lights_render stack, so a motion re-render mid-alarm stays red; mode: restart re-arms the breathe cleanly if the alarm re-triggers.
Whole-house Alarm Audio (#52–58). Spoken feedback for the alarm's full lifecycle, layered on top of the two automations above. Three layers, mirroring script.speak_briefing / script.night_lights_render: each automation below detects a state transition and builds the message, script.alarm_audio_event resolves policy (enable + volume + speakers), and script.alarm_speak does the low-level fan-out (volume_set → Alexa announce).
| # | Automation | Trigger | Speakers | Default volume | Message |
|---|---|---|---|---|---|
| 52 | Alarm Audio — Trigger Loop | → triggered | All 7 Echoes | 0.80 | "Security alert. The Ring alarm has been triggered. {{ zones }}." — repeats every input_number.alarm_audio_trigger_interval (default 12s) while triggered; mode: restart mirrors #51's beacon loop |
| 53 | Alarm Audio — Exit Delay | → arming | Front room Echo only | 0.60 | "Arming to {{ target }}. You have {{ secs }} seconds to exit." then "Ten seconds." ~10s before the countdown ends (skipped if arming already ended) |
| 54 | Alarm Audio — Entry Delay | → pending | All 7 Echoes | 0.80 | "Entry delay. Disarm now." |
| 55 | Alarm Audio — Armed | → armed_home / armed_away | Master bedroom + office Echo | 0.30 | "Alarm armed to home/away." |
| 56 | Alarm Audio — Disarmed | → disarmed, from armed_home/armed_away/pending | Master bedroom + office Echo (announce) + all 7 Echoes (volume reset) | 0.30 | "Alarm disarmed." — also sets all 7 Echoes to input_number.alarm_audio_reset_volume (default 0.40) |
| 57 | Alarm Audio — Failed to Arm | arming → disarmed | Master bedroom + office Echo | 0.60 | "The alarm could not arm. Check doors and windows." — best-effort: a manual cancel during exit delay also fires this |
| 58 | Alarm Audio — Test Button Pressed | Any input_button.alarm_audio_test_* pressed | That event's assigned speakers | That event's current volume | Canned sample message for the pressed event; force: true bypasses both the master and per-event enable toggles |
script.alarm_speak(message, speakers, volume) — low-level fan-out. Sets volume on all speakers via media_player.volume_set, waits 0.4s for it to settle, then calls notify.alexa_media_<slug> (per-device, data.type: announce) for each speaker. Owns how the house talks; nothing else.
script.alarm_audio_event(event, message, force) — mid-level policy. Resolves the fixed speaker list and the input_number.alarm_audio_<event>_volume for event (trigger/exit/entry/armed/disarmed/failed), gates on input_boolean.alarm_audio_enabled (master) and input_boolean.alarm_audio_<event>_enabled — unless force: true (used by test-button automation #58 so a disabled event can still be previewed) — then calls script.alarm_speak. Callers build message since only they have the live template vars (zones, target, seconds left); this script owns speaker/volume/enable resolution.
Helpers (configuration.yaml; none have initial:, so dashboard tuning persists across restarts):
input_boolean.alarm_audio_enabled— master on/off for all alarm audioinput_boolean.alarm_audio_{trigger,exit,entry,armed,disarmed,failed}_enabled— per-event on/offinput_number.alarm_audio_{trigger,exit,entry,armed,disarmed,failed}_volume— per-event volume, 0–1 (defaults 0.80 / 0.60 / 0.80 / 0.30 / 0.30 / 0.60)input_number.alarm_audio_reset_volume— volume all 7 Echoes are set to on disarm (default 0.40)input_number.alarm_audio_trigger_interval— seconds between trigger repeats, 5–60 (default 12)input_button.alarm_audio_test_{trigger,exit,entry,armed,disarmed,failed}— dial-in preview buttons (automation #58)
All of the above are tuned from the Alarm Audio dashboard (sidebar, alarm-audio) — see Security & Alarm.
Section 23 — Henry's TVs¶
| Automation | Trigger | Action |
|---|---|---|
| Henry's TVs — Off at 11:30 PM | Daily at 23:30 | Turn off Henry's TV + TV Tuner — but only the ones currently on |
A nightly failsafe so Henry's TV doesn't stay on overnight. It targets both media_player.henry_s_tv and media_player.henry_s_room_henrys_tv_tuner, but a template picks out only the players that are actually on (the tuner is often unavailable, so a blind turn_off would error), and a condition skips the run entirely when neither is on. Quiet — no notification.
Section 50 — Master Bedroom Sitting Lamp¶
The sitting-area wall lamp is a Philips Hue bulb, light.master_bedroom_sitting_room_lamp_bulb ("Sitting Room Lamp Bulb", via the Hue Bridge — migrated off the old switch.master_bedroom_sitting_lamp ZHA plug, which is left paired but no longer driven). One renderer decides the lamp's state so its behaviors never fight, the same pattern as script.night_lights_render — and because it's now a tunable-white bulb, when it's ON it renders a circadian colour-temperature + brightness that tracks the sun and reacts to how dark the room actually is.
"Dark" now means sun.sun below horizon OR the master-bedroom lux sensor sensor.night_light_7_illuminance below input_number.sitting_lamp_dark_lux (default ~20 lx, dashboard-tunable) — so a storm that darkens the room mid-day counts as dark. A missing lux reading is treated as bright (fail-safe: a dead sensor never lights the lamp).
script.master_bedroom_sitting_lamp_render — the single source of truth. A choose: evaluated top-down, first match wins:
| Priority | Condition | Output |
|---|---|---|
| 1 | zone.home count 0 (away) | stop — yield to the away presence-sim |
| 2 | input_boolean.morning_armed on and dark (waking) | On (circadian) — outranks sleeping |
| 3 | input_boolean.sleeping on | Off |
| 4 | media_player.master_bedroom_apple_tv playing or media_player.bedroom_tv on (movie) | Off |
| 5 | input_boolean.sitting_lamp_manual on (manual override) | On (circadian) — ignores the dark test |
| 6 | binary_sensor.bedroom_occupancy on and ( sun below horizon and 16:00–02:00 ) or ( sun above horizon and dark-by-lux ) | On (circadian) |
| — | (else) | Off |
Manual override / "reading light" (input_boolean.sitting_lamp_manual, Alexa "Sitting Room Lamp"). The dark test means the lamp never comes on when the room is bright — so this override forces it on at the circadian setting even when it's not dark. It sits below Sleeping and Movie (those still turn it off) but above the occupied branch, so it only adds the bright-time "I want light now" case. Because it's a bright-time on, cozy is deliberately gated to the storm case only (sun above horizon **and** dark-by-lux **and** occupied), so manual-on by day uses the true cool/bright midday circadian value, not the warm override. It auto-clears when the bedroom is empty for 2 minutes (master_bedroom_sitting_lamp_manual_clear), dropping back to renderer mode, and Good Morning clears it too. Exposed to Alexa as a switch named "Sitting Room Lamp" so "turn on/off the sitting room lamp" works; "off" returns to renderer mode (dark by day / when you leave — in the evening the automatic ambiance rule may keep it on).
Circadian output: when the sun is down it maps the shared sun-phase sensor sensor.ambiance_scene (the same 10-phase sensor the Govee lamps use) to a Kelvin + brightness — warm/dim at night, cooler/brighter around midday. When the lamp turns on with the sun up (only reachable via the storm branch in priority 5), it overrides to a fixed warm/cozy 2700 K, 65% rather than a cool midday look.
The 16:00–02:00 gate on priority 5 is load-bearing, not cosmetic: bedroom_occupancy stays on for a partner still asleep in bed, so without the gate the Evening rule would re-light the lamp around 5 AM the moment Good Morning clears sleeping. Morning-off is the kitchen-motion moment — when you reach the kitchen, NL2 motion fires Good Morning, which clears morning_armed and sleeping.
| Automation | Trigger | Action |
|---|---|---|
| Master Bedroom Sitting Lamp — Render Triggers | input_boolean.morning_armed/sleeping, binary_sensor.bedroom_occupancy → on (immediate) / → off (20 min), sun.sun below/above horizon, media_player.master_bedroom_apple_tv/bedroom_tv, zone.home, sensor.ambiance_scene (circadian drift), sensor.night_light_7_illuminance crossing the lux threshold, input_number.sitting_lamp_dark_lux | Call script.master_bedroom_sitting_lamp_render (mode: restart) |
| Master Bedroom Sitting Lamp — Away Presence Sim | Every 30 min (cond: away, dark, before 23:00) | Randomly light.turn_on (cozy 2500 K/40%) / light.turn_off (coin-flip) |
| Master Bedroom Sitting Lamp — Away Off at 11pm | Daily at 23:00 (cond: away) | light.turn_off |
| Master Bedroom Sitting Lamp — Clear Manual When Room Empties | binary_sensor.bedroom_occupancy → off for 2 min (cond: manual on) | Turn off input_boolean.sitting_lamp_manual → back to renderer mode |
The away presence-sim only runs because the renderer yields (stop) while away, so the two never fight. Returning home re-renders via the zone.home trigger and hands the bulb back.
A manual override (Apple Home/app) sticks until the next render trigger fires — since triggers are state changes, a manual toggle during stable occupancy can persist until a real event (sun/TV/sleep/leave/arrive/lux). Full design: docs/superpowers/specs/2026-07-21-sitting-lamp-hue-circadian-design.md (migration) and docs/superpowers/specs/2026-07-07-master-bedroom-sitting-lamp-design.md (original), backlog: projects/smart_lighting_scenes.md.
Section 59/60 — Ambiance Lamps: Corner Lamp + Office Lamp Renderers¶
Two Govee lamps in Louis's office — the Corner Lamp (H607C, 16 segments) and the 3-head Office Lamp (H600B ×3) — are pure-ambiance twins: they run the identical priority stack, driven by one shared schedule, and differ only in device id(s) and their per-SKU scene-id map. In automations.yaml these read # 59. Corner Lamp Renderer (#74) and # 60. Office Lamp Renderer — numbered in that file's own automation-numbering sequence (# 24. is already taken by Food Temp, so these live at the next free slots).
Guiding principle: ambiance lamps stay ambient; data lives on dedicated bulbs. Both lamps show only circadian + severe-weather ambiance and safety alerts. All data (TOU price, SOFI ticker) lives on the two Sengled A19 bulbs instead (§21, §19) — the Corner Lamp's former market-hours SOFI ticker (shipped with #74) was removed for this reason; see the "Shared ambiance schedule" box below.
Shared ambiance schedule¶
One package, configuration_ambiance_addition.yaml, is the single source of truth both renderers read — edit it once and both lamps follow:
sensor.ambiance_scene— template sensor, a 10-phase sky key driven bysun.sun's actual elevation (degrees) and whether it's rising or setting, not a fixed clock:e >= 30→ midday ·6–30°→ morning (rising) / afternoon (setting) ·0–6°→ sunrise (rising) / golden_hour (setting) — the golden-hour band ·-6–0°→ first_light (rising) / sunset (setting) — civil twilight / blue hour ·-12–-6°→ dawn (rising) / dusk (setting) — nautical twilight · below-12°→ night. Because it tracks the real sun, the same phase lands at very different clock times across the year (golden hour ~8:30pm in July, ~4:30pm in December). Each renderer maps this key to its own SKU's scene ids via asmapvariable inscripts.yaml; if the sensor is transientlyunknown/unavailable(e.g. right at HA start, before the template first evaluates), both renderers fall back tonightrather than raisingUndefinedErrorand aborting the render.binary_sensor.weather_is_severe—onwhenweather.forecast_homeislightning/lightning-rainy/pouring/hail/exceptional. Severe-only, by design: there is no mild-weather branch — plain rain/snow/fog is intentionally ignored (the forecast integration over-called "rainy" when it was merely cloudy), so only a genuine storm changes either lamp.
Scene delivery: both renderers fire scenes through one generalized rest_command.govee_scene (configuration.yaml; data vars device, sku, scene_id, param_id) — hitting the Govee Platform API's /device/control endpoint directly, bypassing govee2mqtt (which stays responsible for on/off power). This replaces the Corner-Lamp-only rest_command.govee_corner_lamp_scene from the original #74 design; the Office Lamp calls it once per bulb (3 device ids) so all 3 heads show the same scene.
Cloud-offline detection (per-head). The Govee Platform API returns HTTP 200 even when a device is offline — the real per-device status is buried in the response body (
content.code:200ok /400"Device is offline"). The apply-scene primitives therefore read the body viaresponse_variableand only store the dedupe token (input_text.*_last_render) when the scene actually applied. If a head is cloud-offline the token is left unchanged, so the next render retries it automatically instead of silently deduping the lamp to a stale scene, and a standing persistent notification (office_lamp_offline/corner_lamp_offline) names the offline head(s) — it auto-clears once every head applies. This fixes the original #office-lamp symptom where an offline head would freeze on an old vibe while the reachable head painted the new one (govee2mqtt's LAN path keeps on/off/state working, masking the dead cloud link that scenes need). A head being offline is a device/Wi-Fi condition (re-add it in the Govee app / improve signal), not a config bug — the software now surfaces and self-heals it rather than hiding it.
The shared priority stack (top wins, both lamps)¶
| Priority | Condition | Output |
|---|---|---|
| 0 | This lamp's own manual flag on (corner_lamp_manual / office_lamp_manual) | Presence-gated (#vibe-presence): office empty → power off (intent kept); occupied → honor input_text.<lamp>_vibe — "off" stays dark, "<id>,<param>" re-fires that vibe (deduped), else ("paint") powers on for Govee-app free-paint. A held vibe never burns in an empty office but restores on return. |
| 1 | alarm_control_panel.blairmont_alarm is triggered | Alert scene, force: true |
| 2 | binary_sensor.any_leak_wet on | Alert scene, force: true |
| 3 | Movie mode: sun.sun below horizon and media_player.office_apple_tv in playing/paused | Off |
| 4 | Louis away (person.louis ≠ home) | Off |
| 5 | Asleep (input_boolean.sleeping on) | Off |
| 5b | Office vacant (input_boolean.office_occupied off) | Off (token off:vacant) |
| 6 | Severe weather (binary_sensor.weather_is_severe on) | Storm scene |
| 7 | (else) | Ambiance → sensor.ambiance_scene → this lamp's scene map |
Alert scenes (1, 2) deliberately outrank the off-gates (3–5b) — an intrusion or leak while nobody's home, or during a movie, is exactly when the beacon matters most. Severe weather (6) sits below the off-gates, so a movie, an empty/sleeping office, or a vacant office (5b — see Section 64) is never interrupted by a storm animation. On-at-wake / off-at-sleep is emergent, not a dedicated branch: Good Night sets sleeping → branch 5 (off); Good Morning clears it (via script.wake_house, which also clears each lamp's manual flag) → branch 7 renders (= on).
As-built note: the original #74 design specced Strike/Thunderstorm as the Corner Lamp's alert scenes and a 6-block Sunrise→Candlelight circadian arc with a mild-weather override; these were tuned live against the physical lamp (alerts → Breathe/Dripping, Thunderstorm repurposed as the severe-weather layer, arc reshaped to 5 blocks). The subsequent #office-lamp refactor removed the SOFI-ticker branch entirely, added the movie-mode + Louis-away gates (replacing whole-house
zone.home == 0), introduced the Office Lamp as a twin, and moved both lamps onto the sharedsensor.ambiance_scene+binary_sensor.weather_is_severeschedule. Seedocs/superpowers/specs/2026-07-10-corner-lamp-renderer-design.mdanddocs/superpowers/specs/2026-07-10-office-lamp-ambiance-renderer-design.mdfor the full history. A later pass replaced the 5-block fixed-clock arc with the current sun-elevation-driven, 10-phasesensor.ambiance_scene(night/dawn/first_light/sunrise/morning/midday/afternoon/ golden_hour/sunset/dusk) described above, so the arc now tracks the real sky instead of the wall clock.
Corner Lamp (H607C) scene map¶
| Branch | Scene | id / paramId |
|---|---|---|
| Alarm alert | Breathe | 9690 / 16565 |
| Leak alert | Dripping | 9695 / 16570 |
| Severe weather | Thunderstorm | 9705 / 16580 |
| Ambiance — night | Starry Sky | 9707 / 16582 |
| Ambiance — dawn | Moonlit Night | 9650 / 16525 |
| Ambiance — first_light | Aurora | 9633 / 16508 |
| Ambiance — sunrise | Sunrise | 9558 / 16433 |
| Ambiance — morning | Morning | 9668 / 16543 |
| Ambiance — midday | Blue Sky | 9648 / 16523 |
| Ambiance — afternoon | Afternoon | 9669 / 16544 |
| Ambiance — golden_hour | Sunset Glow | 9649 / 16524 |
| Ambiance — sunset | Sunset | 9631 / 16506 |
| Ambiance — dusk | Twilight | 9670 / 16545 |
Power switch: switch.corner_lamp_power_switch (branches 1/2/6/7 turn it on before sending a scene; branches 3/4/5 turn it off). Dedup: input_text.corner_lamp_last_render.
Office Lamp (H600B ×3) scene map¶
| Branch | Scene | id / paramId |
|---|---|---|
| Alarm alert | Fire | 3128 / 3278 |
| Leak alert | — | 1176 / 1238 |
| Severe weather | — | 3125 / 3275 |
| Ambiance — night | Starry Sky | 1174 / 1236 |
| Ambiance — dawn | Dreamland | 1202 / 1264 |
| Ambiance — first_light | Awaken | 1197 / 1259 |
| Ambiance — sunrise | Warm | 1193 / 1255 |
| Ambiance — morning | Morning | 3133 / 3283 |
| Ambiance — midday | White Light | 10825 / 25319 |
| Ambiance — afternoon | Reading | 1186 / 1248 |
| Ambiance — golden_hour | Sunset Glow | 1173 / 1235 |
| Ambiance — sunset | Dusk | 1172 / 1234 |
| Ambiance — dusk | Quiet | 1195 / 1257 |
Power: 3 switch.office_lamp_bulb_1/2/3_power_switch, one Govee call per bulb via rest_command.govee_scene, so all 3 heads always show the same scene. Dedup: input_text.office_lamp_last_render. Device ids: bulb 1 07:F6:3C:DC:75:17:45:E8, bulb 2 D2:06:3C:DC:75:14:FF:8C, bulb 3 8D:97:3C:DC:75:11:63:54. H600B ids were chosen to read as coordinated with the H607C's, not pixel-identical — the two SKUs' scene libraries don't overlap.
Automations¶
| Automation | Trigger | Action |
|---|---|---|
| Corner Lamp — Render Triggers | input_boolean.corner_lamp_manual/sleeping, binary_sensor.any_leak_wet, alarm_control_panel.blairmont_alarm, sensor.ambiance_scene, binary_sensor.weather_is_severe, media_player.office_apple_tv, person.louis; sun sunset/sunrise events; every 5 min; HA start (the old fixed at: list — the 6 circadian block boundaries plus the 09:30/16:00 market-open/close legacy backstop — was pruned; now matches the Office Lamp's trigger set exactly) | Call script.corner_lamp_render |
| Corner Lamp — Manual On (webhook) | POST/GET webhook ac955561fe6ecd830cca9c276d32dc0c | input_boolean.turn_on → corner_lamp_manual |
| Corner Lamp — Manual Off / Auto (webhook) | POST/GET webhook bcc9004f10d08ba65ad3dfbf6d7b0831 | input_boolean.turn_off → corner_lamp_manual, then call script.corner_lamp_render |
| Office Lamp — Render Triggers | input_boolean.office_lamp_manual/sleeping, binary_sensor.any_leak_wet, alarm_control_panel.blairmont_alarm, person.louis, sensor.ambiance_scene, binary_sensor.weather_is_severe, media_player.office_apple_tv, sun.sun; HA start | Call script.office_lamp_render |
| Office Lamp — Manual On (webhook) | POST/GET webhook 5895625ac666da27f53277f7204c2e36 | input_boolean.turn_on → office_lamp_manual |
| Office Lamp — Manual Off / Auto (webhook) | POST/GET webhook 83a05cadc50d2e5ed89c6c4ed8d00d77 | input_boolean.turn_off → office_lamp_manual, then call script.office_lamp_render |
Manual override & reset: enter manual via the dashboard toggle or a manual-on webhook — the renderer yields (priority 0) so that lamp can be free-painted in the Govee app. Exit manual via the dashboard toggle, the auto webhook, or automatically at the next Good Morning: script.wake_house unconditionally clears both input_boolean.corner_lamp_manual and input_boolean.office_lamp_manual then re-renders both lamps, guaranteeing neither can stay stuck in manual past the next morning wake.
Superseded: the original 7 Corner Lamp gradient scenes (corner_lamp_cozy + Sunset/Fireplace/Ocean/Aurora/Cyberpunk/Lavender) and their corner_lamp_gradient per-segment throttle helper are deleted — native scenes replace them in one REST call instead of a ~30s, ~32-call per-segment paint. The Corner Lamp's SOFI-ticker branch (magnitude-tiered Lava/Fire/Sunset Glow/Ocean/Rustling Leaves/Forest/Green Reign scenes keyed off sensor.sofi_change_pct) is also retired — SOFI now lives solely on the Sengled bulb (light.louis_office_sengled_a19_bulb_2, §19, issue #75).
Full design: docs/superpowers/specs/2026-07-10-corner-lamp-renderer-design.md, docs/superpowers/specs/2026-07-10-office-lamp-ambiance-renderer-design.md. User-facing: docs/lighting.md. Integration background: projects/govee_floor_lamp.md, backlog: projects/smart_lighting_scenes.md.
Section 64 — Office Presence Light Control¶
An Aqara FP300 mmWave presence sensor (binary_sensor.louis_office_office_presence_sensor_occupancy, Matter/Thread) turns Louis' Office into a "follow-me" room: vacant → all office lights off; occupied → restore exactly. Three sibling automations own it, plus one helper and small edits to the self-driving controllers.
Helper / single source of truth: input_boolean.office_occupied (configuration.yaml) — the one debounced presence signal every office-light controller reads.
Automation (id) | Trigger | Does |
|---|---|---|
office_presence_vacant_off | occupancy → off, for: 1 min | Clears office_occupied → scene.create scene.office_presence_snapshot of the salt lamp only → homeassistant.turn_off all 3 plain lights → calls both Govee renderers (their off:vacant branch turns the lamps off) |
office_presence_occupied_restore | occupancy → on | Sets office_occupied → light.turn_on the TOU bulb (§37 tints it) → scene.turn_on the salt-lamp snapshot (skipped if no snapshot yet) → calls both Govee renderers (resume) |
office_presence_startup_sync | HA start | Re-derives office_occupied from the live sensor (the runtime snapshot is lost on restart); occupied → light the TOU bulb + settle lamps; vacant → plain lights off |
Only the salt lamp (switch.office_plug_1) is snapshotted for exact on/off restore. The two data bulbs are NOT snapshotted — they're live displays, so a captured on/off is wrong: the TOU bulb (..._a19_bulb_1) is turned on unconditionally on entry and §37 tints it (snapshotting it was the #92 bug — it drifted to off and the snapshot restore, skipped after a restart when the snapshot is unknown, never re-lit it); the SOFI bulb (..._a19_bulb_2) is re-rendered live by its own automation. The two Govee ambiance lamps aren't snapshotted either — their animated scene isn't captured by scene.create, so they're owned by their renderers' off:vacant branch (Section 59/60, priority 5b) and re-rendered on return.
Gates added so the self-driving lights stand down while vacant (a naïve all-off would be clobbered otherwise):
- Corner/Office lamp renderers — new priority-5b
off:vacantbranch (below alarm/leak so a house-wide alert still shows on an empty office; above severe-weather/ambiance so a storm/scene never lights it). SOFI Ticker — Stock Color—office_occupied == oncondition (its/5-min tick would otherwise re-light the bulb while vacant).Salt Lamp — On at Civil Dusk—office_occupied == oncondition (so dusk never lights an empty office).corner_lamp_render_triggers/office_lamp_render_triggers—input_boolean.office_occupiedadded to the trigger list.
The TOU bulb needs no gate — its automation only recolors an already-on bulb. Debounce leans on the FP300's on-device hold_time plus the 1-min for:. Full design: docs/superpowers/specs/2026-07-13-office-presence-lighting-design.md.
Section 41 — SOFI Ticker: Stock Color & Heartbeat¶
Numbered to match
automations.yaml's own inline §41/§41b, not this page's running order (see Sections 59/60 for the same cross-numbering convention).
light.louis_office_sengled_a19_bulb_2 ("SOFI Ticker") is a live SoFi stock display on Louis' Office desk: a resting tint that tracks the day's cumulative move, plus an event-driven heartbeat — a "lub-dub" flash burst — on a sharp intraday move. Two scripts own the bulb; two automations keep them fed.
Resting tint — script.sofi_ticker_render (single source of truth, extracted from the automation's old inline body in automations.yaml, before the split): tints the bulb by sensor.sofi_change_pct (SoFi's % change vs previous close) during US market hours (9:30–16:00 ET, weekdays) — hue = direction (green up / red down), saturation = magnitude (40→100 across 0→±4%, clamped), brightness constant at input_number.sofi_bulb_bri_pct (default 55%, dashboard-tunable, falls back to 55 if unavailable — §41 re-renders on change); amber (hue 40, sat 35) when flat; off outside market hours. The retry loop verifies hue + saturation + brightness together — see docs/superpowers/specs/2026-07-24-sofi-ticker-saturation-tint-design.md.
Automation (id) | Trigger | Action |
|---|---|---|
SOFI Ticker — Stock Color (sofi_ticker_color) | /5-min tick, HA start, office-return (office_occupied → on) | Calls script.sofi_ticker_render — conditioned on office_occupied == on (a vacant office doesn't relight) and sofi_heartbeat_active == off (won't stomp a beat mid-flight). It no longer triggers on the price sensor directly — that moved to §41b below |
Heartbeat — script.sofi_heartbeat(pairs, hue): a lub-dub burst of pairs bright/dim flash pairs in hue, every step transition: 0. (The bulb's own ramp is a fixed ~1.4%/s hardware behavior, so any transition: shorter than that is a no-op anyway — beats have to be instant snaps to read as a heartbeat rather than a fade.) It primes to a dim floor before the first beat (so beat 1 punches with the same contrast as the rest), holds a 900 ms gap between pairs, and lets the final beat resolve up into the resting tint rather than fading to the floor — a clean finish into the day's glow even when the beat color differs from the resting color (a red down-jerk on a green up-day). Sets input_boolean.sofi_heartbeat_active on for its duration, then off, then calls script.sofi_ticker_render to settle back to the resting tint.
Automation (id) | Trigger | Action |
|---|---|---|
SOFI Ticker — Heartbeat on Sharp Move (sofi_ticker_heartbeat) | sensor.sofi_change_pct state change (cond: office_occupied == on) | Computes Δ = new − old straight from the trigger's from_state/to_state; below the freshness/threshold gates it just re-renders the resting tint; otherwise calls script.sofi_heartbeat with the beat count and direction below |
Beat mapping — size of the move that just happened (not the day's cumulative change) sets the pair count:
| |Δ%| of this move ≥ | Pairs |
|---|---|
input_number.sofi_heartbeat_threshold (default 0.5, dashboard-tunable) | 2 |
| 1.0 | 3 |
| 1.5 | 4 |
Below the threshold, no beat plays — the automation just re-renders the resting tint (a normal small tick).
Move direction, not day direction: the burst's hue is green for an up-tick / red for a down-tick of that single move, independent of the bulb's resting (day-cumulative) tint — a stock that's up on the day but just dropped sharply flashes red, then settles back to its green resting tint.
Freshness guard: a Δ is only actioned if the previous reading (trigger.from_state) last changed under 600 seconds ago. This specifically kills the false beat at the 9:30 market open, where the sensor's first poll of the day jumps from yesterday's close (or unknown) to the opening print — a real number, but not a "move" worth a heartbeat — and guards the same false-beat risk after an HA restart or a stalled sensor.
Full design: docs/superpowers/specs/2026-07-22-sofi-heartbeat-design.md.
Notification Deep Links¶
Push notifications include a url: that deep-links to the relevant dashboard tab. These are stored as template sensors (sensor.nabu_casa_security_url, sensor.nabu_casa_climate_url) in configuration_tou_addition.yaml — values come from secrets.yaml. This keeps !secret directives out of automations.yaml, which fixes the HA automation editor's YAML loader limitation.