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.
95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
"""Shared config helpers for the NVIDIA Shield integration."""
|
|
|
|
import os
|
|
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,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class AdbConfig:
|
|
"""Resolved ADB connection settings for a config entry."""
|
|
|
|
host: str
|
|
port: int
|
|
key_path: str
|
|
|
|
|
|
def adb_key_path(hass: HomeAssistant) -> str:
|
|
"""Path of the ADB private key for this integration (may not exist yet)."""
|
|
return os.path.join(hass.config.config_dir, ".storage", DOMAIN, "adbkey")
|
|
|
|
|
|
def protocol_cert_paths(hass: HomeAssistant) -> tuple[str, str]:
|
|
"""Local (certfile, keyfile) paths for the remote protocol client.
|
|
|
|
Mirrors core androidtv_remote's storage pattern: private material under
|
|
``.storage/<domain>/``. Shared between config-flow pairing and runtime —
|
|
an entry aborts duplicate setup by unique_id (= device MAC), so one pair
|
|
of files suffices per install.
|
|
"""
|
|
base = os.path.join(hass.config.config_dir, ".storage", DOMAIN)
|
|
os.makedirs(base, exist_ok=True)
|
|
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 (t41: no other integration is consulted)
|
|
and its ADB port option.
|
|
"""
|
|
host = entry_host(entry)
|
|
if not host:
|
|
return None
|
|
port = int(entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT))
|
|
return AdbConfig(host=host, port=port, key_path=adb_key_path(hass))
|