Files
infoscreen/TV_POWER_INTENT_SERVER_CONTRACT_V1.md

5.3 KiB
Raw Permalink Blame History

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.

{
  "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)

{
  "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)

{
  "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