feat(telemetry): broadcast door hardware state transitions as a first-class event #55

Closed
opened 2026-09-22 16:31:12 +00:00 by gabogg · 0 comments
Owner

Spun out of #14 Candidate 2 during the grilling session of 2026-09-22.

Problem

Door hardware contact transitions are persisted but never broadcast. The table door_hardware_state_transitions is created at app/db/database.py:276 and written at app/db/door_repository.py:486, :527, :577, :596 — but no WebSocket message type carries them.

monitor_service.py emits three message types today: doors_update (:110-119, :32-42), occupancy_update (:45-52) and telemetry (:145-157). A client wanting contact transitions has to infer them by diffing successive doors_update overviews, which loses any transition that occurs between two polls.

Why it was split out

#14 bundled this with the streaming-seam deepening under a DoorStateTransition event type. It is not a deepening — the existing broadcast is not wrong about transitions, it simply does not carry them. This is a new capability and should be scoped and tested as one.

Suggested approach

  • Define a DoorStateTransition DTO (no equivalent exists in app/schemas/; contrast PassengerFlowEvent at occupancy_models.py:234, which already covers the passage case).
  • Emit it from the same seam that writes the table, so persistence and broadcast cannot diverge.
  • Decide whether it rides inside doors_update or gets its own message type — the client's handleIncomingWsMessage (app/static/js/app.js:511) and TelemetryEngine.ingestMessage both switch on type.

Acceptance criteria

  • DoorStateTransition DTO defined with the fields the table persists.
  • Every write to door_hardware_state_transitions produces exactly one broadcast.
  • Client ingests transitions without diffing overviews.
  • Tests cover the transition-to-broadcast path.

🤖 Generated with Claude Code


Triage resolution — 2026-09-23

This resolution supersedes conflicting original acceptance criteria.

A door state transition is a persisted change between door states, including
physical open/close changes, commanded states and offline/recovery changes.
Do not describe every state change as a physical contact movement.

Deliver live updates with current-state resynchronization on reconnect. Recovery
of all transitions missed during disconnection is outside the initial scope;
there is no exactly-once browser-delivery guarantee. Current-state initialization
already exists on WebSocket connection and must be retained.

Acceptance:

  • Add a dedicated transition message with transition identity, door identity,
    previous/new state, timestamp, source and exclusion metadata.
  • Cover all persistence paths: async polling, sync polling, webhook and
    reconciliation. Services own emission after successful persistence;
    repositories remain persistence-only. Avoid duplicate emissions by callers.
  • In normal connected operation, each newly persisted transition causes one
    emission attempt. An emission attempt is not a delivery acknowledgment.
  • Include excluded doors so hardware inventory stays current, while keeping
    them excluded from activity streams and rankings under existing rules.
  • Do not serialize complete audit records or details_json; person names
    and card numbers remain in existing access-cycle messages.
  • Include dashboard ingestion through the telemetry module, preserving
    overview snapshots for initialization, reconnect and derived metadata.
  • Test persistence-to-emission and client ingestion, including offline,
    commanded and excluded-door transitions, failed persistence, duplicate
    emission prevention and reconnect snapshots.
Spun out of #14 Candidate 2 during the grilling session of 2026-09-22. ## Problem Door hardware contact transitions are **persisted but never broadcast**. The table `door_hardware_state_transitions` is created at `app/db/database.py:276` and written at `app/db/door_repository.py:486`, `:527`, `:577`, `:596` — but no WebSocket message type carries them. `monitor_service.py` emits three message types today: `doors_update` (`:110-119`, `:32-42`), `occupancy_update` (`:45-52`) and `telemetry` (`:145-157`). A client wanting contact transitions has to infer them by diffing successive `doors_update` overviews, which loses any transition that occurs between two polls. ## Why it was split out #14 bundled this with the streaming-seam deepening under a `DoorStateTransition` event type. It is not a deepening — the existing broadcast is not *wrong* about transitions, it simply does not carry them. This is a **new capability** and should be scoped and tested as one. ## Suggested approach - Define a `DoorStateTransition` DTO (no equivalent exists in `app/schemas/`; contrast `PassengerFlowEvent` at `occupancy_models.py:234`, which already covers the passage case). - Emit it from the same seam that writes the table, so persistence and broadcast cannot diverge. - Decide whether it rides inside `doors_update` or gets its own message type — the client's `handleIncomingWsMessage` (`app/static/js/app.js:511`) and `TelemetryEngine.ingestMessage` both switch on `type`. ## Acceptance criteria - [ ] `DoorStateTransition` DTO defined with the fields the table persists. - [ ] Every write to `door_hardware_state_transitions` produces exactly one broadcast. - [ ] Client ingests transitions without diffing overviews. - [ ] Tests cover the transition-to-broadcast path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ### Triage resolution — 2026-09-23 This resolution supersedes conflicting original acceptance criteria. A **door state transition** is a persisted change between door states, including physical open/close changes, commanded states and offline/recovery changes. Do not describe every state change as a physical contact movement. Deliver live updates with current-state resynchronization on reconnect. Recovery of all transitions missed during disconnection is outside the initial scope; there is no exactly-once browser-delivery guarantee. Current-state initialization already exists on WebSocket connection and must be retained. Acceptance: - [ ] Add a dedicated transition message with transition identity, door identity, previous/new state, timestamp, source and exclusion metadata. - [ ] Cover all persistence paths: async polling, sync polling, webhook and reconciliation. Services own emission after successful persistence; repositories remain persistence-only. Avoid duplicate emissions by callers. - [ ] In normal connected operation, each newly persisted transition causes one emission attempt. An emission attempt is not a delivery acknowledgment. - [ ] Include excluded doors so hardware inventory stays current, while keeping them excluded from activity streams and rankings under existing rules. - [ ] Do not serialize complete audit records or `details_json`; person names and card numbers remain in existing access-cycle messages. - [ ] Include dashboard ingestion through the telemetry module, preserving overview snapshots for initialization, reconnect and derived metadata. - [ ] Test persistence-to-emission and client ingestion, including offline, commanded and excluded-door transitions, failed persistence, duplicate emission prevention and reconnect snapshots.
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
gabogg/hikcentral#55
No description provided.