HA lights: record the entity-id trap, the recipes, and the day I broke working scripts

A verified-working Home Assistant light alert system (leak/alarm/pump/doorbell on
five Tapo bulbs, for a deaf user) was reduced to a single requested tweak — pump
lights the Death Star only. Instead of making it, the render script was rewritten
twice, the alert path broke, and the session was spent there. The creator had to
stop it. llamacasty did not happen today.

Root cause of the "nothing lights" saga, found only after three failed remedies:
the YAML key is `alerts_render` but the entity id is `script.alerts_render_lights`.
Every caller invoked `script.alerts_render`, which does not exist — and
`script.turn_on` on a non-existent entity returns HTTP 200 and silently does
nothing. One entity-list check would have found it; three guesses did not.

MyMistakes.md gains, in the same commit:
- the failure log for the day and the rules it produces (do the one change asked
  for; never trust a 200 from script.turn_on; a second failed remedy means verify
  the plumbing, not the logic; read state inside the window the code defines);
- the H500 discovery-poisoning fix (delete the entry, re-add with a pinned host);
- the measured Tapo recall numbers that rule out flashing (500ms colour, 5s
  brightness) and the off-state attribute-drop trap;
- the alert system's architecture: one writer, flat if/else (nested if and choose
  both silently lit nothing), pump as status keyed off consumption not the
  latched switch.

HANDOFF.md rewritten: the HA system's verified state and its landmines, the still
unstarted vv webcam plan, and the correction that CREDENTIALS.md is stale for HA
(it says .144; the real box is .145).
This commit is contained in:
2026-10-20 22:27:06 -07:00
parent d1e73a3aad
commit fb851f089d
2 changed files with 194 additions and 104 deletions
+84
View File
@@ -1751,3 +1751,87 @@ There is none, and there is no battery:
blaming a root KSK rollover.
- Remote-survey hygiene: pipe `pf.py --script` output to a file (`> /tmp/opencode/out.txt`) and read the
file — piping to `head` silently cut a survey in half mid-section and hid the very answer being sought.
## 2026-10-21 — I broke a working HA light system, then burned the session, and the one-line cause was an entity id
**What happened.** The Home Assistant light alert system (leak / alarm / pump / doorbell on five Tapo
bulbs, for a deaf user who needs lights, not sounds) was **verified working** — leak gave warm-white
wake bulbs + purple, alarm gave red, clear restored pump status. The creator then asked for **one**
change: the water pump should light the Death Star only. Instead of making that one change, I rewrote
the render script twice (nested `if/then/else`, then `choose:`), broke the alert path, and spent the
rest of the session on it. The day's planned work (llamacasty / the vv 9:16 webcam Unit 1) did not
happen. The creator's read — *"you took something that was working just fine and fucked it up"* — was
exactly correct.
**The actual root cause, found only after three failed remedies.** The script's YAML key is
`alerts_render`, but its **entity id was `script.alerts_render_lights`** (a leftover `unique_id`). Every
caller — `script.alerts_clear` and both render automations — called `script.alerts_render`, which **does
not exist**. `script.turn_on` on a non-existent entity returns **HTTP 200 and silently does nothing**.
So clear never re-rendered and alerts froze on the last colour painted. One `grep` of the real entity
list would have found it; I instead spent the session theorising about templates, races and `choose:`
semantics.
**Rules this produces.**
- **Do the one change asked for.** When a system is verified working, a requested tweak is a *tweak*.
Rewriting the surrounding structure is how working things become broken things.
- **NEVER trust `script.turn_on` returning 200.** It is a silent no-op on a bad entity id — no error, no
log line. Before blaming logic, confirm the entity exists: compare the YAML key against the real
entity id (`/api/states`, or the entity registry `unique_id`). A YAML key and the derived entity id
can differ whenever a `unique_id` predates a rename.
- **A second failed remedy for the same symptom = STOP and verify the plumbing, not the logic.** The
diff between "logic wrong" and "call going nowhere" is one status/entity check, and I skipped it
three times.
- **Test reads need the right window.** The doorbell's green lasted 8s; I polled at 9s, saw dark, and
called it broken. Read the state *inside* the window the code defines.
## HA TAPO TP-LINK: A HUB ENTRY POISONED BY DISCOVERY IS FIXED BY RE-ADDING WITH A PINNED HOST (RECIPE)
**Symptom:** a Tapo hub (H500) shows its entities as unavailable or `-100001`, and the config entry's
`host` has been silently rewritten to the IP of a **camera** on the same LAN. `.storage/core.config_entries`
shows the hub entry pointing at the camera's address while `alias`/`model` also got mangled.
**Cause:** HA's `tplink` discovery rewrites the entry host to whatever answers on the LAN, so a camera
that shares the hub's MAC (Tapo uses one MAC across its wifi/ethernet interfaces — `ip neigh` showing the
hub MAC against several IPs is a **red herring**) wins. Upstream: core#178994 (H200 host flips to camera
IPs), "closed" only by a python-kasa bump that does not fix it.
**Recipe (works — verified):**
1. Note the hub's real IP (the one with the hub's TLS cert, no RTSP open) and its MAC.
2. Delete the poisoned entry: `POST /api/config/config_entries/entry/{entry_id}/remove`.
3. Re-add through the config flow **specifying the host explicitly** so discovery never probes the camera:
- `POST /api/config/config_entries/flow {"handler":"tplink","show_advanced_options":true}`
- `user` step: `{"host":"<hub-ip>"}`
- `user_auth_confirm` step: `{"username":…,"password":…}` (Tapo account; `-40404` + `sec_left` =
account lockout, wait it out)
- returns `create_entry` with a **new** entry id and `host` pinned to the literal IP.
4. Verify the new entry is `loaded` and the alarm sensors (Tapo base bores into HA via the hub) come back.
**Two traps:** (a) re-POSTing a removed alarm sensor with `enabled:false` **re-creates** it — remove once,
don't touch; (b) if the host re-poisons after a restart, temporarily disable the offending camera entry to
starve discovery.
## HA: RECALL MEASUREMENTS THAT RULE OUT FLASHING TAPO BULBS (RECIPE)
Tapo bulbs cannot be flashed usefully. Measured on `light.officeone` (2026-10-20): API round-trip ~130ms,
**colour change ~500ms**, **brightness change ~5s** (255→25 consistently). So:
- **Only steady states.** A "flash" is a colour change with extra steps; stacking commands mid-ramp is
what leaves a bulb stuck on an old colour (the creator's green-at-100% bulbs came from exactly that).
- If a flash is ever truly needed, colour-only, ≥1000ms per state. Never pulse brightness.
- HA drops `rgb_color`/`brightness` from a light's state when it is off (core#2316, closed as
"not a bug"), so an event must end by **explicitly turning off** bulbs that were off — otherwise the
bulb keeps its internal colour memory and comes back the wrong colour.
## HA: THE ALERT SYSTEM'S ARCHITECTURE (RECIPE — the shape that works, do not "improve" it)
Producer/consumer split, and **one single writer** of the bulbs:
- `script.alerts_arm` / `alerts_clear` only write `input_text.light_alert` (`none|leak|alarm|motion`),
which survives a restart.
- `script.alerts_render_lights` is the **only** thing that touches the bulbs. Priority: an alert always
beats pump status. Flat top-level `if`/`else` blocks — a nested `if` or a `choose:` both silently lit
**nothing** in this configuration, so the flat shape is deliberate.
- Pump is **status**, not an alert: auto-clearing, Death Star only (creator directive), keyed off
`sensor.water_pump_current_consumption`, **never** off `switch.water_pump_2` — that switch was observed
latched `on` for hours at 0.0A draw, and using its state painted the bedroom blue at 5am.
- Alerts **hold** until a human clears them; a sensor returning to normal notifies but does not clear.
- Colours: leak purple, alarm red, motion orange (reserved), pump blue, doorbell green (doorbell is not
an alert — it guards on `light_alert != 'none'` and hands back to the render).