# 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": "", "group_id": , "desired_state": "on" | "off", "reason": "active_event" | "no_active_event", "issued_at": "", "expires_at": "", "poll_interval_sec": , "active_event_ids": [, ...], "event_window_start": "" | null, "event_window_end": "" | 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