Skip to content

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 to switch.master_bedroom_sitting_lamp. Do NOT touch switch.upstairs_hallway_1 (Henry's Ramen Sign): its §11 hallway schedule, the §49 toggle button, and its group.lights membership are a different plug.
  • plug_2 is not in group.lights — the renderer owns off-at-sleep (sleeping → off). Do not add it to group.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 clears sleeping.
  • 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.txt until make snapshot post-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 scripts fixture + the failing structural test

In tests/conftest.py, add next to the configuration fixture (mirror its style — it uses @pytest.fixture + load_yaml):

@pytest.fixture
def scripts():
    return load_yaml("scripts.yaml") or {}

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.yaml references

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_plug_2:
      name: Upstairs Hallway Plug 2
    switch.master_bedroom_sitting_lamp:
      name: Master Bedroom Sitting Lamp
(Leave 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)

  1. Live registry rename (safe stop → back up → edit → start): switch.upstairs_hallway_plug_2switch.master_bedroom_sitting_lamp in /config/.storage/core.entity_registry (single entity, no collision). Back up first (core.entity_registry.bak-sittinglamp).
  2. 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 ref switch.upstairs_hallway_plug_2switch.master_bedroom_sitting_lamp (leave the power-card sensor refs), commit.
  3. Merge → push. CI: test → deploy-ha (full restart, since homekit.yaml/configuration-class files changed) + deploy-lovelace (stop-swap-start for the dashboard).
  4. make snapshot — folds switch.master_bedroom_sitting_lamp into entities.txt; remove the allowlist line; commit. (Watch for unrelated drift as before.)
  5. 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.
  6. Close #72, move the board card to Done.

Notes for the implementer

  • The renderer's choose is first-match-wins = the priority stack; the away branch uses stop: to yield. Never reorder without re-checking: waking must stay above sleeping.
  • The Evening branch's 16:00–02:00 time gate is load-bearing, not cosmetic: it stops a partner asleep in bed (still bedroom_occupancy: on) from re-lighting the lamp at ~5am after Good Morning clears sleeping. Morning-off happens when morning_armed→off (kitchen NL2 motion fires Good Morning); the renderer's morning_armed trigger catches it. Do not drop the time gate.
  • sun.sun triggers use to: below_horizon / to: above_horizon (two triggers) — a bare sun.sun state trigger would fire on every elevation attribute update.
  • The away-sim uses if: "{{ [true,false] | random }}" (HA Jinja random is fine here — this is not a workflow script).
  • Do NOT touch switch.upstairs_hallway_1, the §11 automations, the §49 toggle, or group.lights.