Two gate gaps let the t62 findings ship, and both are closed here. A pure-AST unreachable-statement pass (scan(): pass 6, entry point scan_unreachable()): in every statement block, a statement after return/raise/continue/break can never run. Per-block on purpose — an `else`, an except handler or a nested function is a different block and not dead, so the pass stays signal. Red proof: the pre-fix connection.py, saved from HEAD, is flagged as `connection.py:1113: unreachable-statement: return at line 1110 ends this block`; the live tree is clean. A new counter (statement_blocks, floor 500 of ~1102) keeps the "no findings" verdict from being a pass that stopped parsing. The call-argument floor was 20 while the walk counted 46, so deleting a flow-display owner from HA_CALL_OWNERS did not trip it. It is 44 now, and the suite proves the trip: removing async_show_menu yields 40 checked arguments and `only 40 call_args (floor: 44)`. Measured removals: show_form 25, abort 38, show_menu 40, create_entry 41, update_entry 41 all violate; the single-call-site async_update_reload_and_abort (45) is the one owner a count floor cannot watch, which is why the floor sits two below the count. Also (t62 nit a): the two comments that pinned the t40 entity-attribute finding to ``media_player.py:828`` now name the file and the attribute instead — the line had rotted to an unrelated property.
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 (
vYYYY.MM.DD, plusvYYYY.MM.DD.Nfor a second release in the same day) — see Releases for how a push becomes one. 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. - 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/paused/bufferingfor apps that publish a session (foreground app preferred).bufferingis the media session's PlaybackState 6 (state=PlaybackState {state=6, …}) — the stock-Android buffering value, inferred from that line shape: no captured fixture here containsstate=6and no real device was available. A buffering session advertises the playback controls exactly likeplaying. - Idle — the device is powered (protocol
is_ontrue, 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 readsidle. - On — no source reported a power or playback fact (t58 UNIT 0; the
earlier wording said "no data", which was too loose).
onis the "we cannot know" state and the only situation it describes, so every source that answers — including an answer of "not playing" — stops the ladder atidleat worst, never here:- a configured Jellyfin entity in any state:
idle,off,standbyandunavailableall mean "its session is not playing", so they report — and because the ladder keeps going, a silent Jellyfin still cannot mask a live media session below it; - a live media session with no playback flag — the app publishes a session but is not playing, paused or buffering;
- a bare foreground app — the protocol's
current_apppush or the ADBmResumedActivityparse answered; - a configured power entity, whatever it currently says;
- protocol
is_on/ ADB awake, plugged or not.onsurvives only for an entry where none of those exists, and it never masks aplaying/paused/bufferingsource.
- a configured Jellyfin entity in any state:
The foreground tier is what keeps an app's identity visible while the state is
idle: the current_app protocol push (then the ADB mResumedActivity
parse), with its title from the TTL-capped last-known-state cache, the last
channel entered, or the zero-cost "audio playback" hint
(media recently active) — never resurrecting playing past the 60 s state
TTL.
TiviMate (ar.tvplayer.tv) is the documented no-session case: it publishes
no MediaSession on Shield Android 11 (RESEARCH_SHIELD R2), so tiers 5–6 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 / buffering / idle / off, plus on only when no source reported a power or playback fact) |
androidtv app name / app_id lookups |
the manager's store + the app-name map + the manifest label of the app that is currently on screen (fetched on demand; a failed fetch is retried on the next poll instead of being cached as a permanent miss) |
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, andvYYYY.MM.DD.N(N from 2) for a second release in the same UTC day. - Pushes to
masteron Gitea run the release workflow (.gitea/workflows/release.yml):- A plain push publishes today's release. If that date is already tagged it takes
the lowest free
.N, so a gap left by an earlier failure is reused rather than skipped. - Setting
versionincustom_components/nvidia_shield/manifest.jsonto an untagged value releases exactly that version instead.
- A plain push publishes today's release. If that date is already tagged it takes
the lowest free
- The release commit. When the resolved version differs from the manifest, the
workflow writes it back, commits
chore(release): v<VERSION> [skip ci], pushes it tomaster, and tags that commit. It has to do this: HACS compares the installed version against the manifest on the default branch, so a release that only ever existed as a tag is offered to nobody (v2026.10.09 shipped that way).- Because the tag names the bot commit,
git describe --tagson a developer commit reports the previous release. That is expected, not a missing tag. - Bump it by hand instead and the tag lands on your own commit: set
versionin the manifest, commit, push. The workflow then has nothing to write back (it sees an untagged manifest version and tags your commit).
- Because the tag names the bot commit,
[skip ci]: the bot commit carries it, and the workflow's job additionally checks the commit subject (chore(release): …), so a write-back cannot start a second release. Both only recognise release commits — a normal commit whose message body merely mentions[skip ci]can still be skipped by Gitea's own handling, so keep the marker out of ordinary commit messages.- The workflow refuses to publish unless the default branch's manifest already carries the version it is about to tag, and fails loudly (with a "bump the manifest by hand" instruction and nothing tagged) if the write-back push is rejected, for example by branch protection.
- 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
The Configure page is a menu of sub-screens, not one long form. Home Assistant renders it as a list of rows; each row opens a short form and comes back to the menu, and only Save and close commits:
| Row | What it holds |
|---|---|
| Main setup | Shield address, ADB port, Read state over ADB, Add the Nvidia Remote Protocol, Show title and artist, Allow the adb_command action, Jellyfin player, power switch |
| Receiver | the receiver/soundbar entity and the input it uses |
| Apps | the apps table — one row per app (see below) |
| Scripts | one optional script per command, plus the remote-entity script, the channel digit delay and send-ENTER |
| Save and close | writes the merged options and reloads the integration |
Every field that used to be on the single page is still there, under one of those rows. Edits are merged per key, so visiting another sub-screen (or saving from a later one) never drops what an earlier one changed. Half-finished pages are not written: closing the dialog without Save and close leaves the entry as it was.
The setup flow follows the same shape: its first form is unchanged (name, host, transports, the linked entities), and after it the flow is a menu — Receiver, Connection, Set up ADB, Pair the remote protocol, Create the entry — so anything you want to correct is one row away instead of buried in a form you have already submitted. The two pairing rows appear only while that transport still needs pairing (a skip leaves its row there to retry; a successful pairing removes it), and the menu's description names what is still to pair.
The Apps table
The Apps row is an editable table. One app per line, in the documented format:
ar.tvplayer.tv=TiviMate
com.plex.hdx=Plex|http://192.168.1.10:9000/plex.png
com.netflix.ninja=Netflix
package=name is enough; an optional |icon after the name adds a picture (a URL, or
a camera.* entity id — see Sources below). The field always opens carrying the
current rows, so adding, renaming or deleting an app is an edit to that text:
- add a row — type a new line
- edit a row — change its name, icon or package id
- remove a row — delete its line
A blank field clears the table (and the integration falls back to its built-in app
list). A malformed row — no =, or an empty name — is rejected with an error on the
page rather than being half-saved.
A receiver may be a switch.* as well as a media_player.*: a soundbar or
speaker that only knows on and off is a valid receiver. (Driving that switch during
playback is a separate, later change.)
- Jellyfin Media Player — auto-detected or manually selected
- Receiver + Input — for media-zone coordination;
media_playerorswitch - 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
- Command scripts — one optional script per command, plus one for the remote entity; see below
Command scripts (per-command overrides)
Some commands need hardware this integration deliberately does not drive: a speaker switch that owns the volume, an AVR input that has to move with the Shield, a mute that has to hit two devices. For those, the Configure page takes a script per command:
| Command | Runs when | Variables passed |
|---|---|---|
turn_on, turn_off |
turn on/off, from the media player or the remote | — |
volume_up, volume_down |
volume step | — |
volume_set |
absolute volume (there is no native path — the protocol has VOLUME_UP/DOWN/MUTE only) | level |
volume_mute |
mute/unmute | is_volume_muted |
select_source |
source selection (the app list drives it natively; a script overrides) | app_name |
A command with a script calls the script instead of the protocol/ADB path, never
both. Left unset — the default — every command behaves exactly as it did before, and
clearing a selector on the Configure page restores that. Two of the seven appear as
services only when there is something to do: volume_set, which has no native path at
all, and select_source, which is offered once the integration has sources to offer
(the app list below) — or a script.
The scripts are ordinary Home Assistant scripts. This block covers all three awkward cases: a speaker switch owning the volume, an AVR owning the input, and a mute that has to hit the switch as well.
script:
shield_volume: # select as "Volume set script"
alias: Shield volume (speaker switch)
sequence:
- action: media_player.volume_set
target:
entity_id: media_player.living_room_speaker
data:
volume_level: "{{ level }}"
shield_source: # select as "Select source script"
alias: Shield source (AVR)
sequence:
- action: media_player.select_source
target:
entity_id: media_player.avr
data:
source: "{{ app_name }}"
shield_mute: # select as "Mute script"
alias: Shield mute (speaker switch)
sequence:
- choose:
- conditions: "{{ is_volume_muted }}"
sequence:
- action: switch.turn_off
target:
entity_id: switch.living_room_speaker
default:
- action: switch.turn_on
target:
entity_id: switch.living_room_speaker
Then set script.shield_volume as Volume set script, script.shield_source as
Select source script and so on on the Configure page. Scripts are set there, not
in the YAML seed above — the seed only carries the address and the transport choice.
A script that fails surfaces as a translated error on the service call that triggered 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.
(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
seconds instead of the minutes a strictly serialised sweep would take; the commands
share the watchdog's own ADB connection, and adb-shell serialises the I/O on it, so
the burst queues politely instead of interleaving on the socket. 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 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, 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.
# 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. 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.
# a media-player card, or anything that reads source_list
type: media-control
entity: media_player.living_room_shield
source_list— the app names. They come from the map (in the order it lists them), so editing the map renames the dropdown entries.source— the app currently on screen, resolved exactly like the Current App sensor: your map first, then the cached device label, then the raw package.media_player.select_source— picks one by name and launches it over the protocol, the same pathplay_mediauses for apps. A raw package id works too (that is what the Browse tree publishes), names match case-insensitively, and a name that is not in the list is an error rather than a silent no-op:
action: media_player.select_source
target:
entity_id: media_player.living_room_shield
data:
source: TiviMate
Selecting a source is sometimes more than launching an app — an AVR input, a speaker switch. That is what the Select source script above is for: with one configured it replaces the native launch entirely.
Known limits (the Apps map)
The map is your own override, so two shapes are accepted rather than papered over:
- Names differing only by case (
com.a=Dup,com.b=dup) offer one dropdown row — the first spelling — because name matching is case-insensitive. The shadowed app is not lost: the Browse tree still lists it under its own package id, andmedia_player.select_sourceaccepts that id. - A padded value typed by hand (
com.x= Netflix) is shown cleaned in the dropdown and the Browse tree, butapp_name,sourceand the Current App sensor display the map value verbatim — sosourcecan be a string that is not insource_list. A blank value falls through to the raw package instead.
Remote command script
remote.send_command sends protocol keys: repeats, long presses (START_LONG/
END_LONG) and inter-repeat delays. Some setups need that call to go somewhere else —
an IR bridge, a macro, a second device — so one optional Remote command script
replaces the whole key path when it is set. It receives the service's own parameters,
named as they are on the service:
| Variable | Meaning |
|---|---|
command |
the list of key commands as submitted, e.g. ["HOME", "KEYCODE_DPAD_CENTER"] |
num_repeats |
how many times to send them (default 1) |
delay_secs |
the delay between repeats in seconds (default 0.0) |
hold_secs |
the requested long-press duration (default 0.0) — the script owns the key semantics, so it decides what "hold" means |
script:
shield_remote: # select as "Remote command script"
alias: Shield remote (IR bridge)
sequence:
- action: remote.send_command
target:
entity_id: remote.living_room_bridge
data:
command: "{{ command }}" # a list: pass it through
num_repeats: "{{ num_repeats }}"
delay_secs: "{{ delay_secs }}"
hold_secs: "{{ hold_secs }}"
All four variables are always present, including their defaults, so a script can rely on the shape. Unset — the default — nothing changes: keys go out over the protocol exactly as before.
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