Skip to content

Corner Lamp Priority 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 light.corner_lamp (Govee H607C, office) a single priority renderer that composes a circadian native-scene arc, a SOFI stock ticker, and non-red animated alert scenes — all via one Govee Platform API call — with a manual-override escape hatch.

Architecture: One choose:-stack script (script.corner_lamp_render) owns the lamp, mirroring script.string_lights_render. It delegates the send to two primitive scripts (corner_lamp_apply_scene, corner_lamp_off) that dedupe against input_text.corner_lamp_last_render and set power via govee2mqtt's switch.corner_lamp_power_switch. Scenes are fired by a rest_command posting {id, paramId} straight to /router/api/v1/device/control. A trigger automation re-renders on every governing signal; two webhooks + a wake_house edit drive the manual flag.

Tech Stack: Home Assistant YAML (configuration.yaml, scripts.yaml, automations.yaml), rest_command, Govee Platform API, the repo's tiered test suite (make lint / make check-config / make pytest).

Global Constraints

  • Everything visible is an animated Govee scene — never a solid color. Only non-scene states are off (away/asleep) and yield (manual).
  • Clock is ET (America/New_York); market hours = Mon–Fri 09:30–16:00.
  • One secret: govee_api_key in secrets.yaml (first !secret this config uses). The H607C device id is inlined in configuration.yaml (non-credential identifier), not a secret.
  • Deploy gate: configuration.yaml, scripts.yaml, automations.yaml all change → CI does a full ha core restart. Real govee_api_key must be in /config/secrets.yaml on the box before the deploy or the health check fails.
  • Verified scene ids (H607C, 2026-07-10), id / paramId: Strike 9693/16568, Thunderstorm 9705/16580, Lava 9645/16520, Fire 9647/16522, Sunset Glow 9649/16524, Ocean 9635/16510, Rustling leaves 9656/16531, Forest 9634/16509, Green Reign 29882/41597, Sunrise 9558/16433, Morning 9668/16543, Afternoon 9669/16544, Sunset 9631/16506, Starry Sky 9707/16582, Candlelight 9666/16541.
  • Branch: feat/corner-lamp-renderer (spec already committed). Tracking issue #74 — move it to In Progress on the Blairmont Smart Home board when execution starts.
  • TDD note for YAML: there is no unit-test harness for HA config; each task's "test" is make lint (yamllint + structural + entity-ref pytest) and, where the file is container-validatable, make check-config (needs Docker). Runtime behavior is verified live at deploy (Task 8).

File Structure

  • configuration.yaml — add rest_command.govee_corner_lamp_scene, input_boolean.corner_lamp_manual, input_text.corner_lamp_last_render.
  • scripts.yaml — add corner_lamp_apply_scene, corner_lamp_off, corner_lamp_render; edit wake_house; delete 10 old corner_lamp_* scripts.
  • automations.yaml — add corner_lamp_render_triggers, corner_lamp_manual_webhook_on, corner_lamp_manual_webhook_off.
  • secrets.yaml.template, tests/fixtures/ci_secrets.yaml — add govee_api_key.
  • tests/fixtures/entities_allow.txt — allowlist the 2 new helpers (pre-snapshot).
  • Docs: projects/govee_floor_lamp.md, projects/smart_lighting_scenes.md, CLAUDE.md, docs/lighting.md, docs/automations.md.

Task 1: Secrets plumbing + config scaffolding (rest_command + helpers)

Files: - Modify: tests/fixtures/ci_secrets.yaml - Modify: secrets.yaml.template - Modify: configuration.yaml (add rest_command:, extend input_boolean:, extend input_text:) - Modify: tests/fixtures/entities_allow.txt

Interfaces: - Produces: rest_command.govee_corner_lamp_scene (data vars: scene_id, param_id); input_boolean.corner_lamp_manual; input_text.corner_lamp_last_render.

  • [ ] Step 1: Fetch the real H607C device id to inline

Run (prints only the device id — safe, it is not a credential):

ssh haos 'KEY=$(ha addons info b9845f46_govee2mqtt --raw-json | jq -r .data.options.govee_api_key); \
  curl -s -H "Govee-API-Key: $KEY" https://openapi.api.govee.com/router/api/v1/user/devices \
  | jq -r ".data[] | select(.sku==\"H607C\") | .device"'
Expected: one line like AB:CD:12:34:...:H607C device id. Note it — call it <DEVICE_ID> below.

  • [ ] Step 2: Add the API-key stub to the CI secrets fixture

Append to tests/fixtures/ci_secrets.yaml:

govee_api_key: "fake_govee_api_key"

  • [ ] Step 3: Document the secret in the template

Append to secrets.yaml.template:

# Govee Platform API key (Govee Home app → Settings → Apply for API Key).
# Used by rest_command.govee_corner_lamp_scene to fire native H607C scenes.
govee_api_key: "YOUR_GOVEE_API_KEY"

  • [ ] Step 4: Add the rest_command to configuration.yaml

Add a new top-level block (place near other integration blocks). Replace <DEVICE_ID> with the value from Step 1:

# Fire a native Govee scene on the Corner Lamp (H607C) in ONE Platform-API call.
# Native scenes are reachable on the API key alone (not blocked by govee2mqtt #649,
# which only gates real-time push / tap-to-run). The device id is a non-secret
# identifier (useless without the key); only the key is a !secret. See
# docs/superpowers/specs/2026-07-10-corner-lamp-renderer-design.md.
rest_command:
  govee_corner_lamp_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-corner-lamp","payload":{"sku":"H607C","device":"<DEVICE_ID>","capability":{"type":"devices.capabilities.dynamic_scene","instance":"lightScene","value":{"id":{{ scene_id }},"paramId":{{ param_id }}}}}}

  • [ ] Step 5: Add the two helpers

Under the existing input_boolean: block in configuration.yaml, add:

  # Manual override for the Corner Lamp renderer. On = script.corner_lamp_render
  # yields (stop) so you can free-paint the lamp in the Govee app. Cleared by the
  # dashboard toggle, the corner_lamp_auto webhook, or the morning wake (wake_house).
  corner_lamp_manual:
    name: "Corner Lamp Manual"
    icon: mdi:floor-lamp-torchiere
Under the existing input_text: block, add:
  # Dedupe token for the Corner Lamp renderer — the last render it applied
  # (e.g. "circadian:morning", "ticker:lava", "alert:strike", "off:away"). The
  # renderer skips re-sending an unchanged scene so the on-device animation
  # isn't restarted every 5-minute re-eval. Alerts bypass this (force).
  corner_lamp_last_render:
    name: "Corner Lamp Last Render"
    icon: mdi:history
    max: 32

  • [ ] Step 6: Allowlist the two helpers (pre-snapshot)

Append to tests/fixtures/entities_allow.txt:

# Corner Lamp renderer helpers — defined in configuration.yaml, registered after
# the next config deploy. Move to entities.txt via `make snapshot` post-deploy (#74).
input_boolean.corner_lamp_manual
input_text.corner_lamp_last_render

  • [ ] Step 7: Validate

Run: make lint Expected: PASS (yamllint clean; structural + entity-ref pytest pass — the two helpers resolve via the allowlist).

Run: make check-config Expected: PASS — Testing configuration at /config → no errors. (!secret govee_api_key resolves from the stub; the rest_command schema validates.)

  • [ ] Step 8: Commit
git add configuration.yaml secrets.yaml.template tests/fixtures/ci_secrets.yaml tests/fixtures/entities_allow.txt
git commit -m "feat(lighting): corner lamp scene rest_command + manual/dedupe helpers (#74)"

Task 2: Scene-apply + off primitive scripts

Files: - Modify: scripts.yaml (add two scripts)

Interfaces: - Consumes: rest_command.govee_corner_lamp_scene, input_text.corner_lamp_last_render, switch.corner_lamp_power_switch. - Produces: script.corner_lamp_apply_scene (fields: scene = "id,paramId" csv, token string, force bool default false); script.corner_lamp_off (field: token string).

  • [ ] Step 1: Add the primitives to scripts.yaml
# Corner Lamp render primitives (#74). apply_scene = dedupe → power on → fire ONE
# Govee scene → store token. off = dedupe → power off → store token. The renderer
# (script.corner_lamp_render) is the only intended caller. Dedupe skips a no-op
# re-send (avoids restarting the on-device animation every 5-min re-eval); alerts
# pass force:true to always re-fire. See the design spec.
corner_lamp_apply_scene:
  alias: "Corner Lamp  apply scene"
  icon: mdi:floor-lamp-torchiere
  mode: queued
  max: 10
  fields:
    scene:
      description: "Scene as 'id,paramId' (e.g. '9633,16508')."
      example: "9633,16508"
    token:
      description: "Dedupe token stored after a successful apply."
      example: "ticker:forest"
    force:
      description: "Bypass dedupe (alerts always re-fire)."
      default: false
  sequence:
    - if: "{{ force or states('input_text.corner_lamp_last_render') != token }}"
      then:
        - action: switch.turn_on
          target:
            entity_id: switch.corner_lamp_power_switch
          continue_on_error: true
        - action: rest_command.govee_corner_lamp_scene
          data:
            scene_id: "{{ scene.split(',')[0] | trim }}"
            param_id: "{{ scene.split(',')[1] | trim }}"
        - action: input_text.set_value
          target:
            entity_id: input_text.corner_lamp_last_render
          data:
            value: "{{ token }}"

corner_lamp_off:
  alias: "Corner Lamp  off"
  icon: mdi:floor-lamp-torchiere
  mode: queued
  max: 10
  fields:
    token:
      description: "Dedupe token stored after turning off."
      example: "off:away"
  sequence:
    - if: "{{ states('input_text.corner_lamp_last_render') != token }}"
      then:
        - action: switch.turn_off
          target:
            entity_id: switch.corner_lamp_power_switch
          continue_on_error: true
        - action: input_text.set_value
          target:
            entity_id: input_text.corner_lamp_last_render
          data:
            value: "{{ token }}"
  • [ ] Step 2: Validate

Run: make lint Expected: PASS.

Run: make check-config Expected: PASS (scripts parse; corner_lamp_apply_scene/corner_lamp_off register).

  • [ ] Step 3: Commit
git add scripts.yaml
git commit -m "feat(lighting): corner lamp apply-scene + off render primitives (#74)"

Task 3: The priority-stack renderer

Files: - Modify: scripts.yaml (add corner_lamp_render)

Interfaces: - Consumes: script.corner_lamp_apply_scene, script.corner_lamp_off, input_boolean.corner_lamp_manual, alarm_control_panel.blairmont_alarm, binary_sensor.any_leak_wet, zone.home, input_boolean.sleeping, sensor.sofi_change_pct, sensor.sofi_quote. - Produces: script.corner_lamp_render (no fields).

  • [ ] Step 1: Add the renderer to scripts.yaml
# Corner Lamp priority renderer (#74) — owns light.corner_lamp via native Govee
# scenes. Same choose-stack pattern as script.night_lights_render. Priority (top
# wins): manual→yield · alarm→Strike · leak→Thunderstorm · away→off · asleep→off ·
# market+fresh SOFI→ticker · else→circadian. Everything is an ANIMATED scene (never
# a solid color) except off/yield. Re-render triggers: automation
# corner_lamp_render_triggers. Full design + scene-id table: the design spec.
corner_lamp_render:
  alias: "Corner Lamp  render"
  icon: mdi:floor-lamp-torchiere
  mode: restart
  sequence:
    - choose:
        # 0. Manual override → yield. You own the lamp via the Govee app until the
        #    flag clears (dashboard toggle / corner_lamp_auto webhook / morning wake).
        - conditions: "{{ is_state('input_boolean.corner_lamp_manual','on') }}"
          sequence:
            - stop: "manual override  renderer yields"
        # 1. Alarm triggered → Strike (electric lightning). force: always re-fire.
        - conditions: "{{ is_state('alarm_control_panel.blairmont_alarm','triggered') }}"
          sequence:
            - action: script.corner_lamp_apply_scene
              data:
                scene: "9693,16568"
                token: "alert:strike"
                force: true
        # 2. Leak wet → Thunderstorm (water-themed, distinct from alarm). force.
        - conditions: "{{ is_state('binary_sensor.any_leak_wet','on') }}"
          sequence:
            - action: script.corner_lamp_apply_scene
              data:
                scene: "9705,16580"
                token: "alert:thunderstorm"
                force: true
        # 3. Away (nobody home) → off. Below alerts on purpose.
        - conditions: "{{ states('zone.home') | int(0) == 0 }}"
          sequence:
            - action: script.corner_lamp_off
              data:
                token: "off:away"
        # 4. Asleep → off.
        - conditions: "{{ is_state('input_boolean.sleeping','on') }}"
          sequence:
            - action: script.corner_lamp_off
              data:
                token: "off:sleep"
        # 5. Market hours (Mon–Fri 09:30–16:00 ET) → SOFI ticker. Freshness-gated:
        #    stale/missing quote → neutral Ocean (guards the #47 stuck-value failure).
        - conditions: >-
            {{ now().weekday() < 5
               and '09:30' <= now().strftime('%H:%M') < '16:00' }}
          sequence:
            - variables:
                fresh: >-
                  {{ has_value('sensor.sofi_change_pct')
                     and states.sensor.sofi_quote is not none
                     and (now() - states.sensor.sofi_quote.last_updated).total_seconds() < 1200 }}
                pct: "{{ states('sensor.sofi_change_pct') | float(0) }}"
            - choose:
                - conditions: "{{ not fresh }}"
                  sequence:
                    - action: logbook.log
                      data:
                        name: "Corner Lamp"
                        message: "SOFI quote stale/missing  showing neutral Ocean."
                    - action: script.corner_lamp_apply_scene
                      data:
                        scene: "9635,16510"
                        token: "ticker:stale"
              default:
                - variables:
                    tier: >-
                      {% set a = pct | abs %}
                      {% if pct <= -4 %}lava
                      {% elif pct <= -2 %}fire
                      {% elif pct <= -0.5 %}sunset_glow
                      {% elif a < 0.5 %}ocean
                      {% elif pct < 2 %}rustling_leaves
                      {% elif pct < 4 %}forest
                      {% else %}green_reign{% endif %}
                    tmap:
                      lava: "9645,16520"
                      fire: "9647,16522"
                      sunset_glow: "9649,16524"
                      ocean: "9635,16510"
                      rustling_leaves: "9656,16531"
                      forest: "9634,16509"
                      green_reign: "29882,41597"
                - action: script.corner_lamp_apply_scene
                  data:
                    scene: "{{ tmap[tier | trim] }}"
                    token: "ticker:{{ tier | trim }}"
      # 6. default → circadian scene by ET time block (weekends + weekday off-hours).
      default:
        - variables:
            block: >-
              {% set h = now().hour %}
              {% if 5 <= h < 8 %}sunrise
              {% elif 8 <= h < 12 %}morning
              {% elif 12 <= h < 17 %}afternoon
              {% elif 17 <= h < 20 %}sunset
              {% elif 20 <= h < 23 %}starry_sky
              {% else %}candlelight{% endif %}
            cmap:
              sunrise: "9558,16433"
              morning: "9668,16543"
              afternoon: "9669,16544"
              sunset: "9631,16506"
              starry_sky: "9707,16582"
              candlelight: "9666,16541"
        - action: script.corner_lamp_apply_scene
          data:
            scene: "{{ cmap[block | trim] }}"
            token: "circadian:{{ block | trim }}"
  • [ ] Step 2: Validate

Run: make lint Expected: PASS (entity-ref: input_boolean.corner_lamp_manual resolves via allowlist; sensor.sofi_change_pct, alarm/leak/zone/sleeping already in the snapshot).

Run: make check-config Expected: PASS.

  • [ ] Step 3: Commit
git add scripts.yaml
git commit -m "feat(lighting): corner lamp priority renderer — circadian + SOFI ticker + alerts (#74)"

Task 4: Trigger automation + manual webhooks

Files: - Modify: automations.yaml (add three automations, in a new numbered section)

Interfaces: - Consumes: script.corner_lamp_render, input_boolean.corner_lamp_manual. - Produces: automations corner_lamp_render_triggers, corner_lamp_manual_webhook_on, corner_lamp_manual_webhook_off.

  • [ ] Step 1: Generate two random webhook ids

Run:

python3 -c "import secrets; print('manual', secrets.token_hex(16)); print('auto', secrets.token_hex(16))"
Note the two hex strings as <MANUAL_WEBHOOK_ID> and <AUTO_WEBHOOK_ID>.

  • [ ] Step 2: Add the automations to automations.yaml

Append near the other renderer trigger automations (after the string-lights section). Replace the two webhook-id placeholders:

##############################################################################
# 24. Corner Lamp Renderer (#74)
##############################################################################

# Re-render the Corner Lamp on every governing signal. script.corner_lamp_render
# owns the priority stack; this just calls it. /5-min tick refreshes the market
# ticker; time triggers mark the circadian block boundaries.
- alias: "Corner Lamp  Render Triggers"
  id: corner_lamp_render_triggers
  triggers:
    - trigger: state
      entity_id:
        - input_boolean.corner_lamp_manual
        - input_boolean.sleeping
        - binary_sensor.any_leak_wet
        - alarm_control_panel.blairmont_alarm
        - zone.home
        - sensor.sofi_change_pct
    - trigger: sun
      event: sunset
    - trigger: sun
      event: sunrise
    - trigger: time
      at:
        - "05:00:00"
        - "08:00:00"
        - "12:00:00"
        - "17:00:00"
        - "20:00:00"
        - "23:00:00"
        - "09:30:00"
        - "16:00:00"
    - trigger: time_pattern
      minutes: "/5"
    - trigger: homeassistant
      event: start
  actions:
    - action: script.corner_lamp_render
  mode: single

# Manual override ON — renderer yields so you can free-paint in the Govee app.
- alias: "Corner Lamp  Manual On (webhook)"
  id: corner_lamp_manual_webhook_on
  triggers:
    - trigger: webhook
      webhook_id: <MANUAL_WEBHOOK_ID>
      allowed_methods:
        - POST
        - GET
      local_only: false
  actions:
    - action: input_boolean.turn_on
      target:
        entity_id: input_boolean.corner_lamp_manual
  mode: single

# Manual override OFF — hand the lamp back to the renderer.
- alias: "Corner Lamp  Manual Off / Auto (webhook)"
  id: corner_lamp_manual_webhook_off
  triggers:
    - trigger: webhook
      webhook_id: <AUTO_WEBHOOK_ID>
      allowed_methods:
        - POST
        - GET
      local_only: false
  actions:
    - action: input_boolean.turn_off
      target:
        entity_id: input_boolean.corner_lamp_manual
    - action: script.corner_lamp_render
  mode: single

  • [ ] Step 3: Validate

Run: make lint Expected: PASS (Tier-2 unique automation id/alias check passes; the three ids are new).

Run: make check-config Expected: PASS.

  • [ ] Step 4: Record the webhook URLs

Note for later wiring (Alexa routine / Home / NFC): https://<nabu-casa-or-local>/api/webhook/<MANUAL_WEBHOOK_ID> (manual on), .../api/webhook/<AUTO_WEBHOOK_ID> (auto/off). These go in docs/lighting.md in Task 7.

  • [ ] Step 5: Commit
git add automations.yaml
git commit -m "feat(lighting): corner lamp render triggers + manual on/off webhooks (#74)"

Task 5: Morning reset in wake_house

Files: - Modify: scripts.yaml (the wake_house corner-lamp block, ~lines 187–205)

Interfaces: - Consumes: input_boolean.corner_lamp_manual, script.corner_lamp_render.

  • [ ] Step 1: Replace the raw corner-lamp turn-on block

In script.wake_house, replace the existing step that does the repeat: count: 3 over switch.corner_lamp_power_switch + light.corner_lamp (the block whose comment mentions "#65 — that was the bug") with:

    # 3. Corner Lamp — hand it to the renderer for the day. Clear any manual override
    #    (guaranteed daily reset), then render (→ circadian/ticker; turns the power
    #    switch on itself). Replaces the old raw 3× power+segment turn-on (#65, #74).
    - action: input_boolean.turn_off
      target:
        entity_id: input_boolean.corner_lamp_manual
    - action: script.corner_lamp_render
Leave the rest of wake_house (TOU bulb, sleeping/error clear, alarm disarm) unchanged.

  • [ ] Step 2: Validate

Run: make lint Expected: PASS.

Run: make check-config Expected: PASS.

  • [ ] Step 3: Commit
git add scripts.yaml
git commit -m "refactor(lighting): wake_house clears corner_lamp_manual + renders (was raw turn-on) (#74)"

Task 6: Teardown — delete the 10 old scene scripts

Files: - Modify: scripts.yaml (delete 10 scripts)

  • [ ] Step 1: Confirm nothing outside scripts.yaml/docs references them

Run:

grep -rn "corner_lamp_cozy\|corner_lamp_sunset\|corner_lamp_fireplace\|corner_lamp_ocean\|corner_lamp_aurora\|corner_lamp_cyberpunk\|corner_lamp_lavender\|corner_lamp_focus\|corner_lamp_rainbow\|corner_lamp_gradient" \
  automations.yaml lovelace/ *.yaml 2>/dev/null
Expected: no matches (refs live only in scripts.yaml definitions + docs, which Task 7 rewrites). If a dashboard match appears, remove that tile in this task too.

  • [ ] Step 2: Delete the scripts

Remove these top-level keys and their bodies from scripts.yaml: corner_lamp_cozy, corner_lamp_sunset, corner_lamp_fireplace, corner_lamp_ocean, corner_lamp_aurora, corner_lamp_cyberpunk, corner_lamp_lavender, corner_lamp_focus, corner_lamp_rainbow, corner_lamp_gradient. Keep movie_mode. Also remove the block comment header that introduced the Govee gradient scenes.

  • [ ] Step 3: Validate

Run: make lint Expected: PASS (nothing references the removed scripts via captured patterns).

Run: make check-config Expected: PASS.

  • [ ] Step 4: Commit
git add scripts.yaml
git commit -m "refactor(lighting): remove 10 corner_lamp gradient scripts — native scenes replace them (#74)"

Task 7: Documentation

Files: - Modify: projects/govee_floor_lamp.md, projects/smart_lighting_scenes.md, CLAUDE.md, docs/lighting.md, docs/automations.md

  • [ ] Step 1: Correct projects/govee_floor_lamp.md

Fix the false claim that native scenes are impossible/blocked by #649. Replace the "Not done — and not doable by us" paragraph and the §B1 stopgap note with: native dynamic_scene/lightScene scenes ARE reachable on the API key alone (one /device/control call, verified 2026-07-10); #649 only blocks real-time push + tap-to-run. Note the renderer (script.corner_lamp_render) now drives the lamp via rest_command.govee_corner_lamp_scene, and the 10 throttled gradient scripts are retired. Cross-link the design spec and issue #74.

  • [ ] Step 2: Rewrite the Corner Lamp section of projects/smart_lighting_scenes.md

Replace the "Corner Lamp (built)" scene-scripts description with: a priority renderer (circadian arc / SOFI ticker / non-red alerts) via native scenes; manual override (input_boolean.corner_lamp_manual + two webhooks + morning reset); dedupe via input_text.corner_lamp_last_render. Move the old gradient-scene notes to a "superseded" line.

  • [ ] Step 3: Update CLAUDE.md

  • Architecture/automations: add a §24 "Corner Lamp Renderer" entry describing the priority stack and the single-call scene REST path.

  • Alexa Voice Exposure: drop the 8 corner-lamp scenes from the table and change the count 20 → 12 (movie_mode stays; the exposed set removed: corner_lamp_cozy/gradient/sunset/fireplace/ocean/aurora/cyberpunk/lavender).
  • Key Entities: add input_boolean.corner_lamp_manual.
  • Govee integration note: native scenes now used via rest_command; correct the "blocked" language.
  • Fix the scripts.yaml-referenced claim that animated scenes are blocked (in the govee note).

  • [ ] Step 4: Update user manual docs/lighting.md

Add a Corner Lamp section: what it shows through the day (circadian), during market hours (SOFI ticker — green up / red down / blue flat), and on alerts (dramatic non-red). How to take manual control (dashboard toggle or the manual webhook) then pick colors in the Govee app, and that it returns to auto via the auto webhook or automatically at the morning wake. Include the two webhook URLs from Task 4 Step 4.

  • [ ] Step 5: Update docs/automations.md

Add a technical section for the §24 Corner Lamp renderer: the priority stack table, the ticker tiers, the circadian blocks, the trigger list, and the manual/webhook flow.

  • [ ] Step 6: Validate docs build

Run: mkdocs build --strict Expected: PASS — no broken internal links / missing-nav warnings (warnings are fatal under --strict). Fix any cross-page anchor that doesn't resolve.

  • [ ] Step 7: Commit
git add projects/govee_floor_lamp.md projects/smart_lighting_scenes.md CLAUDE.md docs/lighting.md docs/automations.md
git commit -m "docs(lighting): document corner lamp renderer; correct native-scenes-blocked claim (#74)"

Task 8: Deploy + post-deploy verification

Files: - Modify (post-deploy): tests/fixtures/entities.txt, tests/fixtures/entities_allow.txt

  • [ ] Step 1: Put the real API key on the box (pre-deploy, required)

Run (paste the real key when prompted; do not echo it into history):

ssh haos 'grep -q "^govee_api_key:" /config/secrets.yaml || echo "govee_api_key: \"REPLACE_ME\"" >> /config/secrets.yaml; \
  grep "^govee_api_key:" /config/secrets.yaml'
Then edit /config/secrets.yaml on the box to the real key (e.g. ssh haos vi /config/secrets.yaml). Verify it matches the add-on key:
ssh haos 'diff <(ha addons info b9845f46_govee2mqtt --raw-json | jq -r .data.options.govee_api_key) <(grep "^govee_api_key:" /config/secrets.yaml | sed -E "s/.*\"(.*)\".*/\1/") && echo MATCH'
Expected: MATCH.

  • [ ] Step 2: Merge to main and push (triggers CI)

git checkout main && git merge --no-ff feat/corner-lamp-renderer -m "feat(lighting): corner lamp priority renderer (#74)"
git push
gh run list --workflow=ci.yml --limit 1
Expected: CI test job passes, then deploy-ha runs a full ha core restart. Confirm green:
gh run watch $(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')
ssh haos "ha core logs --lines 20"
Expected: HA back up, no config errors.

  • [ ] Step 3: Preview-test the alert scenes on the real lamp

With the lamp reachable, fire each alert scene and eyeball it (reuse the tonight pattern):

ssh haos 'KEY=$(ha addons info b9845f46_govee2mqtt --raw-json | jq -r .data.options.govee_api_key); \
  DEV=$(curl -s -H "Govee-API-Key: $KEY" https://openapi.api.govee.com/router/api/v1/user/devices | jq -r ".data[]|select(.sku==\"H607C\").device"); \
  for V in "9693,16568" "9705,16580"; do \
    curl -s -X POST -H "Govee-API-Key: $KEY" -H "Content-Type: application/json" \
    -d "{\"requestId\":\"t\",\"payload\":{\"sku\":\"H607C\",\"device\":\"$DEV\",\"capability\":{\"type\":\"devices.capabilities.dynamic_scene\",\"instance\":\"lightScene\",\"value\":{\"id\":${V%,*},\"paramId\":${V#*,}}}}}" \
    https://openapi.api.govee.com/router/api/v1/device/control | jq -r .msg; sleep 10; done'
Expected: success twice; Strike reads as an alarm (electric, dramatic, non-red) and Thunderstorm as a distinct water/storm look. If either underwhelms, swap the id/paramId in script.corner_lamp_render (backups: Fireworks 9665/16540, Dance Party 15766/25596) and re-deploy that one line.

  • [ ] Step 4: Trim Alexa exposure

Remove the 8 corner-lamp scenes from the allowlist and re-sync (per CLAUDE.md "Alexa Voice Exposure" — stop HA, jq-edit /config/.storage/homeassistant.exposed_entities to set each script.corner_lamp_* should_expose:false, start, then WS cloud/alexa/sync), then re-discover in the Alexa app. Verify:

ssh haos 'jq "[.data.exposed_entities|to_entries[]|select(.value.assistants[\"cloud.alexa\"].should_expose==true)|.key]|length" /config/.storage/homeassistant.exposed_entities'
Expected: the exposed count dropped by 8 (to 12).

  • [ ] Step 5: Refresh the entity snapshot

Now that the helpers are registered and the old scripts are gone:

make snapshot
Then remove the two helper lines (and their comment) from tests/fixtures/entities_allow.txt, since make snapshot now lists them in entities.txt.

  • [ ] Step 6: Validate + commit the snapshot

Run: make lint Expected: PASS (helpers now resolve from entities.txt; the 10 deleted scripts are pruned from the snapshot).

git add tests/fixtures/entities.txt tests/fixtures/entities_allow.txt
git commit -m "chore(snapshot): corner lamp helpers in, retired gradient scripts out (#74)"
git push
  • [ ] Step 7: Live behavior spot-check

  • Toggle input_boolean.corner_lamp_manual on → confirm the renderer stops touching the lamp; off → confirm it re-renders.

  • Call the manual/auto webhook URLs (Task 4) → confirm the flag flips.
  • Force a render outside market hours → confirm a circadian scene for the current block.
  • (Optional, market hours) watch a /5 tick pick the right ticker tier for the live sensor.sofi_change_pct.
  • Close issue #74 with a note linking the spec + plan.

Self-Review

Spec coverage: circadian arc (Task 3 default branch) ✓ · SOFI ticker tiers + freshness gate (Task 3 branch 5) ✓ · non-red alert scenes (Task 3 branches 1–2) ✓ · manual override + webhooks + morning reset (Tasks 1, 4, 5) ✓ · dedupe/robustness (Task 2) ✓ · one rest_command, scenes-only (Tasks 1–2) ✓ · teardown + Alexa trim (Tasks 6, 8) ✓ · secrets/CI stub (Tasks 1, 8) ✓ · snapshot/allowlist handling (Tasks 1, 8) ✓ · docs incl. corrected #649 claim (Task 7) ✓ · alert-scene preview (Task 8 Step 3) ✓. Spec's second secret (govee_corner_lamp_device) intentionally dropped — device id inlined as a non-credential; noted in Global Constraints.

Placeholder scan: the only <...> tokens (<DEVICE_ID>, <MANUAL_WEBHOOK_ID>, <AUTO_WEBHOOK_ID>) are values the implementer fetches/generates via the exact command given in the same task — not deferred work.

Type/name consistency: corner_lamp_apply_scene(scene, token, force) and corner_lamp_off(token) are defined in Task 2 and called with those exact field names in Tasks 3 and 5; rest_command.govee_corner_lamp_scene data vars scene_id/param_id match between Task 1 (definition) and Task 2 (call); input_text.corner_lamp_last_render and input_boolean.corner_lamp_manual names consistent across Tasks 1–5.