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
+110 -104
View File
@@ -1,22 +1,57 @@
# HANDOFF — 2026-10-17 (PLANNING ONLY session; nothing pushed — push at the next milestone)
# HANDOFF — 2026-10-21 (HA lights session; the day's planned llamacasty work did NOT happen)
## At a glance
- **Branch** main, tracking `origin/main`. **origin/main = `931ab46`; HEAD = the tip commit
"HANDOFF: vv webcam plan, and drop the stale vertical-preview landmine"** — **5 ahead, 0 behind**
(nothing pushed; per the commit/push policy, push only when the creator says so). This file cannot
record its own commit's hash, so the tip is named by subject.
- The five local-only commits: `3cb29ea` (previous session's HANDOFF refresh), `a3a4681`
(per-element composite timing + scene dump), `84cbec2` (FramePumpTests thread-safe log sink),
`cdf82f6` (read the first instrumented take), the tip. Always check
`git log origin/main..HEAD` — never trust a handoff's own "0 ahead" claim.
- Working tree clean after the tip commit.
- **This session produced NO code, NO build, NO test run.** It specified the creator's **vv
(vertical 9:16) webcam feature** end to end; implementation has not started. The locked rules are
below — they are the deliverable, and they must land in `TASKS.md` when work is queued.
The session's later half moved **off-repo** to the officerFriendly/Pi-hole/PS5 "DNS errors" diagnosis —
findings and the pending creator decision are in `Open (operational)` below.
- **Branch** main, tracking `origin/main`. **origin/main = `931ab46`; HEAD = `d1e73a3`** — **6 ahead, 0
behind** (nothing pushed; push only at sub-milestones/milestones, at the creator's word). This file
cannot record its own commit's hash, so the tip is named by subject.
- Local-only commits: `3cb29ea`, `a3a4681`, `84cbec2`, `cdf82f6`, `2a15be2`, `d1e73a3`, plus this
session's. Always check `git log origin/main..HEAD` — never trust a handoff's own "0 ahead" claim.
- **This session produced NO ytLive code, NO build, NO test run.** It was spent — entirely and
wastefully — on the creator's Home Assistant light alert system (off-repo, `192.168.50.145`). See
`MyMistakes.md` → 2026-10-21: the plan was one tweak to a verified-working script; I rewrote the
script twice, broke the alert path, and burned the day. The creator had to say so before I stopped.
- **The HA work landed OK in the end** (recipes + measurements now in `MyMistakes.md`). Records below so
the next session does not re-hunt. **llamacasty / the vv webcam Unit 1 is still NOT started.**
## vv webcam feature — locked rules (creator, 2026-10-17)
## HA light alert system — current, verified state (2026-10-21)
Five Tapo bulbs: `light.br1`, `light.br2`, `light.officeone`, `light.officetwo`,
`light.death_star_light`. Purpose: the creator is **deaf** — lights are the alert channel; the phone
(`notify.mobile_app_mis_lx9`) is secondary. **The iPhone is gone — never notify it.**
| State | br1 / officeone | br2 / officetwo | Death Star |
|---|---|---|---|
| Idle | off | off | off |
| Pump running | off | **off** | **blue only** (creator directive) |
| Leak | warm-white 100% | purple | purple |
| Alarm | warm-white 100% | red | red |
| Doorbell | green, 8s | green, 8s | green, 8s |
| Doorbell during an alert | — | suppressed — | — |
| Clear | off | off | off |
**Architecture:** `script.alerts_arm` / `alerts_clear` write only `input_text.light_alert`
(`none|leak|alarm|motion`, restart-surviving). **`script.alerts_render_lights` is the single writer of
the bulbs** — flat top-level `if`/`else` blocks (nested `if` and `choose:` both silently lit nothing;
do not "improve" the shape). Priority: an alert always beats pump status.
**⛔ THE ENTITY-ID TRAP (cost this session hours):** the YAML key is `alerts_render` but the entity id is
**`script.alerts_render_lights`**. `script.turn_on` on a non-existent entity returns **HTTP 200 and does
nothing** — no error, no log. All callers now use the `_lights` id. Check this first on any "script does
nothing" symptom.
**Pump status** is a *status*, not an alert (auto-clearing), and is keyed off
`sensor.water_pump_current_consumption` — **never** `switch.water_pump_2`, which was observed latched
`on` for hours at 0.0A draw and painted the bedroom blue at 5am.
**HA files touched** (`/config`, root-owned → writes need `sudo`): `scripts.yaml`, `automations.yaml`,
`configuration.yaml` (added `input_text.light_alert` / `_prev`). Backups: `scripts.yaml.bak-alertbuild`,
`automations.yaml.bak-alertbuild`, `automations.yaml.bak-notify-20261020`, `configuration.yaml.bak-tplinkfix`.
Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbell_flash` (old one),
`automation.leak_strobe_notify_clear_restore`, `script.alerts_render_lights_2`.
**Leak sensor is healthy** (`binary_sensor.pump_leak_moisture` = off, battery 100, signal 3) — an earlier
"unavailable" claim was a bad read during a reload; `alert_leak_on` now also triggers on the latched
`sensor.pump_leak_last_water_leak_alert`.
## vv webcam feature — locked rules (creator, 2026-10-17 — unchanged, still awaiting go-ahead)
| mode | shape | size | position |
|---|---|---|---|
| 16:9 | as today | as today | as today |
@@ -24,119 +59,90 @@
| **vv · Chat** | **rect only — no circle option** | normal, or enlarged to fill one area (607×540) | enlarged → top or bottom |
| Starting / BRB / Ending | unchanged | unchanged | unchanged |
- **Placement is set by tap/click-drag with snap, exactly like the socials today**, plus manual entry
in properties. Creator's explicit precedent, not an inferred one.
- **vv keeps its own camera position, size and shape per scene** — creator-confirmed: switching back
to 16:9 restores the desktop layout exactly as it was.
- **No explicit borderless/no-background mode is needed**: round + border/bg options already let the
creator strip both (creator's ruling — this deletes that item from the original request).
- **Placement is set by tap/click-drag with snap, exactly like the socials today**, plus manual entry in
properties. **vv keeps its own camera position, size and shape per scene** — switching back to 16:9
restores the desktop layout exactly.
- **No explicit borderless/no-background mode is needed** (round + border/bg options already allow it).
- Creator's "square" means the rectangular view (the camera's own aspect), not a literal 1:1 box.
## What the code already gives us (established this session — do not re-hunt)
### What the code already gives us (do not re-hunt)
- **vv = the active tier's centred band crop.** 1080p60-vertical: master band **x 656.25–1263.75
(607.5 wide), full height**, upscaled to 1080×1920 (`MainViewModel.Streaming.cs` `ApplyStreamQuality`
→ `OutputRect*`). The band splits into **two 607×540 areas** (top/bottom) — the creator's "two mini
16:9 views", each exactly 50% of the frame.
- **The Live webcam sits at x≈1425 → entirely outside the band.** That is the whole motivation: in vv
the camera is not misplaced, it is invisible.
- **Geometry only — no render-path change.** Preview (`Controls/PreviewPane.xaml` 144–230) and
compositor (`Services/Compositor/SceneCompositor.cs` 389–397, `DrawBorder`) both read the element's
own `X/Y/W/H` + `ClipShape`, so parking the camera needs no compositor or preview work. The
two-tile look *falls out of* the camera covering half the band — the capture element, chat box and
blit path stay untouched.
- **Round / border / background are independent**: `ClipShape.Round`; `HasBorder` = width > 0 AND a
valid 6-hex colour; `BackgroundEffect` = None/Remove/Blur.
- **Per-scene camera size policy already exists**: `MaxWebcamWidthFor/HeightFor` (Chat 1358×764,
others 960×540) + `ClampWebcamToBounds`. **A 607×540 tile sits inside both → the clamp needs no
change.**
- **Storage**: `WebcamSceneConfig` table; new columns go in `MigrateWebcamConfigTable()` as idempotent
`ALTER TABLE … ADD COLUMN` guarded on `PRAGMA table_info` — the established "additive, no version
bump" idiom (`RectWidth/RectHeight`, TASK 51's `BackgroundEffect` landed this way) — plus
`LayoutStore.Save.cs` INSERT and `LayoutStore.Load.cs` SELECT.
- **Existing per-element precedent for remembered geometry**: `RectWidth/RectHeight` +
`ToggleClipShape` (`Models/SceneElement.cs` 124–152) — the pattern for keeping the 16:9 layout
while vv owns its own.
→ `OutputRect*`). The band splits into **two 607×540 areas** (top/bottom).
- **The Live webcam sits at x≈1425 → entirely outside the band** — in vv it is invisible. That is the
whole motivation.
- **Geometry only — no render-path change.** Preview (`Controls/PreviewPane.xaml` 144–230) and compositor
(`Services/Compositor/SceneCompositor.cs` 389–397) both read the element's own `X/Y/W/H` + `ClipShape`.
- **Round / border / background are independent:** `ClipShape.Round`; `HasBorder` = width > 0 AND valid
6-hex colour; `BackgroundEffect` = None/Remove/Blur.
- **Per-scene camera size policy exists**: `MaxWebcamWidthFor/HeightFor` (Chat 1358×764, others 960×540)
+ `ClampWebcamToBounds`. **A 607×540 tile sits inside both → no clamp change.**
- **Storage:** `WebcamSceneConfig` table; new columns via `MigrateWebcamConfigTable()` as idempotent
`ALTER TABLE … ADD COLUMN` guarded on `PRAGMA table_info` + `LayoutStore.Save.cs` INSERT and
`LayoutStore.Load.cs` SELECT.
- **Precedent for remembered geometry:** `RectWidth/RectHeight` + `ToggleClipShape`
(`Models/SceneElement.cs` 124–152).
## Units (each: ONE Good Dog integration test, own commit, memory in the same commit)
1. **vv camera layout** — stored separately per scene, resolved when the tier changes. Live: small
tile (circle or rect) snapped to one of six spots; drag like the socials + properties entry.
### Units (each: ONE Good Dog integration test, own commit, memory in the same commit)
1. **vv camera layout** — stored separately per scene, resolved when the tier changes. Live: small tile
(circle or rect) snapped to one of six spots; drag like the socials + properties entry.
*Test:* with the vertical tier active, dragging the Live camera to top-right places it top-right
inside the band; switching back to 16:9 restores the original layout.
2. **vv · Chat** — rect enforced with the circle option unavailable, plus enlarge-to-one-area
(607×540) snapping top or bottom.
*Test:* in Chat under the vertical tier, enlarge sets the full top area and the circle option is
not offered.
## Corrections made this session
- **Withdrawn landmine (creator-confirmed):** the old line "Vertical-format regression (STALE, not
fixed): 1080p60-vertical selection no longer shrinks the capture border in the preview" was **never
verified**, and it cost this session a pointless question. The creator confirms vv *does* dim the two
bands astride the centre band and that this is expected/best practice. If the original symptom was
something else (the capture element's own border?), it is **unexplained, not known-broken** — do not
re-raise it as a dependency.
- The prev-session content (first instrumented take: judder did not reproduce, no element slow; probe
+ recipe; the four local commits) is unchanged and lives in `ai.md` →
"✅ What the first instrumented take said".
2. **vv · Chat** — rect enforced with the circle option unavailable, plus enlarge-to-one-area (607×540)
snapping top or bottom.
*Test:* in Chat under the vertical tier, enlarge sets the full top area and the circle option is not
offered.
## Next step
- **Implement Unit 1 on the creator's go-ahead.** Nothing is queued in `TASKS.md` — the creator queues
work, and this plan lives here until then. Queue it in `TASKS.md` as part of the first Unit 1 commit.
- **Implement vv Unit 1 on the creator's go-ahead.** Nothing is queued in `TASKS.md` — the creator queues
work; this plan lives here until then. Queue it in `TASKS.md` as part of the first Unit 1 commit.
- **The creator's stated plan for "today" (2026-10-21) was llamacasty. It did not happen.** Confirm the
intended target before starting — do not assume.
## Open (operational)
- **verify.sh's known-failure whitelist is too loose (confirmed twice on 2026-10-16):** it passes when
ANY known failure appears, so a new failure rides along unnoticed — and when the ONLY failure is a
known flake (`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`, which
passes in isolation) it reports "NOT the known pre-existing pair". **Follow-up: require EVERY
failure line to be whitelisted**, and add `MasterLimiter_CapsTheLiveMix_OnThePipe` +
- **verify.sh's known-failure whitelist is too loose** (confirmed twice 2026-10-16): require EVERY failure
line to be whitelisted, and add `MasterLimiter_CapsTheLiveMix_OnThePipe` +
`RealMouseDrag_OnTheLayerList_PersistsTheReorder` to the list.
- **A pre-existing stash is still sitting there:** `stash@{0}` = "WIP on main: 0464593 Compositor:
fast-path opaque 1:1 when source fills canvas" (NOT this session's; do not drop it blindly).
- The judder reproduction take in the FAILING geometry (vertical 1080p60 first) is still outstanding —
**no fix until a take names the stage** (see `ai.md`; the `parts` line decides the latch hypothesis).
fast-path opaque 1:1 when source fills canvas" (NOT this session's; do not drop blindly).
- The judder reproduction take in the FAILING geometry (vertical 1080p60 first) is still outstanding — no
fix until a take names the stage.
- ⛔ Sudo password rotation pending (user; then update both `CREDENTIALS.md` password lines).
- TASK 56 secret scan before `gramps/LlamaCasty` goes public.
- TASK 55 disk hygiene (`startup.log` uncapped — 23MB and the new lines add to it every 5s).
- TASK 55 disk hygiene (`startup.log` uncapped — 23MB and growing every 5s).
## Tests
- Last full run: **428/428 passed, 0 warnings** (2026-10-16, the gate run for `84cbec2`). **No run this
session (planning only).**
session (no ytLive code was touched).**
- Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`,
`MasterLimiter_CapsTheLiveMix_OnThePipe`, `LayerReorderPersistenceTests.RealMouseDrag…`,
`Round_Clip_Corner…`. **Read verify.sh's output — it can pass with an unlisted failure present, or
fail on a known flake (see above).**
`Round_Clip_Corner…`.
## Secrets / paths (do not re-hunt)
- Layout DB: `%APPDATA%\ytLlive\ytLlive.db` (SQLite, full-rewrite per save). Auth:
`%APPDATA%\ytLlive\ytLlive.auth` (DPAPI). Startup log: `%APPDATA%\ytLlive\startup.log` (23MB).
- Recordings land in `C:\Users\gramp\Downloads\recordings\` (`ty-<date>-<time>-<ms>-<seq>.mp4`; a
duplicate name gets a `-2` suffix — the log's path and the file on disk can differ that way).
- **Conversation history (how to recover what was actually said):**
`~/.local/share/opencode/opencode.db`, table **`session_message`** — creator turns are `type='user'`
with the text in `data.text`; my turns are `type='assistant'` with **reasoning AND the rendered reply
concatenated into `content[0].text`** (so my own reasoning quotes the creator's phrases — a `part` or
`data.text` hit may be my echo, not a real message). The **reply marker's verbatim string is recorded
in `MyMistakes.md`** (it was lost once and cost a session to re-derive).
- LAN credentials: `CREDENTIALS.md` (gitignored). Google Cloud OAuth project number `198374416003`,
owned by `llamachile.shop@gmail.com` — verify at
duplicate name gets a `-2` suffix).
- **Conversation history:** `~/.local/share/opencode/opencode.db`, table **`session_message`** — creator
turns are `type='user'` with text in `data.text`; my turns are `type='assistant'` with reasoning AND the
rendered reply concatenated into `content[0].text`. The **reply marker's verbatim string is recorded in
`MyMistakes.md`**.
- LAN credentials: `CREDENTIALS.md` (gitignored) — **⚠️ stale for HA: it says `192.168.50.144`, the real
Home Assistant is `192.168.50.145`** (`homeassistant.local`, HAOS 18.2, Core 2026.10.1, SSH
`gramps`/`einstein`, `/config` root-owned). `gordito` is `.144`. Fix this file when next editing it.
- Google Cloud OAuth project number `198374416003`, owned by `llamachile.shop@gmail.com` — verify at
`https://console.cloud.google.com/apis/credentials?project=198374416003`.
## Open (operational) — officerFriendly / PS5 "DNS errors" (2026-10-17, off-repo work)
- **Root cause found: a ~43 h-wrong system clock on officerFriendly × `dnssec = true`.** The box is a
**Pi 400 with no RTC hardware at all** (no `/dev/rtc*`, `hwclock` cannot open one, no `i2c-rtc`
overlay) and a **healthy oscillator** (`adjtimex` freq = −7.39 ppm = −0.64 s/day), so there is no
"dead RTC battery" — time comes from `fake-hwclock`'s saved file + `systemd-timesyncd`. The journal
shows the clock stepping forward **+54 min, +7 h, then +42.9 h** (last step Oct 14 13:56:44, the first
successful time-server contact after the Oct 12 19:01:43 restart). A validating resolver with a wrong
clock returns BOGUS, and `pihole-FTL.db` shows exactly that: **1,688,396 `dnssec=3` rows on 10-11 and
839,276 on 10-12 vs ≤23 every other day**, hitting `ps5.np.playstation.net` and the other PSN
endpoints = the PS5's "DNS errors". The KSK-rollover theory is **retracted**: both root anchors
(`20326` + `38696`) were already in place, and `dig @127.0.0.1 . DNSKEY` still returns `ad`.
## Open (operational) — officerFriendly / PS5 "DNS errors" (2026-10-17, off-repo)
- **Root cause: a ~43 h-wrong system clock on officerFriendly × `dnssec = true`.** Pi 400, **no RTC
hardware at all**, healthy oscillator (−7.39 ppm). Journal shows +54 min, +7 h, +42.9 h steps.
`pihole-FTL.db`: **1,688,396 `dnssec=3` rows on 10-11, 839,276 on 10-12 vs ≤23 other days**, hitting
PSN endpoints = the PS5's "DNS errors". KSK-rollover theory **retracted**.
- **Pending creator decision (do not apply unilaterally):** tighten
`/etc/systemd/timesyncd.conf.d/lan.conf` (currently IP-literal NTP servers = Cloudflare + Google, but
stock poll settings) to `PollIntervalMinSec=16` / `PollIntervalMaxSec=64` / `ConnectionRetrySec=8` /
`SaveIntervalSec=30`, and/or give the Pi a DS3231 RTC on i2c (`i2c-tools` is already installed).
- **LAN DNS routing vs creator intent:** DHCP still hands out `dhcp_dns1_x = 192.168.50.26` +
`dhcp_dns2_x = 192.168.50.1` (clients query Pi-hole **directly** — 37 distinct LAN nodes in 24 h, 41
including the router), whereas the creator wants everything to point at `.1` with BBR forwarding to
Pi-hole (`wan_dns = 192.168.50.26` already works: `.1` made 3811 queries). Also `dnssec = true` on
Pi-hole duplicates dnscrypt-proxy's `require_dnssec = true`. Both are offered-for-veto, not yet done.
`/etc/systemd/timesyncd.conf.d/lan.conf` to `PollIntervalMinSec=16` / `PollIntervalMaxSec=64` /
`ConnectionRetrySec=8` / `SaveIntervalSec=30`, and/or add a DS3231 RTC (`i2c-tools` already installed).
- **LAN DNS routing vs creator intent:** DHCP still hands out `.26` + `.1` (clients query Pi-hole
directly) whereas the creator wants everything → `.1` with BBR forwarding. Also `dnssec = true` on
Pi-hole duplicates dnscrypt-proxy's `require_dnssec = true`. Both offered-for-veto, not done.
- Router is the DHCP server (ASUS ROG GT-BE98 Pro `.1`) — **Pi-hole `.26` is DNS only**, its
`dhcp.leases` is 0 bytes.
+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).