feat(tv-power): implement server PR1 with tests and documentation
This commit is contained in:
163
TV_POWER_INTENT_SERVER_CONTRACT_V1.md
Normal file
163
TV_POWER_INTENT_SERVER_CONTRACT_V1.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# TV Power Intent — Server Contract v1 (Phase 1)
|
||||
|
||||
> This document is the stable reference for client-side implementation.
|
||||
> The server implementation is validated and frozen at this contract.
|
||||
> Last validated: 2026-04-01
|
||||
|
||||
---
|
||||
|
||||
## Topic
|
||||
|
||||
```
|
||||
infoscreen/groups/{group_id}/power/intent
|
||||
```
|
||||
|
||||
- **Scope**: group-level only (Phase 1). No per-client topic in Phase 1.
|
||||
- **QoS**: 1
|
||||
- **Retained**: true — broker holds last payload; client receives it immediately on (re)connect.
|
||||
|
||||
---
|
||||
|
||||
## Publish semantics
|
||||
|
||||
| Trigger | Behaviour |
|
||||
|---|---|
|
||||
| Semantic transition (state/reason changes) | New `intent_id`, immediate publish |
|
||||
| No change (heartbeat) | Same `intent_id`, refreshed `issued_at` and `expires_at`, published every poll interval |
|
||||
| Scheduler startup | Immediate publish before first poll wait |
|
||||
| MQTT reconnect | Immediate retained republish of last known intent |
|
||||
|
||||
Poll interval default: **15 seconds** (dev) / **30 seconds** (prod).
|
||||
|
||||
---
|
||||
|
||||
## Payload schema
|
||||
|
||||
All fields are always present. No optional fields for Phase 1 required fields.
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"intent_id": "<uuid4>",
|
||||
"group_id": <integer>,
|
||||
"desired_state": "on" | "off",
|
||||
"reason": "active_event" | "no_active_event",
|
||||
"issued_at": "<ISO 8601 UTC with Z>",
|
||||
"expires_at": "<ISO 8601 UTC with Z>",
|
||||
"poll_interval_sec": <integer>,
|
||||
"active_event_ids": [<integer>, ...],
|
||||
"event_window_start": "<ISO 8601 UTC with Z>" | null,
|
||||
"event_window_end": "<ISO 8601 UTC with Z>" | null
|
||||
}
|
||||
```
|
||||
|
||||
### Field reference
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `schema_version` | string | Always `"1.0"` in Phase 1 |
|
||||
| `intent_id` | string (uuid4) | Stable across heartbeats; new value on semantic transition |
|
||||
| `group_id` | integer | Matches the MQTT topic group_id |
|
||||
| `desired_state` | `"on"` or `"off"` | The commanded TV power state |
|
||||
| `reason` | string | Human-readable reason for current state |
|
||||
| `issued_at` | UTC Z string | When this payload was computed |
|
||||
| `expires_at` | UTC Z string | After this time, payload is stale; re-subscribe or treat as `off` |
|
||||
| `poll_interval_sec` | integer | Server poll interval; expiry = max(3 × poll, 90s) |
|
||||
| `active_event_ids` | integer array | IDs of currently active events; empty when `off` |
|
||||
| `event_window_start` | UTC Z string or null | Start of merged active coverage window; null when `off` |
|
||||
| `event_window_end` | UTC Z string or null | End of merged active coverage window; null when `off` |
|
||||
|
||||
---
|
||||
|
||||
## Expiry rule
|
||||
|
||||
```
|
||||
expires_at = issued_at + max(3 × poll_interval_sec, 90s)
|
||||
```
|
||||
|
||||
Default at poll=15s → expiry window = **90 seconds**.
|
||||
|
||||
**Client rule**: if `now > expires_at` treat as stale and fall back to `off` until a fresh payload arrives.
|
||||
|
||||
---
|
||||
|
||||
## Example payloads
|
||||
|
||||
### ON (active event)
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"intent_id": "4a7fe3bc-3654-48e3-b5b9-9fad1f7fead3",
|
||||
"group_id": 2,
|
||||
"desired_state": "on",
|
||||
"reason": "active_event",
|
||||
"issued_at": "2026-04-01T06:00:03.496Z",
|
||||
"expires_at": "2026-04-01T06:01:33.496Z",
|
||||
"poll_interval_sec": 15,
|
||||
"active_event_ids": [148],
|
||||
"event_window_start": "2026-04-01T06:00:00Z",
|
||||
"event_window_end": "2026-04-01T07:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### OFF (no active event)
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"intent_id": "833c53e3-d728-4604-9861-6ff7be1f227e",
|
||||
"group_id": 2,
|
||||
"desired_state": "off",
|
||||
"reason": "no_active_event",
|
||||
"issued_at": "2026-04-01T07:00:03.702Z",
|
||||
"expires_at": "2026-04-01T07:01:33.702Z",
|
||||
"poll_interval_sec": 15,
|
||||
"active_event_ids": [],
|
||||
"event_window_start": null,
|
||||
"event_window_end": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validated server behaviours (client can rely on these)
|
||||
|
||||
| Scenario | Guaranteed server behaviour |
|
||||
|---|---|
|
||||
| Event starts | `desired_state: on` emitted within one poll interval |
|
||||
| Event ends | `desired_state: off` emitted within one poll interval |
|
||||
| Adjacent events (end1 == start2) | No intermediate `off` emitted at boundary |
|
||||
| Overlapping events | `desired_state: on` held continuously |
|
||||
| Scheduler restart during active event | Immediate `on` republish on reconnect; broker retained holds `on` during outage |
|
||||
| No events in group | `desired_state: off` with empty `active_event_ids` |
|
||||
| Heartbeat (no change) | Same `intent_id`, refreshed timestamps every poll |
|
||||
|
||||
---
|
||||
|
||||
## Client responsibilities (Phase 1)
|
||||
|
||||
1. **Subscribe** to `infoscreen/groups/{own_group_id}/power/intent` at QoS 1 on connect.
|
||||
2. **Re-subscribe on reconnect** — broker retained message will deliver last known intent immediately.
|
||||
3. **Parse `desired_state`** and apply TV power action (`on` → power on / `off` → power off).
|
||||
4. **Deduplicate** using `intent_id` — if same `intent_id` received again, skip re-applying power command.
|
||||
5. **Check expiry** — if `now > expires_at`, treat as stale and fall back to `off` until renewed.
|
||||
6. **Ignore unknown fields** — for forward compatibility with Phase 2 additions.
|
||||
7. **Do not use per-client topic** in Phase 1; only group topic is active.
|
||||
|
||||
---
|
||||
|
||||
## Timestamps
|
||||
|
||||
- All timestamps use **ISO 8601 UTC with Z suffix**: `"2026-04-01T06:00:03.496Z"`
|
||||
- Client must parse as UTC.
|
||||
- Do not assume local time.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 (deferred — not yet active)
|
||||
|
||||
- Per-client intent topic: `infoscreen/{client_uuid}/power/intent`
|
||||
- Per-client override takes precedence over group intent
|
||||
- Client state acknowledgement: `infoscreen/{client_uuid}/power/state`
|
||||
- Listener persistence of client state to DB
|
||||
Reference in New Issue
Block a user