Master-Bedroom Sitting-Lamp Automations — Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Drive the master-bedroom sitting-area lamp (switch.upstairs_hallway_plug_2, being renamed to switch.master_bedroom_sitting_lamp) with a single priority renderer covering evening ambiance, wake-to-lamp, movie mode, and away presence-simulation.
Architecture: One renderer script (script.master_bedroom_sitting_lamp_render, a choose: priority stack like script.night_lights_render) + one trigger automation that re-renders on any relevant change + two away-presence-sim automations. The ZHA switch entity is renamed in the live registry (a controller finish step); repo files reference the new id (allowlisted until the post-deploy snapshot).
Tech Stack: HA modern automation/script schema (triggers:/conditions:/actions:/choose:/stop:), Jinja, pytest.
Global Constraints¶
- Target device is
switch.upstairs_hallway_plug_2→ renamed toswitch.master_bedroom_sitting_lamp. Do NOT touchswitch.upstairs_hallway_1(Henry's Ramen Sign): its §11 hallway schedule, the §49 toggle button, and itsgroup.lightsmembership are a different plug. - plug_2 is not in
group.lights— the renderer owns off-at-sleep (sleeping → off). Do not add it togroup.lights. - Rename touches only the switch entity. Leave the diagnostic sensors (
sensor.upstairs_hallway_plug_2_*) as-is (energy history + dashboard power card). - Renderer priority (first match wins): away→
stop· (morning_armed on AND dark)→on · sleeping→off · movie→off · (bedroom_occupancy on AND dark AND time 16:00–02:00)→on · else off.dark = sun.sun below_horizon. The evening time gate prevents a partner asleep in bed from re-lighting the lamp in the early morning after Good Morning clearssleeping. - Entities:
input_boolean.morning_armed,input_boolean.sleeping,binary_sensor.bedroom_occupancy,sun.sun,zone.home,media_player.master_bedroom_apple_tv,media_player.bedroom_tv— all exist. - Unique automation
id/alias. Modern schema, matching existing automations. - The new switch id goes in
tests/fixtures/entities_allow.txtuntilmake snapshotpost-deploy. - Do not push during task execution — the registry rename + storage-dashboard edit + deploy are controller finish steps.
Task 1: Rename repo refs + renderer script + trigger automation¶
Files: - Modify: homekit.yaml (whitelist ~line 48 + entity_config ~line 100) - Modify: tests/fixtures/entities_allow.txt - Modify: scripts.yaml (append the render script) - Modify: automations.yaml (append the trigger automation, new section) - Modify: tests/conftest.py (add a scripts fixture) + Create: tests/test_sitting_lamp.py
Interfaces: - Produces: script.master_bedroom_sitting_lamp_render, automation.master_bedroom_sitting_lamp_render_triggers, all targeting switch.master_bedroom_sitting_lamp.
- [ ] Step 1: Add a
scriptsfixture + the failing structural test
In tests/conftest.py, add next to the configuration fixture (mirror its style — it uses @pytest.fixture + load_yaml):
Create tests/test_sitting_lamp.py:
"""Structural checks for the master-bedroom sitting-lamp renderer + automations (#72)."""
LAMP = "switch.master_bedroom_sitting_lamp"
def test_render_script_exists_and_targets_lamp(scripts):
assert "master_bedroom_sitting_lamp_render" in scripts, "render script missing"
body = str(scripts["master_bedroom_sitting_lamp_render"])
assert LAMP in body, "render script doesn't target the lamp"
# The evening time gate is load-bearing (stops a sleeping partner re-lighting at ~5am).
assert "16:00:00" in body and "02:00:00" in body, "evening branch missing its 16:00-02:00 time gate"
def test_render_triggers_automation_present(automations):
by_id = {a.get("id"): a for a in automations}
a = by_id.get("master_bedroom_sitting_lamp_render_triggers")
assert a is not None, "render-triggers automation missing"
assert a["actions"][0]["action"] == "script.master_bedroom_sitting_lamp_render"
- [ ] Step 2: Run the test to verify it fails
Run: python3 -m pytest tests/test_sitting_lamp.py -v Expected: FAIL — render script / automation absent.
- [ ] Step 3: Rename the two
homekit.yamlreferences
In homekit.yaml: change the whitelist entry - switch.upstairs_hallway_plug_2 → - switch.master_bedroom_sitting_lamp. And the entity_config block:
switch.upstairs_hallway_1 / "Upstairs Hallway Plug 1" untouched — different plug.) - [ ] Step 4: Allowlist the new switch id
Append to tests/fixtures/entities_allow.txt:
# Master-bedroom sitting lamp (#72) — ZHA switch renamed from switch.upstairs_hallway_plug_2;
# exists under the new id after the registry rename deploys. Fold into entities.txt via make snapshot.
switch.master_bedroom_sitting_lamp
- [ ] Step 5: Append the render script to
scripts.yaml
Add at the end of scripts.yaml (top-level key):
# Master Bedroom Sitting Lamp — priority renderer (like night_lights_render). Decides the
# lamp's on/off from a single priority stack so its four behaviors never fight. Called by the
# "… Render Triggers" automation. The lamp is switch.master_bedroom_sitting_lamp (formerly
# upstairs_hallway_plug_2). Not in group.lights — this renderer owns off-at-sleep.
master_bedroom_sitting_lamp_render:
alias: "Master Bedroom Sitting Lamp — render"
icon: mdi:floor-lamp
mode: restart
sequence:
- choose:
# 1. Away → yield; the away presence-sim owns the lamp while nobody's home.
- conditions:
- condition: template
value_template: "{{ (states('zone.home') | int(0)) == 0 }}"
sequence:
- stop: "away — presence-sim owns the lamp"
# 2. Waking (phone off charger armed Good Morning) AND dark → ON. Outranks sleeping,
# which is still on until Good Morning fires at the kitchen-motion stage.
- conditions:
- condition: state
entity_id: input_boolean.morning_armed
state: "on"
- condition: state
entity_id: sun.sun
state: below_horizon
sequence:
- action: switch.turn_on
target:
entity_id: switch.master_bedroom_sitting_lamp
# 3. Sleeping → OFF.
- conditions:
- condition: state
entity_id: input_boolean.sleeping
state: "on"
sequence:
- action: switch.turn_off
target:
entity_id: switch.master_bedroom_sitting_lamp
# 4. Movie — bedroom TV / Apple TV playing → OFF.
- conditions:
- condition: template
value_template: >-
{{ is_state('media_player.master_bedroom_apple_tv', 'playing')
or is_state('media_player.bedroom_tv', 'on') }}
sequence:
- action: switch.turn_off
target:
entity_id: switch.master_bedroom_sitting_lamp
# 5. Occupied AND dark AND evening hours (16:00–02:00) → ON (evening ambiance).
# The time gate excludes the early-morning wake window so a partner still asleep
# in bed (bedroom_occupancy still 'on') can't re-light the lamp at ~5am after Good
# Morning clears `sleeping`. HA reads after>before as the wrap-around 16:00→02:00.
- conditions:
- condition: state
entity_id: binary_sensor.bedroom_occupancy
state: "on"
- condition: state
entity_id: sun.sun
state: below_horizon
- condition: time
after: "16:00:00"
before: "02:00:00"
sequence:
- action: switch.turn_on
target:
entity_id: switch.master_bedroom_sitting_lamp
# else → OFF
default:
- action: switch.turn_off
target:
entity_id: switch.master_bedroom_sitting_lamp
- [ ] Step 6: Append the trigger automation to
automations.yaml
Add at the end of automations.yaml (new list item + section comment):
##############################################################################
# 50. Master Bedroom Sitting Lamp — Renderer (evening / wake / movie / away-sim)
##############################################################################
# The sitting-area wall lamp (switch.master_bedroom_sitting_lamp, formerly
# upstairs_hallway_plug_2) is driven by script.master_bedroom_sitting_lamp_render
# so its four behaviors never fight over the plug. This automation re-renders on
# any relevant change; the away presence-sim automations follow below.
- alias: "Master Bedroom Sitting Lamp — Render Triggers"
id: master_bedroom_sitting_lamp_render_triggers
triggers:
- trigger: state
entity_id: input_boolean.morning_armed
- trigger: state
entity_id: input_boolean.sleeping
- trigger: state
entity_id: binary_sensor.bedroom_occupancy
to: "on"
- trigger: state
entity_id: binary_sensor.bedroom_occupancy
to: "off"
for: "00:20:00"
- trigger: state
entity_id: sun.sun
to: below_horizon
- trigger: state
entity_id: sun.sun
to: above_horizon
- trigger: state
entity_id:
- media_player.master_bedroom_apple_tv
- media_player.bedroom_tv
- trigger: state
entity_id: zone.home
actions:
- action: script.master_bedroom_sitting_lamp_render
mode: restart
- [ ] Step 7: Run the tests + lint
Run: python3 -m pytest tests/test_sitting_lamp.py -v → both pass. Run: make lint && make pytest → yamllint clean; full suite green (entity-ref passes via the allowlist). Also parse-check: python3 -c "import yaml; d=yaml.safe_load(open('automations.yaml')); ids=[a.get('id') for a in d]; assert ids.count('master_bedroom_sitting_lamp_render_triggers')==1; print('ok', len(d))"
- [ ] Step 8: Commit
git add homekit.yaml tests/fixtures/entities_allow.txt scripts.yaml automations.yaml tests/conftest.py tests/test_sitting_lamp.py
git commit -m "feat(lighting): master-bedroom sitting-lamp renderer + trigger automation (#72)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 2: Away presence-simulation¶
Files: - Modify: automations.yaml (2 automations after the render-triggers automation) - Modify: tests/test_sitting_lamp.py (assert both present)
Interfaces: Consumes the renamed lamp + renderer (Task 1).
- [ ] Step 1: Add the failing test
Append to tests/test_sitting_lamp.py:
def test_away_sim_automations_present(automations):
ids = {a.get("id") for a in automations}
assert "master_bedroom_sitting_lamp_away_sim" in ids
assert "master_bedroom_sitting_lamp_away_off" in ids
- [ ] Step 2: Run to verify it fails
Run: python3 -m pytest tests/test_sitting_lamp.py::test_away_sim_automations_present -v → FAIL.
- [ ] Step 3: Add the two away-sim automations
In automations.yaml, immediately after the "Render Triggers" automation (still under §50):
- alias: "Master Bedroom Sitting Lamp — Away Presence Sim"
id: master_bedroom_sitting_lamp_away_sim
# Randomized evening presence while nobody's home. The renderer yields when away
# (its priority-1 stop), so this owns the lamp; on return, the render-triggers
# automation's zone.home trigger re-renders and takes it back.
triggers:
- trigger: time_pattern
minutes: "/30"
conditions:
- condition: template
value_template: "{{ (states('zone.home') | int(0)) == 0 }}"
- condition: state
entity_id: sun.sun
state: below_horizon
- condition: time
before: "23:00:00"
actions:
- if: "{{ [true, false] | random }}"
then:
- action: switch.turn_on
target:
entity_id: switch.master_bedroom_sitting_lamp
else:
- action: switch.turn_off
target:
entity_id: switch.master_bedroom_sitting_lamp
mode: single
- alias: "Master Bedroom Sitting Lamp — Away Off at 11pm"
id: master_bedroom_sitting_lamp_away_off
triggers:
- trigger: time
at: "23:00:00"
conditions:
- condition: template
value_template: "{{ (states('zone.home') | int(0)) == 0 }}"
actions:
- action: switch.turn_off
target:
entity_id: switch.master_bedroom_sitting_lamp
mode: single
- [ ] Step 4: Run tests + lint
Run: python3 -m pytest tests/test_sitting_lamp.py -v → all pass. Run: make lint && make pytest → green.
- [ ] Step 5: Commit
git add automations.yaml tests/test_sitting_lamp.py
git commit -m "feat(lighting): sitting-lamp away presence-simulation (#72)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 3: Docs + final gate¶
Files: - Modify: CLAUDE.md, docs/automations.md, docs/lighting.md, projects/smart_lighting_scenes.md
Interfaces: none.
- [ ] Step 1: CLAUDE.md
Add a "Master Bedroom Sitting Lamp" automation entry to the Architecture automations list (its own numbered/lettered item) describing the renderer + 4 behaviors + the rename (plug_2 → switch.master_bedroom_sitting_lamp). Do NOT modify the §11 Upstairs Hallway entry (different plug). If the Key Entities / group.lights notes mention upstairs_hallway, leave _1 (ramen sign) as-is; only note the new sitting-lamp id.
- [ ] Step 2:
docs/automations.md
Add a section for the sitting-lamp renderer + triggers + away-sim (mirror the file's per-section style — read a neighboring section first).
- [ ] Step 3:
docs/lighting.md
Household-facing description: what the sitting lamp does (comes on in the evening when you're in the bedroom and it's dark; on when you wake if still dark; off during movies; off at bedtime; randomized when away).
- [ ] Step 4:
projects/smart_lighting_scenes.md
Mark the away-simulation backlog item as first implemented here (the sitting lamp).
- [ ] Step 5: Full gate
Run: make lint && make pytest → yamllint clean; all pytest green. make check-config (Tier 1) runs in CI. If docs/** changed, avoid broken cross-page links (mkdocs build --strict on push).
- [ ] Step 6: Commit
git add CLAUDE.md docs/automations.md docs/lighting.md projects/smart_lighting_scenes.md
git commit -m "docs(lighting): document master-bedroom sitting-lamp automations (#72)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Controller finish steps (NOT subagent tasks — run after all tasks + final review)¶
- Live registry rename (safe stop → back up → edit → start):
switch.upstairs_hallway_plug_2→switch.master_bedroom_sitting_lampin/config/.storage/core.entity_registry(single entity, no collision). Back up first (core.entity_registry.bak-sittinglamp). - Storage dashboard (do NOT let a subagent edit it — pull fresh, user edits in UI):
ssh haos "cat /config/.storage/lovelace.home_command_dashboard" > lovelace/home_command_dashboard.json, change the switch tile entity refswitch.upstairs_hallway_plug_2→switch.master_bedroom_sitting_lamp(leave the power-card sensor refs), commit. - Merge → push. CI: test →
deploy-ha(full restart, sincehomekit.yaml/configuration-class files changed) +deploy-lovelace(stop-swap-start for the dashboard). make snapshot— foldsswitch.master_bedroom_sitting_lampintoentities.txt; remove the allowlist line; commit. (Watch for unrelated drift as before.)- Verify live (HA API): the renamed switch exists; the 3 automations + render script loaded; trip
input_boolean.morning_armed/occupancy to sanity-check the renderer. Re-add the accessory's room/name in Apple Home. - Close #72, move the board card to Done.
Notes for the implementer¶
- The renderer's
chooseis first-match-wins = the priority stack; the away branch usesstop:to yield. Never reorder without re-checking: waking must stay above sleeping. - The Evening branch's
16:00–02:00time gate is load-bearing, not cosmetic: it stops a partner asleep in bed (stillbedroom_occupancy: on) from re-lighting the lamp at ~5am after Good Morning clearssleeping. Morning-off happens whenmorning_armed→off (kitchen NL2 motion fires Good Morning); the renderer'smorning_armedtrigger catches it. Do not drop the time gate. sun.suntriggers useto: below_horizon/to: above_horizon(two triggers) — a baresun.sunstate trigger would fire on every elevation attribute update.- The away-sim uses
if: "{{ [true,false] | random }}"(HA Jinjarandomis fine here — this is not a workflow script). - Do NOT touch
switch.upstairs_hallway_1, the §11 automations, the §49 toggle, orgroup.lights.