The user ruling: this integration REPLACES both old integrations, so nothing may depend on an androidtv/androidtv_remote entity anymore. Removed: * the setup-form field + its entity selector (CONF_ANDROIDTV_ENTITY is no longer offered, only read by the migration that drops it); * the entity-derived unique_id derivation (_unique_id_from_atv_device, t29 G2) and, with it, every registry lookup for another integration's devices — the user step now sets NO unique_id at all; the protocol MAC learned during pairing is the identity; * the Jellyfin auto-detect's second tier (the atv entity's friendly_name) — it now uses the entry name the user typed or the device name from discovery; * the app-name and app-id tiers (media_player.app_name/app_id + the shared resolve_app_name chain, the Current App sensor, the sensor's state-change listener and its `_androidtv_state` helper) — the manager's store is the only source; * the "transitional external entity" command tier in every path: `_cmd` and the pan-media_player service call are gone, and so is the fallback in _media_cmd, _remote_key_cmd, the §7 channel-digit sequence and the intent self-heal retry (an intent no transport served now surrenders to the truth instead of delegating); * the entity-derived power signal (`_on_source_change`'s "device off clears the intent" check no longer watches another integration's entity; the power LADDER — protocol is_on → ADB wake → power switch — is unchanged). turn_on/turn_off are native now: they send the protocol POWER key and, like the remote entity, send nothing when the known state already matches (no superfluous toggle). An ADB-only entry (protocol skipped) has no power write path — the manager owns the socket and exposes no device-keyevent, which is what the old delegation used to cover. helpers.discover_adb_host — the one helper that derived a host from another integration's config entry — is deleted; button.py (granted to this task) reads the entry's own address fields instead of the entity, so the Pair ADB button keeps working. Present-day entries that still carry the key are unaffected (nothing reads it); the v3 migration that drops it lands with the host unification. Verified: 309 passed / 2 skipped, 310 / 1 with androidtvremote2, ruff clean, drift engine exit 0 on HEAD (13 modules, 58 imports, 35 call arguments, 6 awaited HA calls) and 16 passed in the real-HA drift suite. The stub harness's legacy service recorder became a watchdog: any HA service call from the media player now fails the routing tests instead of being the expected outcome.
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 accepts any one of the three state sources — the Android
TV Remote entity, the Nvidia Remote Protocol (PIN pairing), or ADB
media-session mode (ADB enabled + host). The no_state_source form error
fires only when none of the three is present; ADB-only setups are valid.
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:
nvidia_shield:
- name: Living Room
adb_host: 10.0.0.31 # or protocol_host for the PIN-pairing hint
adb_port: 5555
protocol_host is a prefill hint only (the PIN pairing still happens in
the flow). Blocks without protocol/adb hosts are ignored with a warning.
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.
Migration from mediasession2mqtt + universal-player glue
The mediasession2mqtt bridge sensors and the universal media player YAML
templates the fork replaces are retired by the ladder above: the media player
entity already carries the DAG attributes (media_dag_running_apps with
foreground/last-known enrichment, media_dag_now_playing with its source
tag) that media-zone orchestration consumed from those sensors. See the
appendix in docs/RESEARCH_SHIELD.md for the
retirement mapping and the universal-player glue it replaces.
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"
- Select the Android TV Remote entity for your Shield
- Enable ADB if you want now-playing from all apps
- If ADB is enabled, the next step will pair — approve the prompt on the Shield
- 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 hosts are updated when the address 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
- ADB Host/Port — override auto-detection if needed
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