fix(bias): don't let an unreachable bulb spin the edge-latch loop - #8
Merged
Conversation
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (observed live 2026-07-25)
A housekeeper unplugged the
Couch lightstrip— a TV-bias light. Its writefailed on every tick, and the daemon only latched a committed bias edge
(
_bias_last_applied_on) when all per-light writes succeeded. So the edgenever 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 edgerule was built for transient bridgerejections (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:
BridgeErrorgains anunreachableflag;HueClient._parsesets it when thev2
errorsarray reports a device has "communication issues" (theunplugged-bulb signal) — the string match lives in exactly one place.
_write_lightreturns_BiasWrite.OK / FAILED / UNREACHABLE. The edgelatches 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 adead 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.test_failed_edge_writes_retry_on_next_applystill green (transientretry preserved). Full suite: 363 passed, mypy clean.
Deploy status
Already built and running live on the NAS
hue-circadiancontainer (recreatedfrom the identical prior spec; old container parked as
hue-circadian-oldforrollback). Verified healthy, day-drive normal, bias edge-fires down to ~1 per
300 log lines.
🤖 Generated with Claude Code