Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions specs/700-ha-control/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
**Created**: 2026-06-23

**Status**: Active — **amended 2026-07-21** (frame identity, US3 / FR-700-16…22 / SC-700-11…14).
The identity half is **release-blocking for the first public release**; see US3.
The identity half is **release-blocking for the first public release**; see US3. **Further
amended 2026-07-26** (in-app UI presentation is not connectivity loss, FR-700-23 / SC-700-15).

**Input**: Consolidated from `specs/005-hacontrol/spec.md`: secure MQTT connection, Home Assistant discovery, availability, and pause/play control for the running slideshow.

Expand All @@ -14,6 +15,17 @@ The identity half is **release-blocking for the first public release**; see US3.
("stable, duplicate-free device and entity IDs"). US3 and FR-700-16…22 below make that
requirement precise enough to be testable, and separate *identity* from *name*.

**Amendment 2026-07-26 — in-app UI presentation is not connectivity loss.** Live verification on
the physical frame showed Home Assistant reporting the frame **offline** whenever any in-app
modal UI (Settings, the album browser, source setup, the connection-error editor) was presented
over the slideshow, even though the app stayed foregrounded and connected; dismissing the UI
reconnected everything. FR-700-23 below makes explicit that availability tracks app-level
connectivity, not which in-app view is frontmost. The same defect was also found re-arming the
system idle timer while such UI is open, which already violates FR-400-01 (400-power-manager: the
display MUST stay awake while the slideshow is active in the foreground) — that spec needs no new
text, but the fix for FR-700-23 MUST restore both the broker session and the idle timer together,
since both regressions share one root cause.

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Pause/play from Home Assistant with availability (Priority: P1)
Expand Down Expand Up @@ -100,6 +112,7 @@ that Home Assistant shows the same device and entity IDs.
- **Missing or invalid credentials**: If no valid broker configuration exists, no connection is attempted and the app continues locally.
- **Connection drop during operation**: LWT marks the device offline; after reconnect, the app republishes online availability and the current state.
- **App in the background**: Commands requiring foreground-only capabilities are not forced in the background; platform boundaries are respected through the relevant module.
- **In-app modal UI over the slideshow** (Settings, album browser, source setup, the connection-error editor): the app is still foregrounded and connected, so this is not backgrounding — availability stays online and the broker session is left alone (FR-700-23).
- **Conflicting or rapid commands**: The last valid command wins, and echoed state always reflects the real app state.
- **Duplicate discovery**: Repeated discovery publication does not create duplicate Home Assistant devices or entities because IDs are stable and unique.
- **Secret leak**: Broker username and password never appear in logs, UserDefaults, cache, source code, or committed files.
Expand All @@ -112,7 +125,7 @@ that Home Assistant shows the same device and entity IDs.
- **FR-700-01**: The app MUST connect to the configured MQTT broker as a client over TLS, and TLS validation MUST remain enabled.
- **FR-700-02**: Broker credentials MUST come from the Keychain-backed broker configuration only and MUST never be logged, cached, committed, or stored in UserDefaults.
- **FR-700-03**: If broker configuration is missing, invalid, or connection fails, the app MUST keep running locally without crashing or blocking the slideshow.
- **FR-700-04**: On connect, the app MUST publish online availability and register a Last Will and Testament so the broker marks the device offline on unexpected disconnect.
- **FR-700-04**: On connect, the app MUST publish online availability and register a Last Will and Testament so the broker marks the device offline on unexpected disconnect (a real loss of app-level connectivity — see FR-700-23 for what does not count).
- **FR-700-05**: After disconnect, the app MUST attempt reconnect and, on success, reannounce online availability and current state.
- **FR-700-06**: The app MUST register through Home Assistant MQTT discovery using stable, duplicate-free device and entity IDs.
- **FR-700-07**: The app MUST expose a pause/play switch entity whose availability follows the app's online/offline availability.
Expand All @@ -137,13 +150,28 @@ to the constitution's rule that nothing secret enters UserDefaults.)*
- **FR-700-21**: On the first run of a build implementing FR-700-16, a frame that is already registered MUST adopt its current identity rather than minting a new one, so no existing Home Assistant entity is orphaned by the upgrade itself.
- **FR-700-22**: The user MUST be able to set a human-readable frame name that determines the Home Assistant display name. Changing it MUST NOT change frame identity, and MUST NOT orphan, duplicate, or rename any entity ID.

*(FR-700-23, added 2026-07-26, closes the gap the same-day observed defect exposed: nothing above
said what "unexpected disconnect" excludes.)*

- **FR-700-23**: Availability (the online/offline state on the availability topic, backed by the
LWT) MUST reflect app-level connectivity only — the app is running in the foreground with a
live broker session. Presenting any in-app modal UI over the slideshow (Settings, the album
browser, source setup, the connection-error editor, or any other sheet/full-screen surface)
MUST NOT publish offline, MUST NOT disconnect or reconnect the broker session, and MUST NOT
re-arm the LWT. Availability MUST transition to offline only on an actual loss of app-level
connectivity — the app leaving the foreground (backgrounded or terminated) or a genuine
broker/network failure — never on which in-app view happens to be frontmost. Whether the
slideshow surface itself is frontmost behind such UI is a separate, finer-grained signal carried
by its own diagnostic sensor (710's `frame_status`) — it MUST NOT be encoded as a third value on
this topic, which is HA-discovery-binary (online/offline only).

### Key Entities *(include if feature involves data)*

- **Broker Configuration**: Host, port, username, and password supplied by broker setup; credentials originate from the Keychain. It carries the frame identity for convenience but is **not its owner** — identity outlives any broker configuration (FR-700-16).
- **Frame Identity**: The opaque, per-frame, per-platform value that anchors every discovery payload, entity `unique_id`, topic namespace, and availability topic. Generated once, never derived from a platform identifier or a name, never displayed, never synchronised (FR-700-16…21). *(Previously "Device Identity"; renamed to make the split from Frame Name explicit.)*
- **Frame Name**: The human-readable label the user gives a frame, determining only its Home Assistant display name. Free-form, non-unique, changeable at any time, and never part of any key (FR-700-22).
- **Home Assistant Entity**: A remotely controllable capability with discovery configuration, command topic, state topic, and availability binding. In this active spec, the entities are pause/play (switch), brightness (dimmable light), and album select.
- **Remote Control State**: The app state echoed to Home Assistant, including running or paused and online or offline.
- **Remote Control State**: The app state echoed to Home Assistant, including running or paused and online or offline. Online/offline here is app-level connectivity only (FR-700-23); which in-app view is frontmost is a separate signal, not encoded here (see 710's `frame_status`).
- **MQTT Transport**: The injectable protocol boundary for publishing, subscribing, connecting, reconnecting, and LWT behavior.

### Roadmap / Deferred (not yet built)
Expand Down Expand Up @@ -174,6 +202,7 @@ photo navigation, current-photo image/metadata, and diagnostics.)*
- **SC-700-12**: No two frames ever share a topic namespace or an entity `unique_id`, including when the platform identifier is unavailable to both.
- **SC-700-13**: Renaming a frame changes only its Home Assistant display name: every entity ID, dashboard binding, and automation referencing it keeps working.
- **SC-700-14**: Updating a frame from a build predating FR-700-16 leaves its existing Home Assistant entities in place and unchanged.
- **SC-700-15**: Presenting any in-app modal UI over the running slideshow (Settings, album browser, source setup, connection-error editor) does not publish offline availability and does not disconnect the broker session — Home Assistant shows the frame online throughout — verified through the injected MQTT transport.

## Assumptions

Expand Down
5 changes: 4 additions & 1 deletion specs/710-ha-full-control/contracts/ha-mqtt-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ depicted lingers on the broker; both are republished on (re)connect/announce ins
| `version` | sensor (diagnostic) | app version string | bundle |
| `battery` | sensor (diagnostic) | integer 0–100, `device_class: battery`, unit `%` | UIDevice.batteryLevel |
| `charging` | binary_sensor (diag) | `ON`/`OFF`, `device_class: battery_charging` (ON = on power) | UIDevice.batteryState |
| `frame_status` | sensor (diagnostic) | `running`\|`inactive` | explicit UI-visibility signal (2026-07-26, FR-710-24) |

Enabled by default: all except `current_photo_image` (opt-in — FR-710-15, `HAPublishOptions`).
`battery` and `charging` are published only on battery-bearing devices (absent on Apple TV, which
has no battery — FR-710-23).
has no battery — FR-710-23). `frame_status` is read-only, free-tier telemetry (FR-1100-03a),
orthogonal to `phase` — added 2026-07-26 alongside the 700 amendment FR-700-23, which is what
availability itself now excludes (in-app UI presentation is not a connectivity change).

## 3. `current_photo` payload (research.md §2)

Expand Down
42 changes: 34 additions & 8 deletions specs/710-ha-full-control/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ changes.

> **Purchase-gate tiering (per spec 1100, amended 2026-07-20).** The entities defined here
> split across tiers: the **read-only sensor entities** (current photo + metadata, current-photo
> image, playback phase, photo count, version, and — on battery-bearing devices — battery level
> and charging state) plus broker connection + availability (LWT) are
> image, playback phase, photo count, version, frame status, and — on battery-bearing devices —
> battery level and charging state) plus broker connection + availability (LWT) are
> **free** — an unentitled frame publishes them so Home Assistant can *see* it. The
> **controllable entities** (everything with a `command_topic`: brightness/light, album select,
> playback switch, the settings controls, next/previous) and all command handling require the
Expand Down Expand Up @@ -115,9 +115,9 @@ update; verify pressing while paused steps without resuming.

### User Story 4 - Diagnostics & state on reconnect (Priority: P3)

HA shows diagnostic sensors (slideshow phase, photo count of the active album, app version, and —
on battery-bearing devices — battery level and charging state) and after any reconnect the full
state of *all* entities is re-published, so HA never shows stale values.
HA shows diagnostic sensors (slideshow phase, photo count of the active album, app version, frame
status, and — on battery-bearing devices — battery level and charging state) and after any
reconnect the full state of *all* entities is re-published, so HA never shows stale values.

**Acceptance Scenarios**:

Expand All @@ -133,6 +133,10 @@ state of *all* entities is re-published, so HA never shows stale values.
state changes, **Then** the battery sensor and the charging binary sensor update (event-driven,
no polling); on a device without a battery (e.g. Apple TV) these two entities are absent from
discovery.
5. **Given** the app is connected and foregrounded, **When** an in-app modal is presented over the
slideshow, **Then** the `frame_status` sensor changes from `running` to `inactive`, while
`phase`, `playback`, and availability are unaffected; dismissing the modal returns it to
`running` (see the 700 amendment, FR-700-23, for why this must not touch availability).

### Edge Cases

Expand Down Expand Up @@ -242,6 +246,23 @@ Numbering continues the 700 series in the `710` sub-spec block.
notifications), not by polling. On a device without a battery (e.g. Apple TV) both entities MUST
be omitted from discovery rather than published with a placeholder.

*(FR-710-24, added 2026-07-26, alongside the 700 amendment of the same date — FR-700-23 — which
fixes the connectivity/UI-visibility conflation this sensor separates out.)*

- **FR-710-24**: The app MUST expose a read-only diagnostic sensor `frame_status` with exactly two
values: `running` (connected, and the slideshow surface is frontmost, uncovered by any modal) and
`inactive` (connected and foregrounded, but a sheet or other in-app modal covers the slideshow).
`frame_status` MUST be driven by an explicit UI-visibility signal from the presenting layer, not
inferred from view-appear/disappear lifecycle — the same inference that caused the connectivity
defect FR-700-23 fixes. Like the other diagnostic sensors, it shares the app's availability
binding, so when the app itself goes offline (FR-700-23) the entity shows unavailable rather than
publishing a third value on its own state topic. It is `entity_category: diagnostic`, publishes
retained state, and carries no `command_topic`; being a read-only sensor, it is **free** telemetry
an unentitled frame still publishes, matching the tiering of `phase` and `battery`/`charging`
(FR-1100-03a). `frame_status` is orthogonal to and does not change `phase` (FR-710-07, unchanged:
`loading|playing|empty|failed`) or `playback` (FR-700-07/08): existing automations keyed on either
continue to see exactly the same values and semantics as before this amendment.

### Key Entities *(include if feature involves data)*

- **Settings Entity**: one HA entity per `ThemeSettings` field; command topic accepts the raw
Expand All @@ -250,9 +271,10 @@ Numbering continues the 700 series in the `710` sub-spec block.
(re)connect instead) and metadata (sensor state + attributes, also not retained, cached
per-asset in a bounded LRU for the session); lifecycle bound to `SlideshowViewModel`
photo-change events.
- **Diagnostics**: read-only sensors (`phase`, `photo_count`, `version`, `battery`) and the
`charging` binary sensor, marked `entity_category: diagnostic`; `battery`/`charging` appear only
on battery-bearing devices.
- **Diagnostics**: read-only sensors (`phase`, `photo_count`, `version`, `battery`, `frame_status`)
and the `charging` binary sensor, marked `entity_category: diagnostic`; `battery`/`charging`
appear only on battery-bearing devices; `frame_status` is `running`/`inactive`, driven by an
explicit UI-visibility signal, and is orthogonal to `phase`.
- **Publish Options**: image publishing enabled flag (default off), image source size, byte
cap — stored with the broker configuration (non-secret part).

Expand All @@ -277,6 +299,10 @@ Numbering continues the 700 series in the `710` sub-spec block.
binary sensor that reflect the device's actual battery level and charging state and update on
change without polling; on a device without a battery, neither entity is discovered — verified
with the fake transport (no real broker) and an injected battery source.
- **SC-710-08**: Presenting any in-app modal over the slideshow changes `frame_status` from
`running` to `inactive` (and back on dismissal) without changing `phase`, `playback`, or
availability — verified with the fake transport and an injected UI-visibility signal, no real
broker or simulator required.

## Open Questions

Expand Down
Loading