feat:init,config_flow: YAML seed-only setup — an optional nvidia_shield block starts interactive SOURCE_IMPORT flows (parsed in async_setup), the import step guards duplicates by host/MAC-virtual unique_id and never re-seeds UI-configured hosts, protocol fields are prefill hints only (t30 U1)

This commit is contained in:
James Mackay committed 2026-10-09 07:24:39 +01:00
1 parent 2e70587871
commit a6eeae86e9
2 files changed
+97 -5

No files matched your search

+49 -5
View File
@@ -13,6 +13,7 @@ passive until the user pairs via the Pair ADB button.
import logging import logging
import os import os
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -21,9 +22,13 @@ from .adb_session import AdbMediaSessionPoller
from .connection import ShieldConnectionManager from .connection import ShieldConnectionManager
from .const import ( from .const import (
CONF_ADB_ENABLED, CONF_ADB_ENABLED,
CONF_ADB_HOST,
CONF_ADB_PORT,
CONF_ANDROIDTV_ENTITY, CONF_ANDROIDTV_ENTITY,
CONF_PROTOCOL_ENABLED, CONF_PROTOCOL_ENABLED,
CONF_PROTOCOL_HOST, CONF_PROTOCOL_HOST,
DEFAULT_ADB_PORT,
DEFAULT_NAME,
DOMAIN, DOMAIN,
SERVICE_ADB_COMMAND, SERVICE_ADB_COMMAND,
) )
@@ -35,13 +40,30 @@ PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.REMOTE, Platform.B
async def async_setup(hass: HomeAssistant, config) -> bool: async def async_setup(hass: HomeAssistant, config) -> bool:
"""Register the integration's services (t29 G8, always-on). """Domain setup: actions (always-on) + optional YAML seeding (t30 U1).
``adb_command`` exists regardless of loaded entries or option state — YAML is SEED-ONLY (user ruling): every ``nvidia_shield`` block starts an
the entry's adb_command_enabled option gates USE (participants self- interactive SOURCE_IMPORT config flow — nothing is created without the
register from the media_player platform), so automations validate and user walking the flow, and pairing stays interactive exactly as for UI
errors are informative instead of 'service not found'. setup. Removing the YAML block creates OR deletes nothing. UI-configured
entries are never re-seeded (host/MAC duplicate guards).
""" """
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):
_LOGGER.warning(
"nvidia_shield YAML block without protocol/adb host ignored: %s",
block.get("name", "<unnamed>"),
)
continue
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=block,
)
)
# t29 G8 (kept here across the t30 splice): the action registers ALWAYS
from .media_player import _ADB_COMMAND_SCHEMA, _async_adb_command_service from .media_player import _ADB_COMMAND_SCHEMA, _async_adb_command_service
hass.services.async_register( hass.services.async_register(
@@ -53,6 +75,28 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
return True return True
def _yaml_seed_blocks(domain_config) -> list[dict]:
"""Normalize the optional YAML domain key into seed blocks (t30 U1)."""
blocks: list[dict] = []
raw_list = domain_config if isinstance(domain_config, list) else [domain_config]
for block in raw_list:
if not isinstance(block, dict):
continue
name = block.get("name") or DEFAULT_NAME
seed = {
"name": name,
CONF_ADB_ENABLED: True,
CONF_ADB_HOST: str(block.get(CONF_ADB_HOST) or ""),
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"])
blocks.append(seed)
return blocks
async def _build_poller_factory(hass: HomeAssistant, entry: ConfigEntry): async def _build_poller_factory(hass: HomeAssistant, entry: ConfigEntry):
"""Poller factory when ADB is configured AND usable today, else None. """Poller factory when ADB is configured AND usable today, else None.
@@ -471,6 +471,54 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_import(self, data):
"""Seed an interactive flow from YAML (t30 U1, seed-only ruling).
The import validates the block and routes into the standard user
flow — pairing stays interactive, exactly one visit. Duplicate
guards: the block's host/MAC-virtual unique_id aborts when an
entry already holds it, and a UI-configured entry with the same
protocol/adb host is never re-seeded.
"""
block = dict(data or {})
name = block.get("name") or DEFAULT_NAME
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'}"
# duplicate guards
yhost = block.get(CONF_ADB_HOST) or block.get(CONF_PROTOCOL_HOST)
for existing in self.hass.config_entries.async_entries():
ehost = existing.options.get(CONF_PROTOCOL_HOST) or existing.options.get(
CONF_ADB_HOST
)
if existing.unique_id == seed_unique_id or (
yhost and ehost == yhost
):
_LOGGER.info(
"nvidia_shield YAML seed for %r matches an existing "
"entry — not re-seeded",
name,
)
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(seed_unique_id)
self._abort_if_unique_id_configured()
# route into the standard user flow (single interactive visit)
self._user_data = {
"name": name,
CONF_ADB_ENABLED: True,
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, ""),
}
self._discovered_host = self._user_data[CONF_PROTOCOL_HOST] or self._user_data[
CONF_ADB_HOST
]
return await self.async_step_user(None)
async def async_step_reauth(self, entry_data): async def async_step_reauth(self, entry_data):
"""Device hard reset invalidated our certificates (review trigger).""" """Device hard reset invalidated our certificates (review trigger)."""
self._reauth_entry = self.hass.config_entries.async_get_entry( self._reauth_entry = self.hass.config_entries.async_get_entry(