Files
infoscreen/TV_POWER_INTENT_SERVER_CONTRACT_V1.md

164 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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