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:
1 parent
86aa2827a2
commit
75ebeb40b0
12 files changed
+302
-276
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user