Files
infoscreen-dev/TV_POWER_INTENT_SERVER_CONTRACT_V1.md
RobbStarkAustria 82f43f75ba docs: refactor docs structure and tighten assistant instruction policy
shrink root README into a landing page with a docs map and focused contributor guidance
add TV_POWER_RUNBOOK as the canonical TV power rollout and canary runbook
add CHANGELOG and move project history out of README-style docs
refactor src README into a developer-focused guide (architecture, runtime files, MQTT, debugging)
prune redundant older HDMI docs and keep a canonical HDMI_CEC_SETUP path
update copilot instructions to a high-signal policy format with strict anti-shadow-README design rules
align references across docs to current files, scripts, and TV power behavior
2026-04-01 10:01:58 +02:00

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