diff --git a/custom_components/nvidia_shield/__init__.py b/custom_components/nvidia_shield/__init__.py index e169985..b830403 100644 --- a/custom_components/nvidia_shield/__init__.py +++ b/custom_components/nvidia_shield/__init__.py @@ -13,6 +13,7 @@ passive until the user pairs via the Pair ADB button. import logging import os +from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant @@ -21,9 +22,13 @@ from .adb_session import AdbMediaSessionPoller from .connection import ShieldConnectionManager from .const import ( CONF_ADB_ENABLED, + CONF_ADB_HOST, + CONF_ADB_PORT, CONF_ANDROIDTV_ENTITY, CONF_PROTOCOL_ENABLED, CONF_PROTOCOL_HOST, + DEFAULT_ADB_PORT, + DEFAULT_NAME, DOMAIN, 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: - """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 — - the entry's adb_command_enabled option gates USE (participants self- - register from the media_player platform), so automations validate and - errors are informative instead of 'service not found'. + YAML is SEED-ONLY (user ruling): every ``nvidia_shield`` block starts an + interactive SOURCE_IMPORT config flow — nothing is created without the + user walking the flow, and pairing stays interactive exactly as for UI + 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", ""), + ) + 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 hass.services.async_register( @@ -53,6 +75,28 @@ async def async_setup(hass: HomeAssistant, config) -> bool: 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): """Poller factory when ADB is configured AND usable today, else None. diff --git a/custom_components/nvidia_shield/config_flow.py b/custom_components/nvidia_shield/config_flow.py index bac1f18..637aafb 100644 --- a/custom_components/nvidia_shield/config_flow.py +++ b/custom_components/nvidia_shield/config_flow.py @@ -471,6 +471,54 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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): """Device hard reset invalidated our certificates (review trigger).""" self._reauth_entry = self.hass.config_entries.async_get_entry(