Office Lamp + Shared Ambiance Renderer 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: Make the 3-head office lamp (H600B ×3) and the corner lamp (H607C) twin pure-ambiance fixtures driven by one shared schedule; remove the SOFI ticker from the corner lamp.
Architecture: A shared sensor.ambiance_scene (time+weather → logical scene key) and binary_sensor.weather_is_severe are the single source of the ambiance policy. Two renderer scripts (corner_lamp_render, office_lamp_render) run an identical priority stack, each mapping the logical key to its own SKU's scene ids and firing via a generalized rest_command.govee_scene(device, scene_id, param_id).
Tech Stack: Home Assistant YAML (packages, template sensors, rest_command, scripts, automations), Govee Platform API, the repo's tiered test suite, live /api/template render-testing.
Global Constraints¶
- Ambiance lamps stay ambient; data lives on the Sengled bulbs. The two Govee lamps show ONLY circadian + weather ambiance + safety alerts (alarm/leak/severe). NO SOFI/TOU/status on either.
- Clock is ET; time blocks:
05–12 morning · 12–17 afternoon · 17–20 twilight · 20–23 night · 23–05 starry_sky. - Logical scene keys (
sensor.ambiance_scenevalues):morning afternoon twilight night starry_sky— time-of-day only, NO mild weather (Met.no forecast unreliable; per user decision 2026-07-10). - Weather affects the lamps ONLY via
binary_sensor.weather_is_severe(lightning/lightning-rainy/pouring/hail/exceptional) → the storm branch. Noraining/snowing/dense_fogambiance keys. - Unified priority stack (top wins): 0 manual→yield · 1 alarm→alert · 2 leak→alert · 3 movie-mode (
sun.sunbelow ANDmedia_player.office_apple_tvinplaying/paused)→off · 4person.louis≠home→off · 5 asleep→off · 6 severe→storm · 7 default→ambiance. (No SOFI branch on either lamp.) - Everything visible is an animated Govee scene (never a solid color) except off/yield.
- Device ids: corner
99:49:E7:99:B2:60:F0:8E; office bulbs07:F6:3C:DC:75:17:45:E8(1),D2:06:3C:DC:75:14:FF:8C(2),8D:97:3C:DC:75:11:63:54(3). One secret:govee_api_key(already on box). - Corner (H607C) scene ids: morning
9668/16543· afternoon9669/16544· twilight9670/16545· night9709/16584· starry_sky9707/16582· alarm9690/16565(Breathe) · leak9695/16570(Dripping) · storm9705/16580(Thunderstorm). - Office (H600B) scene ids (default map — preview-tune the alert/storm picks): morning
3133/3283(Morning) · afternoon1197/1259(Awaken) · twilight1172/1234(Dusk) · night3130/3280(Candlelight) · starry_sky1174/1236(Starry Sky) · alarm3128/3278(Fire) · leak1176/1238(River) · storm3125/3275(Aurora). H600B has no native storm scene — Aurora is the nearest match. - Branch:
feat/office-lamp-ambiance(spec committed). TDD-for-YAML: each task's "test" ismake lint+ (where possible)make check-config, plus live/api/templaterender-tests for template logic; runtime behavior verified at deploy (Task 8).
File Structure¶
configuration_ambiance_addition.yaml— NEW package:sensor.ambiance_scene+binary_sensor.weather_is_severe.configuration.yaml— register the package underhomeassistant.packages; generalizerest_command(→govee_scene); addinput_boolean.office_lamp_manual+input_text.office_lamp_last_render.scripts.yaml— generalizecorner_lamp_apply_scene; refactorcorner_lamp_render; addoffice_lamp_apply_scene/office_lamp_off/office_lamp_render; editwake_house.automations.yaml— refactorcorner_lamp_render_triggers; addoffice_lamp_render_triggers+ 2 office webhooks.tests/fixtures/entities_allow.txt— allowlist new helpers/sensors pre-deploy.- Docs:
projects/smart_lighting_scenes.md,CLAUDE.md,docs/lighting.md,docs/automations.md; corner-lamp spec note.
Task 1: Shared schedule package (sensor.ambiance_scene + binary_sensor.weather_is_severe)¶
Files: - Create: configuration_ambiance_addition.yaml - Modify: configuration.yaml (add package entry) - Modify: tests/fixtures/entities_allow.txt
Interfaces: - Produces: sensor.ambiance_scene (state = one of the 8 logical keys); binary_sensor.weather_is_severe (on/off).
- [ ] Step 1: Create the package file
configuration_ambiance_addition.yaml:
# Shared office AMBIANCE schedule — the single source of truth for what calm scene the
# two Govee ambiance lamps (corner + office 3-head) show. Tweak the time blocks / weather
# map HERE and both lamps follow. Loaded via homeassistant.packages.ambiance. Each lamp
# maps sensor.ambiance_scene's logical key to its own SKU scene ids. Severe weather is a
# SEPARATE higher-priority signal (binary_sensor.weather_is_severe), not a scene key here.
template:
- sensor:
- name: "Ambiance Scene"
unique_id: ambiance_scene
icon: mdi:palette-outline
state: >
{% set h = now().hour %}
{% if 5 <= h < 12 %}{% set block = 'morning' %}
{% elif 12 <= h < 17 %}{% set block = 'afternoon' %}
{% elif 17 <= h < 20 %}{% set block = 'twilight' %}
{% elif 20 <= h < 23 %}{% set block = 'night' %}
{% else %}{% set block = 'starry_sky' %}{% endif %}
{% set wx = {'rainy':'raining','snowy':'snowing','snowy-rainy':'snowing','fog':'dense_fog'} %}
{{ wx.get(states('weather.forecast_home'), block) }}
- binary_sensor:
- name: "Weather Is Severe"
unique_id: weather_is_severe
icon: mdi:weather-lightning
state: >
{{ states('weather.forecast_home') in
['lightning','lightning-rainy','pouring','hail','exceptional'] }}
- [ ] Step 2: Register the package in
configuration.yaml
Find the homeassistant: block's packages: key (the TOU package is loaded there). Add:
packages: uses !include_dir_named or a different form, match the existing TOU entry's style — the TOU package is configuration_tou_addition.yaml; mirror exactly how it is included.) - [ ] Step 3: Allowlist the two new entities
Append to tests/fixtures/entities_allow.txt:
# Shared ambiance schedule (configuration_ambiance_addition.yaml) — registered after the
# next config deploy. Move to entities.txt via `make snapshot` post-deploy.
sensor.ambiance_scene
binary_sensor.weather_is_severe
- [ ] Step 4: Validate schema
Run: make lint Expected: PASS.
Run: make check-config Expected: PASS — package loads, both template entities parse. (If Docker down, note and rely on CI.)
- [ ] Step 5: Commit
git add configuration_ambiance_addition.yaml configuration.yaml tests/fixtures/entities_allow.txt
git commit -m "feat(lighting): shared ambiance schedule — sensor.ambiance_scene + binary_sensor.weather_is_severe (#office-lamp)"
Task 2: Generalize the Govee scene rest_command¶
Files: - Modify: configuration.yaml (rename/parameterize the rest_command) - Modify: scripts.yaml (corner_lamp_apply_scene passes device)
Interfaces: - Consumes: !secret govee_api_key. - Produces: rest_command.govee_scene (data vars: device, scene_id, param_id).
- [ ] Step 1: Replace the rest_command in
configuration.yaml
Find rest_command: govee_corner_lamp_scene: and replace the whole govee_corner_lamp_scene block with:
# Fire a native Govee scene on ANY account device in one Platform-API call. device is a
# non-secret identifier; only the key is a !secret. Used by both lamp renderers.
govee_scene:
url: https://openapi.api.govee.com/router/api/v1/device/control
method: POST
content_type: "application/json"
headers:
Govee-API-Key: !secret govee_api_key
payload: >-
{"requestId":"ha-govee-scene","payload":{"sku":"{{ sku }}","device":"{{ device }}","capability":{"type":"devices.capabilities.dynamic_scene","instance":"lightScene","value":{"id":{{ scene_id }},"paramId":{{ param_id }}}}}}
sku is now also a variable (H607C vs H600B). Callers pass sku, device, scene_id, param_id. - [ ] Step 2: Update
corner_lamp_apply_sceneinscripts.yaml
In corner_lamp_apply_scene, change the rest_command call to pass sku+device:
- action: rest_command.govee_scene
data:
sku: "H607C"
device: "99:49:E7:99:B2:60:F0:8E"
scene_id: "{{ scene_id }}"
param_id: "{{ param_id }}"
corner_lamp_apply_scene — the dedup guard, power switch, token store — unchanged.) - [ ] Step 3: Validate
Run: make lint → PASS. Run: make check-config → PASS (rest_command schema valid; corner apply still resolves).
- [ ] Step 4: Commit
git add configuration.yaml scripts.yaml
git commit -m "refactor(lighting): generalize rest_command.govee_scene(sku,device,scene_id,param_id) (#office-lamp)"
Task 3: Refactor the corner lamp renderer (remove SOFI, consume shared, new gates)¶
Files: - Modify: scripts.yaml (corner_lamp_render) - Modify: automations.yaml (corner_lamp_render_triggers)
Interfaces: - Consumes: sensor.ambiance_scene, binary_sensor.weather_is_severe, person.louis, media_player.office_apple_tv, sun.sun, script.corner_lamp_apply_scene, script.corner_lamp_off.
- [ ] Step 1: Replace
corner_lamp_render's stack inscripts.yaml
Replace the entire corner_lamp_render sequence: (from - choose: through the circadian default: block) with this stack. The corner_lamp_apply_scene/corner_lamp_off primitives and their calls are unchanged in signature.
sequence:
- choose:
# 0. Manual → yield.
- conditions: "{{ is_state('input_boolean.corner_lamp_manual','on') }}"
sequence:
- action: input_text.set_value
target: { entity_id: input_text.corner_lamp_last_render }
data: { value: "manual" }
- stop: "manual override — renderer yields"
# 1. Alarm → Breathe.
- conditions: "{{ is_state('alarm_control_panel.blairmont_alarm','triggered') }}"
sequence:
- action: script.corner_lamp_apply_scene
data: { scene_id: 9690, param_id: 16565, token: "alert:breathe", force: true }
# 2. Leak → Dripping.
- conditions: "{{ is_state('binary_sensor.any_leak_wet','on') }}"
sequence:
- action: script.corner_lamp_apply_scene
data: { scene_id: 9695, param_id: 16570, token: "alert:dripping", force: true }
# 3. Movie mode: dark + office Apple TV playing/paused → off.
- conditions: >-
{{ is_state('sun.sun','below_horizon')
and states('media_player.office_apple_tv') in ['playing','paused'] }}
sequence:
- action: script.corner_lamp_off
data: { token: "off:movie" }
# 4. Louis away → off.
- conditions: "{{ not is_state('person.louis','home') }}"
sequence:
- action: script.corner_lamp_off
data: { token: "off:away" }
# 5. Asleep → off.
- conditions: "{{ is_state('input_boolean.sleeping','on') }}"
sequence:
- action: script.corner_lamp_off
data: { token: "off:sleep" }
# 6. Severe weather → Thunderstorm.
- conditions: "{{ is_state('binary_sensor.weather_is_severe','on') }}"
sequence:
- action: script.corner_lamp_apply_scene
data: { scene_id: 9705, param_id: 16580, token: "weather:storm" }
# 7. default → shared ambiance scene, mapped to H607C ids.
default:
- variables:
smap:
morning: [9668, 16543]
afternoon: [9669, 16544]
twilight: [9670, 16545]
night: [9709, 16584]
starry_sky: [9707, 16582]
# Fallback guard: sensor.ambiance_scene can be `unknown`/`unavailable` transiently
# (HA-start window, before the template sensor first evaluates). Fall back to `night`
# so smap[k] never raises UndefinedError and aborts the render.
k: "{{ states('sensor.ambiance_scene') if states('sensor.ambiance_scene') in smap else 'night' }}"
- action: script.corner_lamp_apply_scene
data:
scene_id: "{{ smap[k][0] }}"
param_id: "{{ smap[k][1] }}"
token: "ambiance:{{ k }}"
sensor.ambiance_scene. - [ ] Step 2: Update
corner_lamp_render_triggersinautomations.yaml
In the - trigger: state entity_id: list, REMOVE sensor.sofi_change_pct and ADD sensor.ambiance_scene, binary_sensor.weather_is_severe, media_player.office_apple_tv, person.louis. Keep input_boolean.corner_lamp_manual, input_boolean.sleeping, binary_sensor.any_leak_wet, alarm_control_panel.blairmont_alarm. REMOVE zone.home (replaced by person.louis). Keep the sun/time/HA-start triggers; the time_pattern /5 may stay (harmless backstop) or be removed (ambiance changes are driven by sensor.ambiance_scene now) — keep it.
- [ ] Step 3: Live render-test the new default mapping (before relying on it)
Run:
cd /Users/pk/code/homeassistant
T='{% set smap={"morning":[9668,16543],"afternoon":[9669,16544],"twilight":[9670,16545],"night":[9709,16584],"starry_sky":[9707,16582]} %}{% set key=states("sensor.ambiance_scene") %}key={{ key }} in_map={{ key in smap }} id={{ smap.get(key,[0,0])[0] }}'
jq -n --arg t "$T" '{template:$t}' | curl -s -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" -d @- http://homeassistant.local:8123/api/template
key=<something> in_map=True id=<nonzero>. (Note: sensor.ambiance_scene only exists after Task 1 deploys; if it reads unknown, this step is deferred to Task 8's post-deploy verify — mark it done there.) - [ ] Step 4: Validate
Run: make lint → PASS (entity refs resolve; sensor.ambiance_scene/binary_sensor.weather_is_severe via allowlist; person.louis, office_apple_tv, sun in snapshot). Run: make check-config → PASS.
- [ ] Step 5: Commit
git add scripts.yaml automations.yaml
git commit -m "refactor(lighting): corner lamp pure ambiance — drop SOFI, consume shared schedule, movie-mode + person.louis gates (#office-lamp)"
Task 4: Office helpers + apply/off primitives¶
Files: - Modify: configuration.yaml (2 helpers) - Modify: scripts.yaml (office_lamp_apply_scene, office_lamp_off) - Modify: tests/fixtures/entities_allow.txt
Interfaces: - Produces: input_boolean.office_lamp_manual; input_text.office_lamp_last_render; script.office_lamp_apply_scene(scene_id, param_id, token, force); script.office_lamp_off(token).
- [ ] Step 1: Add helpers to
configuration.yaml
Under input_boolean: add:
input_text: add: - [ ] Step 2: Add the primitives to
scripts.yaml
# Office 3-head lamp render primitives (#office-lamp). Fires the SAME scene to all 3 H600B
# bulbs (one govee_scene call each). Dedupe via input_text.office_lamp_last_render.
office_lamp_apply_scene:
alias: "Office Lamp — apply scene"
icon: mdi:ceiling-light-multiple
mode: queued
max: 10
fields:
scene_id: { description: "Govee scene id", example: 3133 }
param_id: { description: "Govee scene paramId", example: 3283 }
token: { description: "Dedupe token", example: "ambiance:morning" }
force: { description: "Bypass dedupe (alerts)", default: false }
sequence:
- if: "{{ (force | default(false)) or states('input_text.office_lamp_last_render') != token }}"
then:
- action: switch.turn_on
target:
entity_id:
- switch.office_lamp_bulb_1_power_switch
- switch.office_lamp_bulb_2_power_switch
- switch.office_lamp_bulb_3_power_switch
continue_on_error: true
- repeat:
for_each:
- "07:F6:3C:DC:75:17:45:E8"
- "D2:06:3C:DC:75:14:FF:8C"
- "8D:97:3C:DC:75:11:63:54"
sequence:
- action: rest_command.govee_scene
data:
sku: "H600B"
device: "{{ repeat.item }}"
scene_id: "{{ scene_id }}"
param_id: "{{ param_id }}"
- action: input_text.set_value
target: { entity_id: input_text.office_lamp_last_render }
data: { value: "{{ token }}" }
office_lamp_off:
alias: "Office Lamp — off"
icon: mdi:ceiling-light-multiple
mode: queued
max: 10
fields:
token: { description: "Dedupe token", example: "off:away" }
sequence:
- if: "{{ states('input_text.office_lamp_last_render') != token }}"
then:
- action: switch.turn_off
target:
entity_id:
- switch.office_lamp_bulb_1_power_switch
- switch.office_lamp_bulb_2_power_switch
- switch.office_lamp_bulb_3_power_switch
continue_on_error: true
- action: input_text.set_value
target: { entity_id: input_text.office_lamp_last_render }
data: { value: "{{ token }}" }
- [ ] Step 3: Allowlist the office entities
Append to tests/fixtures/entities_allow.txt:
# Office lamp renderer helpers + bulb entities — registered after deploy / already live via
# govee2mqtt. Fold into entities.txt via `make snapshot` post-deploy.
input_boolean.office_lamp_manual
input_text.office_lamp_last_render
light.office_lamp_bulb_*/switch.office_lamp_bulb_*_power_switch are already live in the registry; make snapshot at Task 8 folds them into entities.txt. If Tier-3 fails before then because the switches aren't in entities.txt, add the three switch.office_lamp_bulb_{1,2,3}_power_switch here too.) - [ ] Step 4: Validate
Run: make lint → PASS. Run: make check-config → PASS.
- [ ] Step 5: Commit
git add configuration.yaml scripts.yaml tests/fixtures/entities_allow.txt
git commit -m "feat(lighting): office lamp manual/dedupe helpers + apply/off primitives (3-bulb fan-out) (#office-lamp)"
Task 5: Office lamp renderer¶
Files: - Modify: scripts.yaml (office_lamp_render)
Interfaces: - Consumes: sensor.ambiance_scene, binary_sensor.weather_is_severe, person.louis, media_player.office_apple_tv, sun.sun, input_boolean.office_lamp_manual, input_boolean.sleeping, script.office_lamp_apply_scene, script.office_lamp_off. - Produces: script.office_lamp_render.
- [ ] Step 1: Add
office_lamp_rendertoscripts.yaml
Same stack as the refactored corner lamp, mapping to H600B ids, using the office manual flag + office primitives:
# Office 3-head lamp priority renderer (#office-lamp) — pure-ambiance twin of the corner lamp.
# Same stack; H600B scene ids; drives all 3 heads as one. Consumes the shared sensor.ambiance_scene.
office_lamp_render:
alias: "Office Lamp — render"
icon: mdi:ceiling-light-multiple
mode: restart
sequence:
- choose:
- conditions: "{{ is_state('input_boolean.office_lamp_manual','on') }}"
sequence:
- action: input_text.set_value
target: { entity_id: input_text.office_lamp_last_render }
data: { value: "manual" }
- stop: "manual override — renderer yields"
- conditions: "{{ is_state('alarm_control_panel.blairmont_alarm','triggered') }}"
sequence:
- action: script.office_lamp_apply_scene
data: { scene_id: 3128, param_id: 3278, token: "alert:fire", force: true }
- conditions: "{{ is_state('binary_sensor.any_leak_wet','on') }}"
sequence:
- action: script.office_lamp_apply_scene
data: { scene_id: 1176, param_id: 1238, token: "alert:river", force: true }
- conditions: >-
{{ is_state('sun.sun','below_horizon')
and states('media_player.office_apple_tv') in ['playing','paused'] }}
sequence:
- action: script.office_lamp_off
data: { token: "off:movie" }
- conditions: "{{ not is_state('person.louis','home') }}"
sequence:
- action: script.office_lamp_off
data: { token: "off:away" }
- conditions: "{{ is_state('input_boolean.sleeping','on') }}"
sequence:
- action: script.office_lamp_off
data: { token: "off:sleep" }
- conditions: "{{ is_state('binary_sensor.weather_is_severe','on') }}"
sequence:
- action: script.office_lamp_apply_scene
data: { scene_id: 3125, param_id: 3275, token: "weather:storm" }
default:
- variables:
smap:
morning: [3133, 3283]
afternoon: [1197, 1259]
twilight: [1172, 1234]
night: [3130, 3280]
starry_sky: [1174, 1236]
# Fallback guard: sensor.ambiance_scene can be `unknown`/`unavailable` transiently
# (HA-start window). Fall back to `night` so smap[k] never raises UndefinedError.
k: "{{ states('sensor.ambiance_scene') if states('sensor.ambiance_scene') in smap else 'night' }}"
- action: script.office_lamp_apply_scene
data:
scene_id: "{{ smap[k][0] }}"
param_id: "{{ smap[k][1] }}"
token: "ambiance:{{ k }}"
- [ ] Step 2: Validate
Run: make lint → PASS. Run: make check-config → PASS.
- [ ] Step 3: Commit
git add scripts.yaml
git commit -m "feat(lighting): office lamp priority renderer — ambiance twin (H600B) (#office-lamp)"
Task 6: Office triggers, webhooks, wake_house wiring¶
Files: - Modify: automations.yaml (new section: triggers + 2 webhooks) - Modify: scripts.yaml (wake_house)
- [ ] Step 1: Generate two webhook ids
Run: python3 -c "import secrets; print('manual', secrets.token_hex(16)); print('auto', secrets.token_hex(16))" Note as <OFFICE_MANUAL_ID> / <OFFICE_AUTO_ID>.
- [ ] Step 2: Add automations to
automations.yaml(new numbered section, next free header number)
- alias: "Office Lamp — Render Triggers"
id: office_lamp_render_triggers
triggers:
- trigger: state
entity_id:
- input_boolean.office_lamp_manual
- input_boolean.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
- trigger: state
entity_id: sun.sun
- trigger: homeassistant
event: start
actions:
- action: script.office_lamp_render
mode: single
- alias: "Office Lamp — Manual On (webhook)"
id: office_lamp_manual_webhook_on
triggers:
- trigger: webhook
webhook_id: <OFFICE_MANUAL_ID>
allowed_methods: [POST, GET]
local_only: false
actions:
- action: input_boolean.turn_on
target: { entity_id: input_boolean.office_lamp_manual }
mode: single
- alias: "Office Lamp — Manual Off / Auto (webhook)"
id: office_lamp_manual_webhook_off
triggers:
- trigger: webhook
webhook_id: <OFFICE_AUTO_ID>
allowed_methods: [POST, GET]
local_only: false
actions:
- action: input_boolean.turn_off
target: { entity_id: input_boolean.office_lamp_manual }
- action: script.office_lamp_render
mode: single
- [ ] Step 3: Wire
wake_houseinscripts.yaml
After the corner-lamp step (which clears corner_lamp_manual + calls script.corner_lamp_render), add:
- action: input_boolean.turn_off
target: { entity_id: input_boolean.office_lamp_manual }
- action: script.office_lamp_render
- [ ] Step 4: Validate
Run: make lint → PASS (unique automation ids). Run: make check-config → PASS.
- [ ] Step 5: Commit
git add automations.yaml scripts.yaml
git commit -m "feat(lighting): office lamp render triggers + webhooks + wake_house wiring (#office-lamp)"
Task 7: Documentation + reframe #75¶
Files: - Modify: projects/smart_lighting_scenes.md, CLAUDE.md, docs/lighting.md, docs/automations.md
-
[ ] Step 1:
projects/smart_lighting_scenes.md— add a "Shared ambiance schedule + Office lamp" section: thesensor.ambiance_scenemaster, the twin renderers, the "ambiance lamps vs data bulbs" principle. Note the corner lamp's SOFI removal. -
[ ] Step 2:
CLAUDE.md— (a) new automation-section entry for the office lamp renderer; (b) update the corner-lamp §-entry: SOFI removed, now consumessensor.ambiance_scene, movie-mode +person.louisgates; (c) Key Entities: addsensor.ambiance_scene,binary_sensor.weather_is_severe,input_boolean.office_lamp_manual; (d) note the two Sengled bulbs are the TOU+SOFI data pair. -
[ ] Step 3:
docs/lighting.md— GREAT user-facing docs with mermaid diagrams (the headline of this task). mkdocs already supports mermaid (pymdownx.superfencescustom fence inmkdocs.yml;docs/architecture.mduses it — follow that style). Rewrite the Corner Lamp section (now pure ambiance, NO SOFI) and add a new Office Lamp (3-head) section, plus a "How the ambiance lamps work" overview. Include at least these three mermaid diagrams: - Priority-stack flowchart (
flowchart TD): the top-wins decision — manual→yield · alarm→red alert · leak→water alert · movie-mode (dark + office Apple TV)→off · Louis away→off · asleep→off · severe weather→storm · else→time-of-day ambiance. Show it as a decision cascade so a household reader can trace "why is the lamp doing X right now." - Shared-schedule architecture (
flowchart LR):sensor.ambiance_scene(time-of-day) +binary_sensor.weather_is_severefeeding BOTHcorner_lamp_renderandoffice_lamp_render, each mapping to its own Govee SKU scenes. Convey "tune one schedule → both lamps follow." -
The ambiance-vs-data split (
flowchart LRor a small graph): the two nice Govee lamps = ambiance; the two Sengled bulbs = data (TOU + SOFI). Reinforce the guiding principle. Also cover in prose: the time-of-day scene arc (Morning→Afternoon→Twilight→Night→Starry Sky), that weather only shows on genuine storms (severe-only, mild weather ignored), the alert scenes, the movie-mode + Louis-away + wake/sleep gates, and how to take manual control (dashboard toggle / webhook / Govee app) with the auto-restore at morning wake. Keep the household tone (plain language, admonitions/tables) per the existing manual style. Updatedocs/automations.mdwith the technical priority-stack reference for both renderers + the shared schedule. -
[ ] Step 4: Reframe #75 — comment on GitHub issue #75 that the Sengled §19 SOFI ticker is now the intended sole SOFI display (not to be retired); close it or relabel per the board.
-
[ ] Step 5: Validate + commit
Run: mkdocs build --strict → PASS. Run: make lint → PASS.
git add projects/smart_lighting_scenes.md CLAUDE.md docs/lighting.md docs/automations.md
git commit -m "docs(lighting): office lamp + shared ambiance schedule; corner SOFI removed (#office-lamp)"
Task 8: Deploy + post-deploy verification¶
- [ ] Step 1: Merge to
main+ push
git checkout main && git pull --ff-only && git merge --no-ff feat/office-lamp-ambiance -m "feat(lighting): office lamp + shared ambiance renderer (#office-lamp)"
git push
gh run watch $(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId') --exit-status. Expect test PASS → deploy-ha full restart. ssh haos "ha core logs --lines 20" clean. - [ ] Step 2: Verify the shared sensor + both renderers live (logs + tokens)
ssh haos "ha core logs --lines 150 2>/dev/null | grep -iE 'corner_lamp|office_lamp|ambiance' | grep -iE 'ERROR|WARNING' | tail -10 || echo clean"
curl -s -H "Authorization: Bearer $HA_TOKEN" http://homeassistant.local:8123/api/states/sensor.ambiance_scene | jq -r .state
curl -s -H "Authorization: Bearer $HA_TOKEN" http://homeassistant.local:8123/api/states/input_text.corner_lamp_last_render | jq -r .state
curl -s -H "Authorization: Bearer $HA_TOKEN" http://homeassistant.local:8123/api/states/input_text.office_lamp_last_render | jq -r .state
sensor.ambiance_scene = a logical key; both last_render tokens = ambiance:<key> (or an off/gate token). If a TupleWrapper/undefined error appears, fix per [[ha-template-runtime-gotchas]] and redeploy. - [ ] Step 3: Preview-test the office scene map (H600B alert/weather picks are approximations)
Freeze the office renderer (input_boolean.office_lamp_manual on), fire each candidate scene to all 3 bulbs 10s apart (reuse the corner-lamp preview pattern with the H600B ids + 3 device ids), eyeball morning/twilight/night/storm/alarm/leak, then clear manual. Swap any weak pick in office_lamp_render's smap/alert branches and redeploy that line.
-
[ ] Step 4: Verify corner lamp behavior preserved — off-market,
input_text.corner_lamp_last_rendershould beambiance:<current key>(same scene it showed before the refactor for this time/weather); confirm no SOFI token ever appears again. -
[ ] Step 5:
make snapshot+ commit
tests/fixtures/entities_allow.txt (now in entities.txt); the 12 bulb entities are folded in too. make lint # PASS
git add tests/fixtures/entities.txt tests/fixtures/notify_services.txt tests/fixtures/entities_allow.txt
git commit -m "chore(snapshot): ambiance + office lamp entities into snapshot (#office-lamp)" && git push
- [ ] Step 6: Live spot-check — toggle
office_lamp_manual(yields all 3 / resumes); fire the office webhooks; confirm movie-mode (dark + office ATV playing → off) and person.louis-away → off; confirm both lamps show the same ambiance key off-market. Reframe/close #75.
Self-Review¶
Spec coverage: shared schedule sensor+binary (Task 1) ✓ · generalized rest_command (Task 2) ✓ · corner refactor: drop SOFI, consume shared, person.louis, movie-mode (Task 3) ✓ · office helpers+primitives 3-bulb fan-out (Task 4) ✓ · office renderer twin stack + H600B map (Task 5) ✓ · triggers+webhooks+wake_house (Task 6) ✓ · docs + #75 reframe (Task 7) ✓ · deploy, live render-test, preview-tune, snapshot (Task 8) ✓. "Ambiance vs data" principle, alerts-stay, no-SOFI, unified stack, TOU-untouched — all reflected.
Placeholder scan: the only <...> tokens (<OFFICE_MANUAL_ID>/<OFFICE_AUTO_ID>) are generated in Task 6 Step 1. H600B weather/alert scene ids are concrete defaults flagged for preview-tuning (Task 8 Step 3), not TBDs. Task 1 Step 2 says "mirror the TOU package include style" — a real instruction against the existing file, not a placeholder.
Type/name consistency: rest_command.govee_scene vars sku/device/scene_id/param_id match between Task 2 (definition), Task 2 (corner call), and Task 4 (office fan-out). sensor.ambiance_scene / binary_sensor.weather_is_severe names consistent Tasks 1/3/5/6. Logical keys identical between the sensor (Task 1) and both smaps (Tasks 3/5). office_lamp_apply_scene(scene_id,param_id,token,force) / office_lamp_off(token) defined Task 4, called Task 5 with those exact fields.