The README now answers the question a migrator actually has: what do I delete, and what covers it afterwards. * a new "Replacing androidtv / androidtv_remote + the glue" section: the back-up → remove-the-platform-YAML → remove-mediasession2mqtt → delete the old integrations → add this one → update automations checklist, plus a table mapping every retired piece to what serves it now. It notes that Wholphin publishes no MediaSession of its own and gets its now-playing from the ADB media session, so no bridge is needed for it either; * the setup steps describe the real flow (name + one address, both transports on by default, ADB pairing then PIN pairing, each with a skip); * the YAML seed section carries a complete, copy-pasteable block. It documents ONLY the keys `_yaml_seed_blocks` honours (name, host, adb_port, adb_enabled, and the still-accepted protocol_host/adb_host) and says explicitly that the rest lives on the Configure page — an example that lists ignored keys is worse than no example; * the state-ladder section, the discovery bullet and the options list follow the two-transport, one-address reality. `tests/test_readme.py` pins the doc/code contract: the example's keys are a subset of what the seed reads, the example uses the current `host` key, and the guide keeps naming the retired integrations and glues. Verified: 326 passed / 2 skipped, ruff clean.
NVIDIA Shield
Home Assistant integration for the NVIDIA Shield TV — ADB media session tracking for now-playing state from any app, plus Google remote-protocol commands, Jellyfin rich metadata, and power monitoring.
This is a Shield-only fork of ltomes/ha-nvidia-shield
(early 0.1.x), rebuilt around a different architecture: one integration, one device,
ADB as the state source, and no dependency on separate androidtv / androidtv_remote
integrations.
Status: early fork, actively rebuilt. Releases are date-based (
v2026.10.08). Expect rough edges until the MVP phases land. The build-out plan is indocs/MVP_PLAN.md; device research is indocs/RESEARCH_SHIELD.md.
What we're building (the fork's goals)
The fork exists to make a single integration own the Shield end-to-end:
- Event-driven ADB media session tracking as the primary playback state source —
dumpsys media_sessionover a persistent connection, not screen capture, not 10-second polling. - Fallback state for apps without media sessions — e.g. TiviMate publishes
none at all; foreground-app detection (
mResumedActivity) + last-known-state- the "audio playback" hint from the same session dump close that gap.
- All-in-one device — absorb the Google remote protocol (
androidtvremote2): aremoteentity alongside the media player,play_mediawithmedia_content_type: channelfor TiviMate channel-by-number, absolute volume, focused-app push events. ADB stays the playback-state source; ADB pairing stays intact. - Retire the surrounding glue — the
mediasession2mqttbridge, universal media player templates, and external remote entity reading all become native integration behavior.
Phases 0–4 with files, risks, and definitions of done live in
docs/MVP_PLAN.md.
Current features (inherited from upstream 0.1.x)
- ADB Media Session — now-playing from any app that publishes a session (Spotify, Netflix, Plex, Wholphin, …)
- Jellyfin rich metadata — title, series/episode, position, stream details (resolution, codec, HDR, audio format) via the HA Jellyfin integration's cache
- Power monitoring — smart plug for reliable on/off with auto-discovered sibling sensors (watts, voltage, current)
- Intent tracking — commands reflect in HA state immediately, self-heal against actual device state
- Privacy mode — show only app name without media details for non-Jellyfin apps
- Media DAG attributes —
media_dag_*attributes for media-zone style orchestration
Changes inherited from upstream still apply in 0.1.x code: polling is timer-based, the ADB connection lives inside the media player entity, and fallback app state arrives in Phase 3.
State ladder and fallback semantics (Phase 3)
The media player state is decided top-down; each tier serves only when the
ones above it are silent. Both transports live in the entry's
ShieldConnectionManager (connection.py) — ADB is the state source, the
remote protocol is the fast push/command path:
- Plug off — the power switch says
off→off. - Protocol power push (
is_on) —False→off;Nonefalls through. - ADB wake (exact
mWakefulnessparse) — asleep →idle(standby). - Intent — a just-sent command's target state, self-healing against reality within 30 s (retry goes over the transport that served it).
- Jellyfin —
playing/pausedwhile a Jellyfin session is active. - ADB media session —
playing/pausedfor apps that publish a session (foreground app preferred). - Foreground fallback — an app is open (
current_appprotocol push, then the ADBmResumedActivityparse) but publishes no media session →on. Its identity comes from the TTL-capped last-known-state cache, the last channel entered, or the zero-cost "audio playback" hint (media recently active) — never resurrectingplayingpast the 60 s state TTL. - On — powered, awake, nothing more specific.
TiviMate (ar.tvplayer.tv) is the documented no-session case: it publishes
no MediaSession on Shield Android 11 (RESEARCH_SHIELD R2), so tiers 6–7 plus
the audio hint are what keep it visible.
Standalone setup offers both state sources at once — the Nvidia Remote
Protocol (PIN pairing) and ADB media-session mode — enabled by default, with the
Shield's address pre-filled when discovery found it. Either can be turned off,
and each pairing step can be skipped (that transport stays off until you pair
it), so ADB-only, protocol-only and fully passive entries are all one click
apart. The no_state_source form error fires only when neither is enabled.
There is no other integration in the loop: one address, two transports, and the integration reads the device itself.
Channel entry options
The options flow exposes the §7 channel behavior the integration replicates:
the inter-digit delay (0.05–1.0 s, default 0.1) and the optional
ENTER press after the digits (off by default — TiviMate's validated
behavior). Both are consumed by play_media with
media_content_type: channel.
YAML seeding (optional, seed-only)
An optional nvidia_shield: block in configuration.yaml seeds an
interactive setup flow — nothing is created without the user walking the
standard flow, and pairing stays interactive exactly as with UI setup. The UI is
the primary path; the block exists so a provisioning/fleet setup can supply the
address and the transport choice.
A complete, copy-pasteable block (every key is optional except an address):
nvidia_shield:
- name: Living Room # the entry's title (default: NVIDIA Shield)
host: 10.0.0.31 # the Shield's IP — the one address field
adb_port: 5555 # optional; the standard ADB port
adb_enabled: true # optional; the block's shape decides (see below)
host is the current key. protocol_host and adb_host are still accepted
(protocol first, then ADB) so an existing configuration.yaml keeps seeding:
the address is a prefill hint either way, and the PIN/ADB pairing still happens
in the flow. adb_enabled defaults to what the block implies — an address under
host/adb_host marks ADB as part of the block, a protocol-only block leaves it
off — and an explicit adb_enabled wins over the shape. Blocks without any
address are ignored with a warning.
The seed carries the address, the ADB toggle and the entry's name; everything
else (Jellyfin, receiver, power switch, app name/icon maps, channel timing, the
adb_command service) is set on the entry's Configure page after setup, or
during the interactive flow.
Duplicate guards: a block matching an existing entry's host (or its
virtual yaml:… unique_id) aborts and never re-seeds a UI-configured
host. Removing the YAML block creates or deletes nothing — entries live
in the storage registry and require user consent to remove. Deleting/re-
adding is the only way to move an imported entry between hosts; the UI is
the contractual path (YAML is the seed, the flow is the truth).
Raw adb_command service (opt-in)
When enabled in the integration's options (adb_command_enabled, default
off), the integration registers the nvidia_shield.adb_command service:
send a raw ADB shell command to the entry's persistent connection
({"command": "your-command", "entity_id": "media_player.<yours>"} — the
entity is optional when exactly one entry provides the service). This is the
same security posture as the Home Assistant core androidtv integration: a
documented, opt-in escape hatch for debugging and automation, registered at
setup and unregistered when the last participating entity unloads. It
rejects (logs a warning, sends nothing) when the ADB connection is down.
Replacing androidtv / androidtv_remote + the glue
This fork replaces the old androidtv media player, the androidtv_remote
integration and the surrounding YAML glue — there is nothing to keep alongside
it: the entry's own two transports cover the whole device.
- Back up your
configuration.yamland note the Shield's IP address. - Remove the old platform YAML — the
media_player:blocks usingplatform: androidtvorplatform: androidtv_remote, anyuniversalmedia player template that fanned them in, and theremote:/switch:shims you added around them. - Remove the
mediasession2mqttbridge — its sensors and the MQTT topics feeding them are replaced by this integration (table below). - Delete the old integrations in Settings > Devices & Services, then restart Home Assistant.
- Add this integration — Settings > Devices & Services > Add Integration >
"NVIDIA Shield" — or seed it from
configuration.yamlwith the block above, then walk the pairing steps. - Update your automations and dashboards to the new entities:
media_player.<entry>,remote.<entry>,sensor.<entry>_current_app,sensor.<entry>_now_playing, and themedia_dag_*attributes.
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 / idle / off) |
androidtv app name / app_id lookups |
the manager's store + the app-name map + the manifest label of the running app |
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) |
universal media player templates |
the media player entity's own attributes plus the media_dag_* attributes |
mediasession2mqtt sensors |
sensor.current_app, sensor.now_playing and the media_dag_* attributes |
Wholphin publishes no MediaSession metadata of its own; its now-playing comes from the ADB media session — the same tier every other app uses — so it needs no bridge either.
The appendix in docs/RESEARCH_SHIELD.md holds the
full retirement mapping this section summarises.
Releases
- Date-based versioning:
vYYYY.MM.DD(e.g.v2026.10.08). - Pushes to
masteron Gitea run the release workflow (.github/workflows/release.yml):- A plain push publishes the current day's release (same-day pushes are deduped).
- Bumping
versionincustom_components/nvidia_shield/manifest.jsonforces that specific version's release before the date fallback kicks in.
- Releases land on Gitea with the tag and a
nvidia_shield.zipasset for manual installs. (No automatic mirroring — see the HACS note below.)
Installation
HACS
HACS only works with public GitHub repositories, so the Gitea repo cannot be added to HACS directly. If you set up a public GitHub mirror of the releases, add it as a custom repository:
- In HACS, open the overflow menu (⋮) → Custom repositories.
- Paste
https://github.com/notexpectedyet/ha-nvidia-shield, pick type Integration, click Add. - Find NVIDIA Shield in the HACS list and click Download.
- Restart Home Assistant.
Manual
Copy custom_components/nvidia_shield to your HA config/custom_components/
directory, restart, then add the integration from Settings → Devices & Services.
Configuration
- Settings > Devices & Services > Add Integration > "NVIDIA Shield"
- Give it a name and its address — the Shield's IP, pre-filled when discovery found the device
- Both transports are on by default: Read state over ADB (now-playing from every app) and Add the Nvidia Remote Protocol (power, availability, app push, volume readout). Uncheck what you do not want.
- If ADB is enabled, the next step pairs it — approve the prompt on the Shield
- The remote protocol pairs next — type the PIN shown on the Shield, or continue without pairing (that transport stays off until you pair it)
- Optionally configure Jellyfin, receiver, power switch
ADB Pairing
ADB is paired during setup via a dedicated config flow step. If you need to re-pair later (e.g. after factory reset), use the "Pair ADB" button on the device page in HA.
Requirements: Settings > Developer options > Network debugging enabled on the Shield.
Discovery (zeroconf)
Home Assistant can discover a Shield through the _androidtvremote2._tcp
service, and the discovered address is pre-filled on the setup form. The dedup
rules are Home Assistant's own, and they differ with what the discovery carries:
- with the device MAC, a duplicate aborts as "already configured", and an existing entry's address is updated when it changed;
- without a MAC, the flow follows HA's stricter
_async_handle_discovery_without_unique_idcontract: any existing entry for this integration aborts the discovery, and only one discovery flow runs at a time.
So a second (or third) Shield that advertises no MAC has to be added manually — Settings > Devices & Services > Add Integration > "NVIDIA Shield" — rather than by discovery. That is a deliberate, tested contract (t37 D3), not a bug: the helper the flow calls is a coroutine and its dedup only runs when awaited.
Options
- Jellyfin Media Player — auto-detected or manually selected
- Receiver + Input — for media-zone coordination
- Power Switch — smart plug entity
- Read state over ADB / Add the Nvidia Remote Protocol — turn either transport on or off
- Shield address / ADB port — the Shield's IP, and the ADB port if you changed it from 5555
Entities
| Entity | Type | Description |
|---|---|---|
| Media Player | media_player |
Main entity with playback controls and metadata |
Current app (sensor.current_app) |
sensor |
Currently running app |
Now playing (sensor.now_playing) |
sensor |
Formatted now-playing string |
| Pair ADB | button |
Initiate or re-pair ADB connection |
Development
- Repository: Gitea — git.notexpectedyet.com/notexpectedyet/ha-nvidia-shield, branch
master - CI/CD: Gitea Actions; every push to
masterreleases (see.gitea/workflows/release.yml). Auth uses the job-providedGITEA_TOKEN— no extra secrets required. - Shared ADB helpers are vendored at
custom_components/nvidia_shield/ha_adb_common/(trimmed in this fork); dev extras inpyproject.toml(uv sync --extra dev).
License
MIT