fix(ws): hold SYNCING through post-connect status replay (#512)#513
Merged
Conversation
WebSocketClient set ES_CONNECTED the instant the aiohttp socket opened, before the controller's post-connect status replay (a burst of ST/DON/DOF frames) drained. connection_events consumers that treat ES_CONNECTED as "the stream is live" therefore saw the replay as live events, firing spurious triggers on every connect, config-entry reload, and reconnect. Add an intermediate ES_SYNCING state held from socket-open until the replay goes quiet for WS_SYNC_QUIET_SECONDS (1.0s, no frame), then promote to ES_CONNECTED. A hard cap (WS_SYNC_MAX_SECONDS, 10.0s) keeps a chatty controller from stalling the stream. A sampled watcher task does the debounce so the read loop stays a plain `async for`; it is cancelled in the read-loop finally, so a socket that drops before settling never reports CONNECTED. Records still update during SYNCING (the dispatcher keeps feeding); only the "stream is live" signal is withheld. SYNCING is re-armed on every reconnect. Status transitions now log at DEBUG. Pure-additive: ES_SYNCING is a new enum value/alias; no existing ES_* constant is renamed or repurposed. HA Core's isy994 only branches on CONNECTED/DISCONNECTED/LOST_STREAM_CONNECTION/RECONNECTING/RECONNECT_FAILED and gates entity availability on node.enabled (not isy.connected), so it is unaffected until it opts into gating event-style entities on connected. Backport of pyisyox#170. Unblocks home-assistant/core#169782. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapse the replay/SYNCING explanation that was restated in the module constant comment and the websocket() inline comment down to the canonical _promote_when_quiet docstring, and document that the quiet window is anchored to socket-open (the known first-frame-latency limitation, matching pyisyox). No behaviour change. Co-Authored-By: Claude Opus 4.7 <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.
Summary
Fixes #512.
WebSocketClient.websocket()setES_CONNECTEDthe instant the aiohttp socket opened, before the controller's post-connect status replay (a burst ofST/DON/DOFframes) drained.connection_eventsconsumers that treatES_CONNECTEDas "the stream is now live" therefore saw the replay as live events, firing spurious triggers on every connect, config-entry reload, and WebSocket reconnect.This is the direct backport of the already-shipped pyisyox fix (shbatm/pyisyox#170) and unblocks home-assistant/core#169782 (event platform for
isy994).Changes
constants.py— addEventStreamStatus.SYNCING(+ES_SYNCINGalias). Pure-additive; no existingES_*constant renamed or repurposed.events/websocket.py— on socket open, setES_SYNCING(notES_CONNECTED) and spawn a_promote_when_quiet()watcher:ES_CONNECTEDonce the replay goes quiet forWS_SYNC_QUIET_SECONDS(1.0s, no frame), with a hard capWS_SYNC_MAX_SECONDS(10.0s) so a perpetually chatty controller can't stall the stream.async for; cancelled in the read-loopfinally, so a socket that drops before settling never reportsCONNECTED.SYNCING(the dispatcher keeps feeding) — only the "stream is live" signal is withheld.Compatibility
EventEmitternotifies the same way — consumers just receive one extra transition value during the ~1s sync window.isy994only branches onCONNECTED/DISCONNECTED/LOST_STREAM_CONNECTION/RECONNECTING/RECONNECT_FAILEDand gates entity availability onnode.enabled(notisy.connected), so it is unaffected until it opts into gating event-style entities onconnected.isy.connectedsimply turns true ~1s later.Known limitation
The quiet window is anchored to socket-open, not to the first replayed frame (matching the validated pyisyox design). The IoX replay is cache-served on the subscribe round-trip, so the first frame is sub-second in practice; a pathologically delayed first frame (> the quiet window) is the accepted trade-off — anchoring on first-frame would regress a genuinely silent controller to the 10s cap.
Tests
tests/test_websocket_lifecycle.py(offline, no live ISY):_promote_when_quiet: silent-controller promote, holds-through-replay-then-promote, hard-cap under constant traffic.websocket(): replay routed (records update) while stream staysSYNCING,CONNECTEDonly after the quiet window.CONNECTEDnever emitted.Full suite: 448 passed. pre-commit (ruff/codespell/prettier): clean.
🤖 Generated with Claude Code