"Continue without protocol pairing" switched the transport off for good: the options flow carries no protocol fields, so nothing in the UI could ever turn it back on, and the skip copy promised nothing about it (t34 F1). The entry now answers Home Assistant's Reconfigure action: * async_step_reconfigure (audit G5) seeds the flow from the entry (title, adb host/port/enabled, androidtv entity for the host fallback) and re-runs the pairing step — the address is editable, the Shield shows a PIN, and _finish_reconfigure persists protocol_enabled=True plus the address that just paired (so the transport no longer depends on the ADB fallback), adopts the paired MAC when it differs, and updates + reloads the entry. HA derives the "reconfigure_successful" abort from the flow source. A Reconfigure visit that clicks skip aborts honestly with "reconfigure_skipped" (entry untouched), and an entry that is gone aborts with "reconfigure_entry_missing". * the skip data_description now states the truth — the transport stays off until you pair it — and names the later path (the integration's entry menu > Reconfigure). * a host corrected on the pairing form now DROPS the session started against the old address and restarts pairing there, so the typed address is authoritative instead of being paired against the stale one. * the success path no longer runs _abort_if_unique_id_configured for a flow that already owns an entry: core looks the domain up by unique_id, so re-pairing an entry aborted on itself as "already_configured" (this also unblocked reauth). The fresh-setup path is unchanged. * adjacent fix while wiring the same call: _finish_reauth passed `options_updates`, which core's async_update_reload_and_abort does not accept (the kwarg is `options`, REPLACE) — the reauth path would have raised TypeError in production. Both paths now pass `options` and the test stubs mirror the real signature. Tests: tests/test_config_flow_ux.py::TestProtocolReconfigure drives reconfigure from a protocol-disabled entry through to updated options (transport on, host persisted, other options kept, MAC adopted, reload recorded), a corrected host reopening the PIN step against the new address, the skip abort, and the missing-entry abort; TestCopyContract gained the F1b copy pin. Against HEAD's config_flow.py all four reconfigure tests fail (no async_step_reconfigure). Verified: uvx --with pytest-asyncio pytest tests -q → 281 passed, 1 skipped; uvx --with pytest-asyncio --with androidtvremote2 pytest tests -q → 282 passed; uvx ruff check custom_components tests → 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 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.
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