pyisyox.runtime.ws module

WebSocket reader loop for the eisy event stream.

Opens wss://{host}/rest/subscribe (or another configured path) using the same pyisyox.auth.Auth strategy the HTTP client uses, then runs a read loop that feeds every frame to an EventDispatcher. Reconnects with exponential backoff on transport errors; refreshes auth tokens on a 401-class WebSocket handshake failure.

Auth integration:

  • LocalAuth returns {"auth": aiohttp.BasicAuth(...)} from request_kwargs — aiohttp’s ws_connect accepts auth directly, so the upgrade carries an Authorization: Basic header.

  • PortalAuth returns {"headers": {"Authorization": "Bearer ..."}}. ws_connect passes headers through verbatim, so the bearer rides on the upgrade.

The loop is intentionally split from the parsing/dispatch logic in pyisyox.runtime.events so the dispatcher can be unit-tested without WebSocket plumbing and the reader can be unit-tested without a real WS server.

class WebSocketEventStream(client, dispatcher, path='/rest/subscribe')[source]

Bases: object

Background reader that feeds frames into an EventDispatcher.

Lifecycle:

  1. start() schedules the read task and returns immediately.

  2. The task connects, dispatches frames, reconnects on transport errors, and pumps EventStreamStatus notifications to any registered status listener. On each connect it holds SYNCING (not CONNECTED) until the controller’s initial status replay drains, so consumers don’t treat the replay as live events.

  3. stop() cancels the task and closes any active WS.

The class deliberately keeps its surface narrow — the consumer is expected to be the top-level ISY glue object that owns both the IoXClient and the dispatcher.

Parameters:
property status: EventStreamStatus

Most-recent stream status.

Updated on every transition (initialise / connect / reconnect / disconnect / lost). Defaults to EventStreamStatus.NOT_STARTED before start(). Useful for system-health pages that want a single readable status string without subscribing to every notification.

property connected: bool

True while the stream is in the CONNECTED state.

Convenience over comparing status directly. Note that connected flipping False doesn’t mean the reader has given up — it may be reconnecting, or in EventStreamStatus.SYNCING (socket open but the controller’s initial status replay hasn’t drained yet — intentionally not “connected” so event consumers don’t treat the replay as live changes).

property last_event_at: datetime | None

UTC timestamp of the most recent text frame, or None if no frame has been received this lifetime.

The eisy emits a heartbeat <control>_0</control> frame every 30 seconds even when nothing else changes, so a stale last_event_at (more than ~60 s ago) is a reasonable signal that the connection is broken even when the WS handshake hasn’t returned an error yet.

add_status_listener(callback)[source]

Register a callback for stream-status changes.

Returns:

An unsubscribe function.

Parameters:

callback (Callable[[EventStreamStatus], None])

Return type:

Callable[[], None]

start()[source]

Start the background read loop. Idempotent — calling twice returns the existing task.

Return type:

Task[None]

async stop()[source]

Stop the read loop and close any active WebSocket.

Return type:

None