feat(config-flow,sensor): the discovered apps reach the UI and dashboards (t57)
SURFACES
- The Configure menu gains a **Discover installed apps** row. A row rather
than a form step, because a menu choice is dispatched with no user input:
it awaits the manager's bounded sweep and returns to the menu with a line
saying what was found. Discovery is therefore always explicit — never a
per-poll cost — and a failed sweep still renders the menu.
- The Apps table opens on the REAL app list: the stored rows first (kept
position, name and icon untouched) and then every discovered app, named
by its device label or its package id when the label fetch did not answer.
The user's entry for a package always wins over the device label.
- `sensor.<name>_installed_apps` (ADB-enabled entries only): state = app
count, `installed_apps` attribute = the same table, `discovery` attribute
= ok / pending / not_connected / adb_disabled. It polls through the
manager's TTL cache, so a poll on a fresh table issues no shell, and the
standard `homeassistant.update_entity` action refreshes it.
The discovered icon is deliberately NOT written into the icon column: that
column is documented as a picture URL or a `camera.*` entity feeding
`entity_picture`, and an `mdi:` name there would be a value the media
player cannot use. The icons are offered in the installed-apps attribute,
and the table's description names them.
strings.json + translations/en.json: the `installed_apps` entity name, the
Discover row label, and the menu description now carries {discovery}, so the
page says how many apps were found (or why none were).
README: an "App discovery" section documenting both commands, how system
and launcher entries are treated, the label/caching semantics, the icon
rule and its limits, the sensor surface and the fallback behaviour — and
that the source-list fallback stays the user's map, not the discovery.
This commit is contained in:
1 parent
ee3f2845af
commit
a56ec38170
5 files changed
+306
-14
No files matched your search
@@ -68,8 +68,10 @@ remote protocol is the fast push/command path:
|
||||
4. **Jellyfin** — `playing` / `paused` while a Jellyfin session is active.
|
||||
5. **ADB media session** — `playing` / `paused` / `buffering` for apps that
|
||||
publish a session (foreground app preferred). `buffering` is the media
|
||||
session's own PlaybackState 6 (`state=PlaybackState {state=6, …}`), and a
|
||||
buffering session advertises the playback controls exactly like `playing`.
|
||||
session's PlaybackState **6** (`state=PlaybackState {state=6, …}`) — the
|
||||
stock-Android buffering value, **inferred** from that line shape: no captured
|
||||
fixture here contains `state=6` and no real device was available. A buffering
|
||||
session advertises the playback controls exactly like `playing`.
|
||||
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`.
|
||||
@@ -416,12 +418,63 @@ A script that fails surfaces as a translated error on the service call that trig
|
||||
it (the command is not retried over protocol/ADB), because a silent no-op is the one
|
||||
outcome that leaves you guessing.
|
||||
|
||||
### App discovery (what is actually installed)
|
||||
|
||||
The Configure page can ask the Shield which apps it has, so the **Apps** table does not
|
||||
start from a four-entry guess. Pick **Discover installed apps** on the Configure menu
|
||||
and the table is rebuilt from two ADB queries:
|
||||
|
||||
| Command | What it returns | How it is treated |
|
||||
| --- | --- | --- |
|
||||
| `pm list packages -3` | every **third-party** (user-installed) package, one `package:<id>` per line | all of them become rows |
|
||||
| `cmd package resolve-activity --brief -c android.intent.category.HOME` | the **active launcher** (`com.google.android.tvlauncher/…MainActivity`) | its package is appended |
|
||||
|
||||
System apps are **not** enumerated — a Shield ships a few hundred of them and they are
|
||||
not launchable apps — with the one exception that matters: the active launcher, which
|
||||
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.
|
||||
|
||||
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
|
||||
seconds instead of the minutes a strictly serialised sweep would take. A label fetch
|
||||
that **fails is not cached**, so that app shows its package id in this table and the
|
||||
next sweep tries again — this is the same rule as the entity path, and it is why a
|
||||
transient ADB hiccup no longer sticks.
|
||||
|
||||
Icons are **best effort and explicitly heuristic**: `pm dump` does not expose an app's
|
||||
icon resource (an extracted APK's icon is a bitmap only its own code can resolve), so
|
||||
there is no device icon to read. Each app gets a curated `package → mdi:` icon where we
|
||||
know one — Netflix, YouTube, Kodi, Plex, Spotify, the launcher, TiviMate, Wholphin —
|
||||
and `mdi:television` otherwise. The icon never enters your **icon column**: that column
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
```yaml
|
||||
# every installed app, with the name this integration would show
|
||||
{{ state_attr('sensor.living_room_shield_installed_apps', 'installed_apps') }}
|
||||
| map(attribute='name') | list }}
|
||||
```
|
||||
|
||||
### Sources (the app list in a dropdown)
|
||||
|
||||
The media player's source list is your **Apps** map — the same map the Browse tree and
|
||||
app-name resolution use. With no map configured it falls back to a small built-in list
|
||||
(TiviMate, Wholphin, Netflix, YouTube), so a universal-media-player card always has
|
||||
something to show:
|
||||
something to show. Discovery fills the **Configure** table, not this fallback: the
|
||||
dropdown stays exactly the map you saved, so what a card offers is always what you
|
||||
chose. An app that discovery found and you want in the dropdown is one row to add.
|
||||
|
||||
```yaml
|
||||
# a media-player card, or anything that reads source_list
|
||||
|
||||
Reference in new issue
Block a user