fix(discovery): a sweep that could not run is not an answer (t62 F1)

The installed-apps sweep stamped an EMPTY answer on every failure path, so a
Shield that was asleep at the entity's startup sweep was cached as "0 apps" for
APP_LIST_TTL (6 h): `homeassistant.update_entity` then issued zero shells and
the sensor kept reporting 0/"pending" until the TTL or the Configure row's
force. A genuinely empty device was indistinguishable from it, so "ok" was
unreachable for an empty list.

Only a sweep that RAN stamps the cache now. Every failure records the outcome
as `unknown` (`_apps_unavailable`), stores no table, and is retried by the next
non-forced refresh; a sweep that reached the device and failed is spaced by
APP_SWEEP_RETRY_SECONDS (30 s, force=True bypasses) so a silent socket is not
hammered once per poll, while an unavailable transport costs nothing and is
retried immediately. `installed_apps_status` exposes ok/unknown/pending, the
sensor surfaces it, and "ok" + an empty table is now the honest empty answer.
Docstrings, the README vocabulary and the docs on what a refresh actually
re-runs are corrected; the "update_entity re-runs only without a fresh table,
the Configure row forces one" wording stays true.

Red proof (pre-fix sources, current tests): the five new/updated tests fail on
HEAD, e.g. `installed_apps_stale` is False after the ADB-down startup sweep, and
the sensor reports 'pending' where it must say 'unknown'.

Same class already fixed for app labels: a fetch that could not run is not an
answer. Also records that the `-3`/resolve-activity/pm-dump output shapes are
documented, not observed.
This commit is contained in:
engineer committed 2026-10-12 08:19:10 +01:00
1 parent ef42682250
commit c05e6fa929
4 files changed
+217 -43

No files matched your search

+19 -7
View File
@@ -448,6 +448,12 @@ not launchable apps — with the one exception that matters: the active launcher
is a system app yet is exactly what the user sees. Nothing else is filtered, so an app
is missing only if Android itself does not report it.
*(Every output shape above is the **documented** Android one, not an observed
capture: no real Shield was available when this was written, so
`pm list packages -3`, `resolve-activity --brief` and `pm dump` are exercised from
fixtures — the same "inferred, not observed" mark the state=6 buffering value
carries in the source ladder above.)*
Names come from the same cached path the Current App sensor uses: a `pm dump` per
package, parsed for `application-label`, cached for 24 h. A full sweep runs the dumps
in a small burst (4 at a time, 50 ms apart), which finishes a typical Shield in
@@ -466,16 +472,22 @@ and `mdi:television` otherwise. The icon never enters your **icon column**: that
is a picture URL or a `camera.*` entity and feeds `entity_picture`, so writing an
`mdi:` name there would put a value the media player cannot use into your own map.
Discovery never runs on a state poll: it happens when you press the row, and the result
is cached for 6 h. With ADB off, or unreachable, the table simply shows the rows you
wrote yourself and the menu says why.
Discovery does not run on the watchdog: the Configure row runs it, the dashboard
sensor refreshes the table when it has no fresh one, and the result is cached for 6 h.
With ADB off, or unreachable, the table simply shows the rows you wrote yourself and
the menu says why. A sweep that **could not run** is not cached as an answer: it is
recorded as `unknown` and retried, so a Shield that was asleep at startup is picked up
by the next refresh instead of reporting an empty device for six hours.
**For dashboards** the same table is a sensor: `sensor.<name>_installed_apps`, whose
state is the app **count** and whose `installed_apps` attribute is the list of
`{package, name, icon}`. Its `discovery` attribute says `ok`, `pending`, `not_connected`
or `adb_disabled`. It only exists for ADB-enabled entries, and it is cheap to refresh —
`homeassistant.update_entity` on it re-runs the sweep only when the cache has expired,
while the Configure row forces one.
`{package, name, icon}`. Its `discovery` attribute says `ok`, `pending`, `unknown`,
`not_connected` or `adb_disabled`: `ok` means the table IS the answer — including a
genuinely empty one — and `unknown` means the last sweep could not run (an empty list
then says nothing about the device, and the next refresh retries). It only exists for
ADB-enabled entries, and it is cheap to refresh — `homeassistant.update_entity` on it
re-runs the sweep only when there is no fresh table (the cache expired, or the last
sweep could not run), while the Configure row forces one.
```yaml
# every installed app, with the name this integration would show