Files
ha-nvidia-shield/custom_components/nvidia_shield/helpers.py
T
James Mackay 75ebeb40b0 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.
2026-10-11 06:18:22 +01:00

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))