fix(media-player): ON means no power or playback fact reported (t58 UNIT 0)

The t59 F2 ruling, implemented. The ladder said ON was for "no source reported
anything at all" but tested for something looser — no transport data — so a
source that ANSWERED "not playing" still fell through to ON. A source that
answers is not a source that is silent, and `on` is the "we cannot know" state.

The ladder now tracks an explicit `reported` flag, set by every source that gave
us a power or playback fact, and the last tier is `IDLE if reported else ON`.
Each case the ruling names now lands on IDLE:

  * a configured Jellyfin entity in ANY state — idle, off, standby, unavailable,
    or even absent from the state machine. It reports "not playing" without an
    early return, so a silent Jellyfin still cannot mask a live media session
    below it (both directions pinned);
  * a live media session that carries no playback flag — the app publishes a
    session but is not playing, paused or buffering;
  * a bare foreground app: either transport's answer counts (the protocol's
    current_app push or the ADB mResumedActivity parse);
  * a configured power entity, whatever it currently says;
  * protocol is_on / ADB awake, plugged or not.

`on` survives only for an entry where none of those exists. The t55 rules are
untouched: an off power entity still outranks a playing session, and no source
may mask playing/paused/buffering.

The module docstring's tier 6, the `state` docstring and the README's ladder
(moving "no data" to the precise wording) all say the same thing now, and the
README's one-line state table was aligned with it.

RED PROOF (the new tests against the parent tree): five of the reporting cases
read `on` where they must read `idle` — `assert 'on' == 'idle'` — plus the
"ON is unreachable with any source configured" shape test.

SIMULATED vs EXECUTED: no real Shield was available, so these are the documented
source shapes, not captures; the state=6 → buffering mapping stays marked
inferred. No browser exists either, so the README pins are textual and the
production behaviour is exercised against real HA's state machine in
tests/test_ha_api_drift.py.
This commit is contained in:
James Mackay committed 2026-10-12 08:04:06 +01:00
1 parent ff8dd1c454
commit ef42682250
4 files changed
+228 -30

No files matched your search

+18 -6
View File
@@ -75,11 +75,23 @@ remote protocol is the fast push/command path:
6. **Idle** — the device is powered (protocol `is_on` true, ADB awake, or the
power plug on) and nothing is playing, or ADB reports it asleep (standby).
A powered Shield with an app open and nothing playing reads `idle`.
7. **On** — **no source reported anything at all**: no transport connected, no
power entity, no Jellyfin, no media session. `on` is the "we cannot know"
state and the only situation it describes — it never appears while any
source reported a power or media state, and never masks a
`playing`/`paused`/`buffering` source.
7. **On** — **no source reported a power or playback fact** (t58 UNIT 0; the
earlier wording said "no data", which was too loose). `on` is the "we cannot
know" state and the only situation it describes, so every source that
*answers* — including an answer of "not playing" — stops the ladder at
`idle` at worst, never here:
- a configured **Jellyfin** entity in **any** state: `idle`, `off`,
`standby` and `unavailable` all mean "its session is not playing", so they
report — and because the ladder keeps going, a silent Jellyfin still
cannot mask a live media session below it;
- a **live media session with no playback flag** — the app publishes a
session but is not playing, paused or buffering;
- a **bare foreground app** — the protocol's `current_app` push or the ADB
`mResumedActivity` parse answered;
- a configured **power entity**, whatever it currently says;
- protocol `is_on` / ADB awake, plugged or not.
`on` survives only for an entry where none of those exists, and it never
masks a `playing`/`paused`/`buffering` source.
The foreground tier is what keeps an app's identity visible while the state is
`idle`: the `current_app` protocol push (then the ADB `mResumedActivity`
@@ -185,7 +197,7 @@ What each retired piece used to provide, and what covers it now:
| Retired piece | Covered by |
|---------------|------------|
| `androidtv` playback state | the ADB media-session state ladder (`playing` / `paused` / `buffering` / `idle` / `off`, plus `on` only when no source reported anything) |
| `androidtv` playback state | the ADB media-session state ladder (`playing` / `paused` / `buffering` / `idle` / `off`, plus `on` only when no source reported a power or playback fact) |
| `androidtv` app name / `app_id` lookups | the manager's store + the app-name map + the manifest label of the app that is currently on screen (fetched on demand; a failed fetch is retried on the next poll instead of being cached as a permanent miss) |
| `androidtv_remote` power, volume and key commands | the protocol transport: the `remote` entity, `media_player` volume, `KEYCODE_*` commands |
| `androidtv_remote` availability | the availability contract (protocol `is_on`, ADB awake, power switch) |