zara-home: add bounded Home Assistant WebSocket state event stream #50

Open
opened 2026-09-08 02:02:52 +00:00 by nsaspy · 0 comments
Owner

Problem

zara-home has a bounded REST adapter on main; PR #514 adds the production HTTP/auth transport, but live Home Assistant state changes still have no production event stream. The next provider-correctness slice should add Home Assistant WebSocket observation without weakening the existing rule that a mutation is verified only by an independent current-state read-back.

Current Home Assistant WebSocket semantics (official docs, September 2026) are explicit:

  • connect at /api/websocket;
  • server sends auth_required, client sends {type:"auth",access_token:...}, then server returns auth_ok or auth_invalid and disconnects;
  • post-auth commands carry unique integer IDs;
  • subscribe_events with event_type:"state_changed" is active only after its matching result.success=true;
  • event frames carry the original subscription ID and an event payload containing entity_id, old_state, and new_state state objects;
  • unsubscribe_events explicitly tears down a subscription;
  • disconnect loses session/subscription state, so reconnect requires a fresh auth handshake and fresh subscription;
  • call_service completion is only command completion; Home Assistant explicitly points clients to state events for resulting entity changes.

Official source: https://developers.home-assistant.io/docs/api/websocket/

Frozen bounded slice

Implement a production read-only Home Assistant WebSocket state stream for zara-home. Do not move mutation verification onto events, do not add SmartThings credential authority, and do not create a generic Core event bus abstraction in this plugin.

Required behavior:

  • derive only the same normalized Home Assistant origin already configured for the provider; connect only to that origin's /api/websocket (ws for configured http, wss for configured https), with no redirect/cross-origin credential forwarding;
  • reuse the provider's current validated access-token source without exposing bearer values in errors, frames, logs, return values, or fixtures;
  • bounded auth handshake: require auth_required, send one auth frame, accept only auth_ok; auth_invalid, malformed phase ordering, timeout, oversized frame, or disconnect => explicit unavailable/reauth state;
  • after auth, issue one subscribe_events command for state_changed; do not treat the subscription as active until the matching result succeeds;
  • accept only bounded JSON object frames with expected integer command/subscription IDs; ignore unrelated command results/events and reject malformed/oversized protocol data without poisoning cached state;
  • normalize only state_changed events whose entity_id is one of zara-home's supported device domains; validate new_state through the existing Home Assistant normalization boundary before publication/cache update;
  • maintain per-entity monotonic observation metadata from Home Assistant state timestamps (last_updated, with last_changed retained as state metadata): a newly received event must not overwrite a locally newer observed state; invalid/missing timestamps fail closed for cache replacement rather than being invented locally;
  • on disconnect, mark the stream stale/unavailable, discard the old subscription identity, reconnect with bounded backoff, re-authenticate, and create a fresh subscription; do not replay the old subscription ID across sessions;
  • after successful reconnect/subscription, reconcile with authoritative current state (GET /api/states or equivalent bounded provider read) before declaring the stream fresh, so events missed during downtime cannot leave the cache silently stale;
  • expose event-derived state only as observation/cache data. HomeService.set_property() must continue its independent provider get_device() read-back and only set verified=true when that observed current value equals the requested value. A matching WebSocket event alone is never mutation verification evidence.

Tests first

Use a deterministic local fake WebSocket provider (no live credentials) and cover at minimum:

  • exact auth phase ordering and secret absence from errors/evidence;
  • auth_invalid / malformed handshake / timeout / disconnect;
  • subscription becomes active only after matching successful result;
  • unrelated IDs and non-state_changed events do not mutate observation state;
  • supported device event normalizes correctly;
  • malformed JSON/object shape, oversized frame, malformed state payload, unsupported domain;
  • older/out-of-order last_updated cannot overwrite newer cached state;
  • disconnect marks stale, reconnect creates a new auth/session/subscription, and state reconciliation runs before fresh status;
  • events received before subscription acknowledgement do not become authoritative cached state;
  • provider service acknowledgement or matching event with mismatching independent REST read-back still yields verified=false;
  • matching independent REST read-back remains verified=true;
  • cancellation/stop tears down the socket/subscription cooperatively and does not leave a reconnect loop alive;
  • bearer/token text never appears in structured failure strings or recorded test evidence.

Then run exact-head:

  • python3 -m unittest discover -s plugins/zara-home/test -t plugins/zara-home/test
  • python3 scripts/validate-registry.py
  • repository compatibility/plugin gates relevant to zara-home
  • nix flake check

Dependencies / boundaries

  • Implementation should branch after #514 is merged (or explicitly rebase onto its exact transport/auth head) so the WebSocket path shares the hardened Home Assistant origin/token boundary rather than duplicating it.
  • SmartThings OAuth/device/event ownership remains Core lost-rob0t/zara#202.
  • No Android credential authority, no arbitrary shell/browser fallback, no committed token.
  • Prefer a maintained WebSocket client library already compatible with repository packaging; if a new dependency is needed, keep it narrowly scoped and cover install/Nix compatibility in the implementation PR.

Research basis

Repository source checked at main eaef81369584453a5a6020d6043415b2489098ca: HomeAssistantAdapter remains REST-only and HomeService.set_property() performs the post-write provider read-back that controls verified. PR #514 exact head e3024d3d725074b3b35ecd468e1f90cffe2c1b20 supplies the bounded production Home Assistant HTTP/auth seam this slice should reuse.

Official Home Assistant docs checked current September 2026:

## Problem `zara-home` has a bounded REST adapter on `main`; PR #514 adds the production HTTP/auth transport, but live Home Assistant state changes still have no production event stream. The next provider-correctness slice should add Home Assistant WebSocket observation without weakening the existing rule that a mutation is verified only by an independent current-state read-back. Current Home Assistant WebSocket semantics (official docs, September 2026) are explicit: - connect at `/api/websocket`; - server sends `auth_required`, client sends `{type:"auth",access_token:...}`, then server returns `auth_ok` or `auth_invalid` and disconnects; - post-auth commands carry unique integer IDs; - `subscribe_events` with `event_type:"state_changed"` is active only after its matching `result.success=true`; - event frames carry the original subscription ID and an event payload containing `entity_id`, `old_state`, and `new_state` state objects; - `unsubscribe_events` explicitly tears down a subscription; - disconnect loses session/subscription state, so reconnect requires a fresh auth handshake and fresh subscription; - `call_service` completion is only command completion; Home Assistant explicitly points clients to state events for resulting entity changes. Official source: https://developers.home-assistant.io/docs/api/websocket/ ## Frozen bounded slice Implement a production **read-only Home Assistant WebSocket state stream** for `zara-home`. Do not move mutation verification onto events, do not add SmartThings credential authority, and do not create a generic Core event bus abstraction in this plugin. Required behavior: - derive only the same normalized Home Assistant origin already configured for the provider; connect only to that origin's `/api/websocket` (`ws` for configured `http`, `wss` for configured `https`), with no redirect/cross-origin credential forwarding; - reuse the provider's current validated access-token source without exposing bearer values in errors, frames, logs, return values, or fixtures; - bounded auth handshake: require `auth_required`, send one auth frame, accept only `auth_ok`; `auth_invalid`, malformed phase ordering, timeout, oversized frame, or disconnect => explicit unavailable/reauth state; - after auth, issue one `subscribe_events` command for `state_changed`; do not treat the subscription as active until the matching `result` succeeds; - accept only bounded JSON object frames with expected integer command/subscription IDs; ignore unrelated command results/events and reject malformed/oversized protocol data without poisoning cached state; - normalize only `state_changed` events whose `entity_id` is one of zara-home's supported device domains; validate `new_state` through the existing Home Assistant normalization boundary before publication/cache update; - maintain per-entity monotonic observation metadata from Home Assistant state timestamps (`last_updated`, with `last_changed` retained as state metadata): a newly received event must not overwrite a locally newer observed state; invalid/missing timestamps fail closed for cache replacement rather than being invented locally; - on disconnect, mark the stream stale/unavailable, discard the old subscription identity, reconnect with bounded backoff, re-authenticate, and create a fresh subscription; do not replay the old subscription ID across sessions; - after successful reconnect/subscription, reconcile with authoritative current state (`GET /api/states` or equivalent bounded provider read) before declaring the stream fresh, so events missed during downtime cannot leave the cache silently stale; - expose event-derived state only as observation/cache data. `HomeService.set_property()` must continue its independent provider `get_device()` read-back and only set `verified=true` when that observed current value equals the requested value. A matching WebSocket event alone is never mutation verification evidence. ## Tests first Use a deterministic local fake WebSocket provider (no live credentials) and cover at minimum: - exact auth phase ordering and secret absence from errors/evidence; - `auth_invalid` / malformed handshake / timeout / disconnect; - subscription becomes active only after matching successful result; - unrelated IDs and non-`state_changed` events do not mutate observation state; - supported device event normalizes correctly; - malformed JSON/object shape, oversized frame, malformed state payload, unsupported domain; - older/out-of-order `last_updated` cannot overwrite newer cached state; - disconnect marks stale, reconnect creates a new auth/session/subscription, and state reconciliation runs before fresh status; - events received before subscription acknowledgement do not become authoritative cached state; - provider service acknowledgement or matching event with mismatching independent REST read-back still yields `verified=false`; - matching independent REST read-back remains `verified=true`; - cancellation/stop tears down the socket/subscription cooperatively and does not leave a reconnect loop alive; - bearer/token text never appears in structured failure strings or recorded test evidence. Then run exact-head: - `python3 -m unittest discover -s plugins/zara-home/test -t plugins/zara-home/test` - `python3 scripts/validate-registry.py` - repository compatibility/plugin gates relevant to `zara-home` - `nix flake check` ## Dependencies / boundaries - Implementation should branch after #514 is merged (or explicitly rebase onto its exact transport/auth head) so the WebSocket path shares the hardened Home Assistant origin/token boundary rather than duplicating it. - SmartThings OAuth/device/event ownership remains Core lost-rob0t/zara#202. - No Android credential authority, no arbitrary shell/browser fallback, no committed token. - Prefer a maintained WebSocket client library already compatible with repository packaging; if a new dependency is needed, keep it narrowly scoped and cover install/Nix compatibility in the implementation PR. ## Research basis Repository source checked at `main` `eaef81369584453a5a6020d6043415b2489098ca`: `HomeAssistantAdapter` remains REST-only and `HomeService.set_property()` performs the post-write provider read-back that controls `verified`. PR #514 exact head `e3024d3d725074b3b35ecd468e1f90cffe2c1b20` supplies the bounded production Home Assistant HTTP/auth seam this slice should reuse. Official Home Assistant docs checked current September 2026: - WebSocket API: https://developers.home-assistant.io/docs/api/websocket/ - Authentication API: https://developers.home-assistant.io/docs/auth_api/ - REST API / State shape: https://developers.home-assistant.io/docs/api/rest/
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nsaspy/zara-plugins#50
No description provided.