refactor(config): one Shield IP field for both transports (t41)

The two address fields were the same Shield spelled twice, and every bug in the
zeroconf/IP-change path came from keeping them in sync. One field now:

* const.CONF_HOST replaces CONF_ADB_HOST / CONF_PROTOCOL_HOST in the setup form,
  the options page, the pairing forms, the reconfigure/reauth seeding and the
  description copy. ADB port 5555 stays the only port field;
* helpers.entry_host(entry) is the single resolver (options first, then data);
  resolve_adb_config and the protocol settings both go through it, so both
  transports read the same value at call time (the t32 F1 factory re-read keeps
  working: the NEXT built poller sees the new address);
* helpers.seed_host / seed_adb_enabled own the YAML seed's shape rule. A block
  may use `host` or the legacy protocol_host/adb_host keys — t41 keeps the old
  spelling working — and t32 F4's outcome is unchanged: a protocol-only block
  leaves ADB off, an address (or an explicit adb_enabled) turns it on;
* the zeroconf IP-change path collapses the t21-F2 per-host dance into one rule:
  the discovery address IS the entry's address, so the entry is updated when it
  differs and left alone when it matches. The blank-self-heal and
  deliberately-different-ADB-host cases are gone with the duplication — a
  second address field cannot express them anymore, and the tests now pin that
  (previously "an explicit override is never clobbered");
* the options page exposes the protocol toggle it never had (the page could turn
  ADB off but not the protocol);
* button.py (granted to this task) reads the entry's own address instead of
  another integration's entity.

The v3 entry migration that maps existing adb_host/protocol_host onto CONF_HOST
and drops the androidtv key lands in the next commit; HA runs migrations before
setup, so an existing entry is never read with the old shape.

Verified: 311 passed / 2 skipped, 312 / 1 with androidtvremote2, ruff clean,
drift engine exit 0 on HEAD. The UI-copy contract tests (every rendered field
has a label in strings.json AND translations/en.json, key trees identical, host
errors name the field) drove the string edits.
This commit is contained in:
James Mackay committed 2026-10-11 06:18:22 +01:00
1 parent 86aa2827a2
commit 75ebeb40b0
12 files changed
+302 -276

No files matched your search

+21 -15
View File
@@ -24,14 +24,20 @@ from .const import (
CONF_ADB_ENABLED,
CONF_ADB_HOST,
CONF_ADB_PORT,
CONF_HOST,
CONF_PROTOCOL_ENABLED,
CONF_PROTOCOL_HOST,
DEFAULT_ADB_PORT,
DEFAULT_NAME,
DOMAIN,
SERVICE_ADB_COMMAND,
)
from .helpers import protocol_cert_paths, resolve_adb_config
from .helpers import (
entry_host,
protocol_cert_paths,
resolve_adb_config,
seed_adb_enabled,
seed_host,
)
_LOGGER = logging.getLogger(__name__)
@@ -49,9 +55,9 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
"""
domain_config = (config or {}).get(DOMAIN)
for block in _yaml_seed_blocks(domain_config or []):
if not block.get(CONF_ADB_HOST) and not block.get(CONF_PROTOCOL_HOST):
if not seed_host(block):
_LOGGER.warning(
"nvidia_shield YAML block without protocol/adb host ignored: %s",
"nvidia_shield YAML block without an address ignored: %s",
block.get("name", "<unnamed>"),
)
continue
@@ -83,15 +89,13 @@ def _yaml_seed_blocks(domain_config) -> list[dict]:
continue
name = block.get("name") or DEFAULT_NAME
# t32 F4 (review rider): the seed honors the block's shape — a
# protocol-only block does NOT force ADB on; ADB comes only when the
# block carries an adb host (interactive cosmetics stay in the flow).
# protocol-only block does NOT force ADB on. t41: the address is ONE
# field (`host`, or a legacy protocol_host/adb_host).
seed = {
"name": name,
CONF_ADB_ENABLED: bool(block.get(CONF_ADB_HOST)),
CONF_ADB_HOST: str(block.get(CONF_ADB_HOST) or ""),
CONF_ADB_ENABLED: seed_adb_enabled(block),
CONF_HOST: seed_host(block),
CONF_ADB_PORT: int(block.get(CONF_ADB_PORT) or DEFAULT_ADB_PORT),
# protocol fields are prefill hints ONLY — pairing stays interactive
CONF_PROTOCOL_HOST: str(block.get(CONF_PROTOCOL_HOST) or ""),
}
if block.get("unique_id"):
seed["unique_id"] = str(block["unique_id"])
@@ -177,14 +181,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _build_protocol_settings(hass: HomeAssistant, entry: ConfigEntry):
"""Manager protocol_settings when the transport is enabled & resolvable.
The address is the entry's own (t41): the protocol field, else the ADB
field — both name the same Shield. Nothing is derived from another
integration.
The address is the entry's own single field (t41). Nothing is derived from
another integration.
"""
if not entry.options.get(CONF_PROTOCOL_ENABLED, False):
return None
host = entry.options.get(CONF_PROTOCOL_HOST) or entry.options.get(CONF_ADB_HOST)
host = entry_host(entry)
if not host:
_LOGGER.info(
"Protocol enabled for entry %s but no host resolvable — continuing "
@@ -214,7 +217,10 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
if config_entry.version < 2:
options = {**config_entry.options}
options.setdefault(CONF_PROTOCOL_ENABLED, False)
options.setdefault(CONF_PROTOCOL_HOST, "")
# t41: the entry's address is the single CONF_HOST field; a v1 entry
# carried it under the legacy adb_host key.
if not options.get(CONF_HOST):
options[CONF_HOST] = options.get(CONF_ADB_HOST) or ""
hass.config_entries.async_update_entry(
config_entry, options=options, version=2
)
+4 -7
View File
@@ -11,12 +11,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .adb_session import AdbMediaSessionPoller, ensure_adb_key
from .const import (
CONF_ADB_ENABLED,
CONF_ADB_HOST,
CONF_ADB_PORT,
CONF_PROTOCOL_HOST,
DEFAULT_ADB_PORT,
DOMAIN,
)
from .helpers import entry_host
_LOGGER = logging.getLogger(__name__)
@@ -54,11 +53,9 @@ class NvidiaShieldPairAdbButton(ButtonEntity):
async def async_press(self) -> None:
"""Attempt ADB connection. The Shield will show a pairing prompt."""
# t41: the address is ours — never derived from another integration's
# entity. The ADB field leads, then the protocol field (same Shield).
host = self._entry.options.get(CONF_ADB_HOST) or self._entry.options.get(
CONF_PROTOCOL_HOST
)
# t41: the entry's own single address — never derived from another
# integration's entity.
host = entry_host(self._entry)
port = self._entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT)
if not host:
+60 -70
View File
@@ -15,19 +15,18 @@ from .const import (
ATTR_MEDIA_DAG_INPUT_MAP,
CONF_ADB_COMMAND_ENABLED,
CONF_ADB_ENABLED,
CONF_ADB_HOST,
CONF_ADB_PORT,
CONF_ADB_STORE_APP_INFO,
CONF_APP_ICON_MAP,
CONF_APP_NAME_MAP,
CONF_CHANNEL_DIGIT_DELAY,
CONF_CHANNEL_SEND_ENTER,
CONF_HOST,
CONF_JELLYFIN_ENTITY,
CONF_LOGCAT_EVENTS,
CONF_PAIRING_CODE,
CONF_POWER_ENTITY,
CONF_PROTOCOL_ENABLED,
CONF_PROTOCOL_HOST,
CONF_RECEIVER_ENTITY,
CONF_RECEIVER_INPUT,
CONF_SKIP_PAIRING,
@@ -35,7 +34,7 @@ from .const import (
DEFAULT_NAME,
DOMAIN,
)
from .helpers import protocol_cert_paths
from .helpers import entry_host, protocol_cert_paths, seed_adb_enabled, seed_host
_LOGGER = logging.getLogger(__name__)
@@ -141,9 +140,10 @@ def _host_field_key(field: str, host: str):
def _user_schema_dict(sel, defaults: dict | None = None) -> dict:
"""User-step schema — every field belongs to THIS integration (t41).
``defaults`` pre-fills name/hosts from a zeroconf discovery event (or a
YAML seed hint); the ADB port is pre-filled with the standard 5555 so it
reads as an advanced setting rather than required knowledge (t33 F-D).
``defaults`` pre-fills the name and the ONE address (t41) from a zeroconf
discovery event or a YAML seed hint; the ADB port is pre-filled with the
standard 5555 so it reads as an advanced setting rather than required
knowledge (t33 F-D).
"""
defaults = defaults or {}
host = str(defaults.get("host") or "")
@@ -166,10 +166,9 @@ def _user_schema_dict(sel, defaults: dict | None = None) -> dict:
# the code's own fallback for this flag is True (async_step_user) — the
# rendered checkbox must agree with it
schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO, default=True)] = bool
schema_dict[_host_field_key(CONF_ADB_HOST, host)] = str
schema_dict[_host_field_key(CONF_HOST, host)] = str
schema_dict[vol.Optional(CONF_ADB_PORT, default=port)] = int
schema_dict[vol.Optional(CONF_PROTOCOL_ENABLED)] = bool
schema_dict[_host_field_key(CONF_PROTOCOL_HOST, host)] = str
return schema_dict
@@ -251,10 +250,9 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_POWER_ENTITY: user_input.get(CONF_POWER_ENTITY, ""),
CONF_ADB_ENABLED: user_input.get(CONF_ADB_ENABLED, False),
CONF_ADB_STORE_APP_INFO: user_input.get(CONF_ADB_STORE_APP_INFO, True),
CONF_ADB_HOST: user_input.get(CONF_ADB_HOST, ""),
CONF_HOST: str(user_input.get(CONF_HOST) or "").strip(),
CONF_ADB_PORT: user_input.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
CONF_PROTOCOL_ENABLED: user_input.get(CONF_PROTOCOL_ENABLED, False),
CONF_PROTOCOL_HOST: user_input.get(CONF_PROTOCOL_HOST, ""),
}
# If ADB is enabled, go to pairing step
@@ -336,22 +334,15 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
for entry in self._async_current_entries():
if getattr(entry, "unique_id", None) != mac:
continue
# Known device — per-host updates (t21 F2): each host is
# tracked independently against the discovery address.
# - a blank (auto-detect) host self-heals from the address;
# - a stale protocol host is the IP change and is updated;
# - a deliberately-different ADB host (an explicit override
# that differs from both the address and the stored
# protocol host) is never clobbered.
protocol_host = entry.options.get(CONF_PROTOCOL_HOST, "")
adb_host = entry.options.get(CONF_ADB_HOST, "")
# Known device — IP change (t21 F2, collapsed by t41): there is
# ONE address field now, so the discovery address simply IS the
# entry's address. The per-host dance (blank self-heal / stale
# protocol host / deliberately different ADB host) only existed
# because the address was stored twice; it went with the
# duplication.
options = dict(entry.options)
if protocol_host != host:
options[CONF_PROTOCOL_HOST] = host
if not adb_host or adb_host == protocol_host:
# blank (self-heal) or auto-synced alongside the protocol
# host — safe to follow the device address on discovery
options[CONF_ADB_HOST] = host
if entry_host(entry) != host:
options[CONF_HOST] = host
if options != entry.options:
# t29 G3a: the deprecated update+schedule-reload combo is
# gone (2026.12 error) — the entry updates ONLY here; the
@@ -396,10 +387,9 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_POWER_ENTITY: d.get(CONF_POWER_ENTITY, ""),
CONF_ADB_ENABLED: d.get(CONF_ADB_ENABLED, False),
CONF_ADB_STORE_APP_INFO: d.get(CONF_ADB_STORE_APP_INFO, True),
CONF_ADB_HOST: d.get(CONF_ADB_HOST, ""),
CONF_HOST: d.get(CONF_HOST, ""),
CONF_ADB_PORT: d.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
CONF_PROTOCOL_ENABLED: d.get(CONF_PROTOCOL_ENABLED, False),
CONF_PROTOCOL_HOST: d.get(CONF_PROTOCOL_HOST, ""),
},
)
@@ -409,9 +399,6 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_protocol_pair()
return self._create_entry()
def _protocol_host(self) -> str | None:
"""Protocol host: explicit override, else the ADB host fallback."""
return self._user_data.get(CONF_PROTOCOL_HOST) or self._resolve_adb_host()
def _discard_protocol_client(self) -> None:
"""Drop a pairing client whose session became void (host corrected)."""
@@ -429,7 +416,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
None when no host is resolvable or ``androidtvremote2`` is missing —
the caller turns that into an error key, never a crash.
"""
host = self._protocol_host()
host = self._host()
if not host:
return None
certfile, keyfile = await self.hass.async_add_executor_job(
@@ -451,7 +438,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_begin_protocol_pairing(self):
"""Start a PIN pairing session: ``(client, None)`` or ``(None, error)``."""
if not self._protocol_host():
if not self._host():
return None, "protocol_no_host"
client = await self._start_protocol_pairing()
if client is None:
@@ -470,7 +457,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{
vol.Required(CONF_PAIRING_CODE): str,
_host_field_key(CONF_PROTOCOL_HOST, self._protocol_host() or ""): str,
_host_field_key(CONF_HOST, self._host()): str,
vol.Optional(CONF_SKIP_PAIRING, default=False): bool,
}
),
@@ -530,12 +517,12 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input.get(CONF_SKIP_PAIRING):
return await self._async_skip_protocol_pairing()
typed_host = str(user_input.get(CONF_PROTOCOL_HOST) or "").strip()
if typed_host and typed_host != (self._protocol_host() or ""):
typed_host = str(user_input.get(CONF_HOST) or "").strip()
if typed_host and typed_host != self._host():
# A host typed HERE is authoritative — the user step is gone, so
# the session started against the old address is dropped and the
# Shield is asked for a NEW code at the corrected address (t35 F1).
self._user_data[CONF_PROTOCOL_HOST] = typed_host
self._user_data[CONF_HOST] = typed_host
self._discard_protocol_client()
code = str(user_input.get(CONF_PAIRING_CODE, "") or "").strip()
@@ -584,19 +571,17 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""
block = dict(data or {})
name = block.get("name") or DEFAULT_NAME
# the block's address comes from `host`, or from the legacy keys
block_host = seed_host(block)
seed_unique_id = block.get("unique_id")
if not seed_unique_id:
host = block.get(CONF_ADB_HOST) or block.get(CONF_PROTOCOL_HOST)
seed_unique_id = f"yaml:{name}:{host or 'nohost'}"
seed_unique_id = f"yaml:{name}:{block_host or 'nohost'}"
# duplicate guards
yhost = block.get(CONF_ADB_HOST) or block.get(CONF_PROTOCOL_HOST)
for existing in self._async_current_entries():
ehost = existing.options.get(CONF_PROTOCOL_HOST) or existing.options.get(
CONF_ADB_HOST
)
ehost = entry_host(existing)
if existing.unique_id == seed_unique_id or (
yhost and ehost == yhost
block_host and ehost == block_host
):
_LOGGER.info(
"nvidia_shield YAML seed for %r matches an existing "
@@ -612,18 +597,16 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# block actually carries an adb host.
self._user_data = {
"name": name,
CONF_ADB_ENABLED: bool(block.get(CONF_ADB_HOST)),
CONF_ADB_HOST: block.get(CONF_ADB_HOST, ""),
CONF_ADB_PORT: block.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
# protocol prefill hints ONLY (pairing stays interactive)
CONF_PROTOCOL_HOST: block.get(CONF_PROTOCOL_HOST, ""),
# t32 F4's shape rule on the t41 keys: an address names the Shield,
# and an explicit adb_enabled says otherwise
CONF_ADB_ENABLED: seed_adb_enabled(block),
CONF_HOST: block_host,
CONF_ADB_PORT: int(block.get(CONF_ADB_PORT) or DEFAULT_ADB_PORT),
}
# t33 F-A: the seed host feeds the same user-step prefill as a
# zeroconf discovery, but WITHOUT the discovery announcement — the
# address came from configuration.yaml, not from the network.
self._seed_host = self._user_data[CONF_PROTOCOL_HOST] or self._user_data[
CONF_ADB_HOST
]
self._seed_host = block_host
return await self.async_step_user(None)
async def async_step_reauth(self, entry_data):
@@ -644,7 +627,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._user_data = {
"name": entry.title,
CONF_PROTOCOL_ENABLED: True,
CONF_PROTOCOL_HOST: entry.options.get(CONF_PROTOCOL_HOST, ""),
CONF_HOST: entry_host(entry),
}
return await self.async_step_protocol_pair() # first visit re-pairs
return self.async_show_form(
@@ -671,10 +654,9 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._user_data = {
"name": entry.title,
CONF_ADB_ENABLED: entry.options.get(CONF_ADB_ENABLED, False),
CONF_ADB_HOST: entry.options.get(CONF_ADB_HOST, ""),
CONF_HOST: entry_host(entry),
CONF_ADB_PORT: entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
CONF_PROTOCOL_ENABLED: True,
CONF_PROTOCOL_HOST: entry.options.get(CONF_PROTOCOL_HOST, ""),
}
return await self.async_step_protocol_pair()
@@ -691,7 +673,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"options": {
**entry.options,
CONF_PROTOCOL_ENABLED: True,
CONF_PROTOCOL_HOST: self._protocol_host() or "",
CONF_HOST: self._host(),
},
}
if mac:
@@ -714,19 +696,25 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
options={
**entry.options,
CONF_PROTOCOL_ENABLED: True,
CONF_PROTOCOL_HOST: self._user_data.get(CONF_PROTOCOL_HOST, ""),
CONF_HOST: self._host(),
},
)
self._reauth_entry = None
return self.async_abort(reason="reauth_successful")
def _resolve_adb_host(self) -> str | None:
"""The address to use for ADB: the ADB field, else the protocol field."""
return (
self._user_data.get(CONF_ADB_HOST)
or self._user_data.get(CONF_PROTOCOL_HOST)
or None
)
def _host(self) -> str:
"""The Shield's address as this flow knows it (t41: ONE field).
What the user typed (or a seed carried) lives in ``_user_data``; the
discovery/seed hints stay as fallbacks for a form that was rendered with
a pre-filled field and submitted unchanged.
"""
return str(
self._user_data.get(CONF_HOST)
or self._discovered_host
or self._seed_host
or ""
).strip()
def _show_adb_pair_form(self, errors=None):
"""adb_pair form: the address/port (fixable here) and the skip escape.
@@ -739,7 +727,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="adb_pair",
data_schema=vol.Schema(
{
_host_field_key(CONF_ADB_HOST, self._resolve_adb_host() or ""): str,
_host_field_key(CONF_HOST, self._host()): str,
vol.Optional(
CONF_ADB_PORT,
default=self._user_data.get(
@@ -770,10 +758,10 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# re-adding the integration.
return await self._async_after_pairing()
typed_host = str(user_input.get(CONF_ADB_HOST) or "").strip()
typed_host = str(user_input.get(CONF_HOST) or "").strip()
if typed_host:
# a host typed HERE is authoritative (the user step is gone)
self._user_data[CONF_ADB_HOST] = typed_host
self._user_data[CONF_HOST] = typed_host
port = user_input.get(CONF_ADB_PORT, DEFAULT_ADB_PORT)
try:
@@ -782,7 +770,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
port = DEFAULT_ADB_PORT
self._user_data[CONF_ADB_PORT] = port
host = self._resolve_adb_host()
host = self._host()
if not host:
errors["base"] = "adb_no_host"
else:
@@ -923,12 +911,14 @@ class NvidiaShieldOptionsFlow(config_entries.OptionsFlowWithReload):
pwr_key = vol.Optional(CONF_POWER_ENTITY)
schema_dict[pwr_key] = sel["power"]
# ADB options
# Transport toggles (t41: both transports are ours; the skip paths and
# this page are the way to turn either one off)
schema_dict[vol.Optional(CONF_ADB_ENABLED)] = bool
schema_dict[vol.Optional(CONF_PROTOCOL_ENABLED)] = bool
schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO)] = bool
# ADB host/port (optional overrides, shown when ADB is enabled)
schema_dict[vol.Optional(CONF_ADB_HOST)] = str
# The ONE address field (t41), plus the ADB port
schema_dict[vol.Optional(CONF_HOST)] = str
schema_dict[vol.Optional(CONF_ADB_PORT)] = int
# t29 G3b: suggested values come from the entry options via the
+10 -2
View File
@@ -12,6 +12,16 @@ from .ha_adb_common.const import ( # noqa: F401
DOMAIN = "nvidia_shield"
# t41: the INTEGRATION's address field — one Shield IP for the whole stack
# (protocol + ADB). The two keys below it are LEGACY: a YAML seed may still
# carry them and the v3 entry migration maps them onto CONF_HOST, but no
# runtime code reads them.
CONF_HOST = "host"
CONF_ADB_HOST = "adb_host"
CONF_PROTOCOL_HOST = "protocol_host"
# t41 legacy: dropped from entries by the v3 migration (the androidtv entity
# path it configured is gone), never read at runtime.
CONF_ANDROIDTV_ENTITY = "androidtv_entity"
CONF_JELLYFIN_ENTITY = "jellyfin_entity"
CONF_RECEIVER_ENTITY = "receiver_entity"
@@ -19,12 +29,10 @@ CONF_RECEIVER_INPUT = "receiver_input"
CONF_POWER_ENTITY = "power_entity"
CONF_ADB_ENABLED = "adb_enabled"
CONF_ADB_STORE_APP_INFO = "adb_store_app_info"
CONF_ADB_HOST = "adb_host"
CONF_ADB_PORT = "adb_port"
# Remote-protocol transport (androidtvremote2) — Phase 2
CONF_PROTOCOL_ENABLED = "protocol_enabled"
CONF_PROTOCOL_HOST = "protocol_host"
CONF_PAIRING_CODE = "pairing_code"
# t33 F-B: the "continue without pairing" escape offered on BOTH pairing
# steps — the entry still finishes (passive mode) instead of stranding the
+43 -4
View File
@@ -6,8 +6,10 @@ from dataclasses import dataclass
from homeassistant.core import HomeAssistant
from .const import (
CONF_ADB_ENABLED,
CONF_ADB_HOST,
CONF_ADB_PORT,
CONF_HOST,
CONF_PROTOCOL_HOST,
DEFAULT_ADB_PORT,
DOMAIN,
@@ -41,14 +43,51 @@ def protocol_cert_paths(hass: HomeAssistant) -> tuple[str, str]:
return os.path.join(base, "client.pem"), os.path.join(base, "client.key")
def entry_host(entry) -> str:
"""The Shield's address for an entry (t41: ONE field).
``options`` is the source of truth; ``data`` is read as a fallback so an
entry created by an older flow (or a hand-written seed) still resolves.
"""
options = getattr(entry, "options", None) or {}
data = getattr(entry, "data", None) or {}
return str(options.get(CONF_HOST) or data.get(CONF_HOST) or "").strip()
def seed_host(block: dict) -> str:
"""The address a YAML seed block carries (t41).
``host`` is the current key; the legacy ``protocol_host`` / ``adb_host``
stay accepted so an existing configuration.yaml keeps seeding, protocol
first (that transport is the one that cannot fall back to anything else).
"""
for key in (CONF_HOST, CONF_PROTOCOL_HOST, CONF_ADB_HOST):
value = str((block or {}).get(key) or "").strip()
if value:
return value
return ""
def seed_adb_enabled(block: dict) -> bool:
"""Does a seed block ask for ADB? (t32 F4's shape rule, t41's keys).
An explicit ``adb_enabled`` wins. Otherwise an address that names the Shield
— the current ``host`` key or the legacy ``adb_host`` — turns ADB on, while
a protocol-only block leaves it off.
"""
block = block or {}
if CONF_ADB_ENABLED in block:
return bool(block[CONF_ADB_ENABLED])
return bool(block.get(CONF_HOST) or block.get(CONF_ADB_HOST))
def resolve_adb_config(hass: HomeAssistant, entry) -> AdbConfig | None:
"""Resolve host/port/key_path for the entry's ADB connection, or None.
Priority: the entry's own address fields (t41: no other integration is
consulted) — the ADB field first, then the protocol field, since both
describe the same Shield.
Priority: the entry's own address (t41: no other integration is consulted)
and its ADB port option.
"""
host = entry.options.get(CONF_ADB_HOST) or entry.options.get(CONF_PROTOCOL_HOST)
host = entry_host(entry)
if not host:
return None
port = int(entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT))
+16 -18
View File
@@ -3,45 +3,41 @@
"step": {
"user": {
"title": "NVIDIA Shield",
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source \u2014 an Android TV Remote entity, the Nvidia Remote Protocol, or ADB. Link the devices and services you want to use.",
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source \u2014 the Nvidia Remote Protocol or ADB. Link the devices and services you want to use.",
"data": {
"name": "Name",
"androidtv_entity": "Android TV Remote entity",
"jellyfin_entity": "Jellyfin media player",
"receiver_entity": "Receiver",
"receiver_input": "Receiver input",
"power_entity": "Power switch",
"adb_enabled": "Read state over ADB",
"adb_store_app_info": "Show title and artist",
"adb_host": "ADB host (IP address)",
"host": "The Shield's IP address",
"adb_port": "ADB port",
"protocol_enabled": "Add the Nvidia Remote Protocol",
"protocol_host": "Protocol host (IP address)"
"protocol_enabled": "Add the Nvidia Remote Protocol"
},
"data_description": {
"androidtv_entity": "The media_player entity from the Android TV Remote integration. Leave blank if you use the Nvidia Remote Protocol or ADB instead.",
"jellyfin_entity": "The Jellyfin media_player for this Shield. Leave blank to detect it automatically.",
"receiver_entity": "The receiver or soundbar this Shield plays through.",
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure.",
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
"adb_host": "The Shield's IP address. Pre-filled when the Shield was discovered, or when an Android TV Remote entity is set \u2014 change it only if you need a different address.",
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if you need a different address.",
"adb_port": "The ADB port. The standard value is 5555 \u2014 change it only if ADB uses a custom port on your network.",
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step.",
"protocol_host": "The Shield's IP address for the remote protocol. Pre-filled when the Shield was discovered \u2014 leave blank to use the ADB host."
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
}
},
"adb_pair": {
"title": "Allow ADB access",
"description": "On the Shield, open Settings > System > Developer options and turn on Network debugging (it may be called ADB debugging). Then select Submit \u2014 the Shield shows an Allow prompt; approve it. Nothing happened? Check the address below and select Submit again. Not now? Turn on 'Continue without ADB pairing' and select Submit.",
"data": {
"adb_host": "ADB host (IP address)",
"host": "The Shield's IP address",
"adb_port": "ADB port",
"skip_pairing": "Continue without ADB pairing"
},
"data_description": {
"adb_host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if the Shield moved or you gave it a different address.",
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if the Shield moved or you gave it a different address.",
"adb_port": "The standard ADB port is 5555.",
"skip_pairing": "Finishes setup now and leaves ADB unpaired. The Pair ADB button stays available on the device page, so you can pair later."
}
@@ -51,12 +47,12 @@
"description": "While this step is open the Shield shows a pairing code. Type it below and select Submit. If no code appears on the Shield, check the IP address below and select Submit again \u2014 pairing restarts and the Shield shows a new code. To set up without the remote protocol, turn on 'Continue without protocol pairing'.",
"data": {
"pairing_code": "Pairing code shown on the Shield",
"protocol_host": "Protocol host (IP address)",
"host": "The Shield's IP address",
"skip_pairing": "Continue without protocol pairing"
},
"data_description": {
"pairing_code": "The code the Shield displays while this step is open.",
"protocol_host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it if the Shield moved.",
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it if the Shield moved.",
"skip_pairing": "Finishes setup with the remote protocol off \u2014 it stays off until you pair it. Pair later from this integration's entry menu: choose Reconfigure."
}
},
@@ -67,11 +63,11 @@
},
"error": {
"adb_failed": "The Shield did not accept the ADB connection. Make sure Network debugging is on, approve the Allow prompt on the Shield, then select Submit to try again.",
"adb_no_host": "No IP address for the Shield yet. Fill in the ADB host field above (a discovered Shield, or one linked to an Android TV Remote entity, is pre-filled automatically).",
"protocol_no_host": "No IP address for the Shield yet. Fill in the protocol host field above (a discovered Shield, or one linked to an Android TV Remote entity, is pre-filled automatically).",
"adb_no_host": "No IP address for the Shield yet. Fill in the host field above (a discovered Shield is pre-filled automatically).",
"protocol_no_host": "No IP address for the Shield yet. Fill in the host field above (a discovered Shield is pre-filled automatically).",
"protocol_pair_failed": "The Shield rejected that pairing code. Check the code on the Shield and select Submit to try again.",
"protocol_unavailable": "The androidtvremote2 library is not installed, so the remote protocol cannot pair. Install it and restart Home Assistant, or turn on 'Continue without protocol pairing' to finish setup anyway.",
"no_state_source": "No state source selected. Choose at least one: an Android TV Remote entity, 'Add the Nvidia Remote Protocol', or 'Read state over ADB'.",
"no_state_source": "No state source selected. Choose at least one: 'Add the Nvidia Remote Protocol' or 'Read state over ADB'.",
"invalid_app_map": "Each line must be package=name with a non-empty package (no spaces) and a non-empty name \u2014 for example ar.tvplayer.tv=TiviMate.",
"invalid_digit_delay": "The digit delay must be between 0.05 and 1.0 seconds."
},
@@ -101,8 +97,9 @@
"receiver_input": "Receiver input",
"power_entity": "Power switch",
"adb_enabled": "Read state over ADB",
"protocol_enabled": "Add the Nvidia Remote Protocol",
"adb_store_app_info": "Show title and artist",
"adb_host": "ADB host (IP address)",
"host": "The Shield's IP address",
"adb_port": "ADB port",
"app_name_map": "App names and icons",
"adb_command_enabled": "Enable the adb_command service",
@@ -117,7 +114,8 @@
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
"adb_host": "The Shield's IP address. Leave blank to use the Android TV Remote entity's address.",
"host": "The Shield's IP address. Change it only if the Shield moved or you gave it a different address.",
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). Turning it on needs a one-time PIN pairing \u2014 use Reconfigure on the entry to pair.",
"adb_port": "The ADB port. The standard value is 5555.",
"app_name_map": "One entry per line, package=name (for example ar.tvplayer.tv=TiviMate), with an optional |icon after the name. Leave blank to clear.",
"adb_command_enabled": "Adds the nvidia_shield.adb_command service (a raw ADB shell command over the persistent connection). Security-relevant: leave it off unless you need it \u2014 the same opt-in posture as the core androidtv integration.",
@@ -3,45 +3,41 @@
"step": {
"user": {
"title": "NVIDIA Shield",
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source \u2014 an Android TV Remote entity, the Nvidia Remote Protocol, or ADB. Link the devices and services you want to use.",
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source \u2014 the Nvidia Remote Protocol or ADB. Link the devices and services you want to use.",
"data": {
"name": "Name",
"androidtv_entity": "Android TV Remote entity",
"jellyfin_entity": "Jellyfin media player",
"receiver_entity": "Receiver",
"receiver_input": "Receiver input",
"power_entity": "Power switch",
"adb_enabled": "Read state over ADB",
"adb_store_app_info": "Show title and artist",
"adb_host": "ADB host (IP address)",
"host": "The Shield's IP address",
"adb_port": "ADB port",
"protocol_enabled": "Add the Nvidia Remote Protocol",
"protocol_host": "Protocol host (IP address)"
"protocol_enabled": "Add the Nvidia Remote Protocol"
},
"data_description": {
"androidtv_entity": "The media_player entity from the Android TV Remote integration. Leave blank if you use the Nvidia Remote Protocol or ADB instead.",
"jellyfin_entity": "The Jellyfin media_player for this Shield. Leave blank to detect it automatically.",
"receiver_entity": "The receiver or soundbar this Shield plays through.",
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure.",
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
"adb_host": "The Shield's IP address. Pre-filled when the Shield was discovered, or when an Android TV Remote entity is set \u2014 change it only if you need a different address.",
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if you need a different address.",
"adb_port": "The ADB port. The standard value is 5555 \u2014 change it only if ADB uses a custom port on your network.",
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step.",
"protocol_host": "The Shield's IP address for the remote protocol. Pre-filled when the Shield was discovered \u2014 leave blank to use the ADB host."
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
}
},
"adb_pair": {
"title": "Allow ADB access",
"description": "On the Shield, open Settings > System > Developer options and turn on Network debugging (it may be called ADB debugging). Then select Submit \u2014 the Shield shows an Allow prompt; approve it. Nothing happened? Check the address below and select Submit again. Not now? Turn on 'Continue without ADB pairing' and select Submit.",
"data": {
"adb_host": "ADB host (IP address)",
"host": "The Shield's IP address",
"adb_port": "ADB port",
"skip_pairing": "Continue without ADB pairing"
},
"data_description": {
"adb_host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if the Shield moved or you gave it a different address.",
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if the Shield moved or you gave it a different address.",
"adb_port": "The standard ADB port is 5555.",
"skip_pairing": "Finishes setup now and leaves ADB unpaired. The Pair ADB button stays available on the device page, so you can pair later."
}
@@ -51,12 +47,12 @@
"description": "While this step is open the Shield shows a pairing code. Type it below and select Submit. If no code appears on the Shield, check the IP address below and select Submit again \u2014 pairing restarts and the Shield shows a new code. To set up without the remote protocol, turn on 'Continue without protocol pairing'.",
"data": {
"pairing_code": "Pairing code shown on the Shield",
"protocol_host": "Protocol host (IP address)",
"host": "The Shield's IP address",
"skip_pairing": "Continue without protocol pairing"
},
"data_description": {
"pairing_code": "The code the Shield displays while this step is open.",
"protocol_host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it if the Shield moved.",
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it if the Shield moved.",
"skip_pairing": "Finishes setup with the remote protocol off \u2014 it stays off until you pair it. Pair later from this integration's entry menu: choose Reconfigure."
}
},
@@ -67,11 +63,11 @@
},
"error": {
"adb_failed": "The Shield did not accept the ADB connection. Make sure Network debugging is on, approve the Allow prompt on the Shield, then select Submit to try again.",
"adb_no_host": "No IP address for the Shield yet. Fill in the ADB host field above (a discovered Shield, or one linked to an Android TV Remote entity, is pre-filled automatically).",
"protocol_no_host": "No IP address for the Shield yet. Fill in the protocol host field above (a discovered Shield, or one linked to an Android TV Remote entity, is pre-filled automatically).",
"adb_no_host": "No IP address for the Shield yet. Fill in the host field above (a discovered Shield is pre-filled automatically).",
"protocol_no_host": "No IP address for the Shield yet. Fill in the host field above (a discovered Shield is pre-filled automatically).",
"protocol_pair_failed": "The Shield rejected that pairing code. Check the code on the Shield and select Submit to try again.",
"protocol_unavailable": "The androidtvremote2 library is not installed, so the remote protocol cannot pair. Install it and restart Home Assistant, or turn on 'Continue without protocol pairing' to finish setup anyway.",
"no_state_source": "No state source selected. Choose at least one: an Android TV Remote entity, 'Add the Nvidia Remote Protocol', or 'Read state over ADB'.",
"no_state_source": "No state source selected. Choose at least one: 'Add the Nvidia Remote Protocol' or 'Read state over ADB'.",
"invalid_app_map": "Each line must be package=name with a non-empty package (no spaces) and a non-empty name \u2014 for example ar.tvplayer.tv=TiviMate.",
"invalid_digit_delay": "The digit delay must be between 0.05 and 1.0 seconds."
},
@@ -101,8 +97,9 @@
"receiver_input": "Receiver input",
"power_entity": "Power switch",
"adb_enabled": "Read state over ADB",
"protocol_enabled": "Add the Nvidia Remote Protocol",
"adb_store_app_info": "Show title and artist",
"adb_host": "ADB host (IP address)",
"host": "The Shield's IP address",
"adb_port": "ADB port",
"app_name_map": "App names and icons",
"adb_command_enabled": "Enable the adb_command service",
@@ -117,7 +114,8 @@
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
"adb_host": "The Shield's IP address. Leave blank to use the Android TV Remote entity's address.",
"host": "The Shield's IP address. Change it only if the Shield moved or you gave it a different address.",
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). Turning it on needs a one-time PIN pairing \u2014 use Reconfigure on the entry to pair.",
"adb_port": "The ADB port. The standard value is 5555.",
"app_name_map": "One entry per line, package=name (for example ar.tvplayer.tv=TiviMate), with an optional |icon after the name. Leave blank to clear.",
"adb_command_enabled": "Adds the nvidia_shield.adb_command service (a raw ADB shell command over the persistent connection). Security-relevant: leave it off unless you need it \u2014 the same opt-in posture as the core androidtv integration.",