Skip to content

fix(bias): don't let an unreachable bulb spin the edge-latch loop - #8

Merged
cnewkirk merged 2 commits into
mainfrom
fix/bias-unreachable-latch
Jul 26, 2026
Merged

fix(bias): don't let an unreachable bulb spin the edge-latch loop#8
cnewkirk merged 2 commits into
mainfrom
fix/bias-unreachable-latch

Conversation

@cnewkirk

Copy link
Copy Markdown
Owner

Problem (observed live 2026-07-25)

A housekeeper unplugged the Couch lightstrip — a TV-bias light. Its write
failed on every tick, and the daemon only latched a committed bias edge
(_bias_last_applied_on) when all per-light writes succeeded. So the edge
never latched, the 2 s control-file poll read every cycle as a fresh TV flip,
and the daemon re-fired a 2.0 s edge fade to all 7 viewing lights every ~2 s
for hours (293 edge fades in 3 minutes). Symptom: the whole viewing set
visibly dimmed/brightened continuously at dawn.

The don't latch a failed edge rule was built for transient bridge
rejections (e.g. "command queue is full") that clear on retry — it had no
concept of a permanently unreachable device.

Fix

Classify write failures instead of collapsing them to a bool:

  • BridgeError gains an unreachable flag; HueClient._parse sets it when the
    v2 errors array reports a device has "communication issues" (the
    unplugged-bulb signal) — the string match lives in exactly one place.
  • _write_light returns _BiasWrite.OK / FAILED / UNREACHABLE. The edge
    latches once every write has landed or hit an unreachable device; only
    FAILED (a transient reject worth retrying) holds the latch open.

A dead viewing light now degrades to "that bulb is dark" — the others behave and
nothing cascades. Transient-retry behaviour is unchanged.

Tests

  • test_unreachable_light_does_not_stall_edge_latch — edge latches despite a
    dead bulb; the next apply is a no-op (no storm); the reachable light still
    held.
  • test_parse_flags_communication_issue_as_unreachable /
    test_parse_transient_error_is_not_unreachable — parser classification.
  • Existing test_failed_edge_writes_retry_on_next_apply still green (transient
    retry preserved). Full suite: 363 passed, mypy clean.

Deploy status

Already built and running live on the NAS hue-circadian container (recreated
from the identical prior spec; old container parked as hue-circadian-old for
rollback). Verified healthy, day-drive normal, bias edge-fires down to ~1 per
300 log lines.

🤖 Generated with Claude Code

cnewkirk and others added 2 commits July 25, 2026 18:00
The daemon only latched a committed TV-bias edge (_bias_last_applied_on)
when every per-light write succeeded, so a *transient* bridge rejection
would retry on the next tick. But a bias light that goes Zigbee-unreachable
(a housekeeper unplugs it / kills the wall switch) fails its write on
*every* tick, so the edge never latched — the 2s control-file poll's
"has tv_on changed?" guard stayed permanently true and re-fired a fresh
2.0s edge fade to all 7 viewing lights every poll, indefinitely. Observed
live 2026-07-25: 293 re-fires in 3 minutes off one unplugged couch strip.

Classify write failures instead of collapsing them to a bool:

- BridgeError gains an `unreachable` flag; the client parser sets it when
  the v2 errors array reports a device has "communication issues" (the
  unplugged-bulb signal), so the string match lives in exactly one place.
- _write_light returns OK / FAILED / UNREACHABLE. The edge latches once
  every write has landed or hit an unreachable device; only FAILED (a
  transient reject worth retrying) holds the latch open.

A dead viewing light now degrades to "that bulb is dark" — the other six
behave, and nothing cascades. Transient-retry behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cnewkirk
cnewkirk merged commit f03a3ab into main Jul 26, 2026
1 check passed
@cnewkirk
cnewkirk deleted the fix/bias-unreachable-latch branch July 26, 2026 14:38
cnewkirk added a commit that referenced this pull request Jul 26, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cnewkirk added a commit that referenced this pull request Jul 26, 2026
…risk

The settle-and-compare detector compares the grouped brightness to target; an
unreachable core member could skew the aggregate into a false suspend — the
core twin of the bias-latch bug (#8). Flag it at the comparison site for a
later audit; not yet observed live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant