"""The Nvidia Shield integration. Phase 1 architecture (docs/MVP_PLAN.md §2): the entry owns a single ShieldConnectionManager stored in ``entry.runtime_data`` — the persistent ADB socket and all ADB-derived state live there, not on the entities. Entities are thin callback-views over the manager's StateStore. ADB is optional: a failed/absent ADB connection never blocks setup (no ConfigEntryNotReady) — the manager retries in the background, or stays passive until the user pairs via the Pair ADB button. """ import logging import os from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from .adb_session import AdbMediaSessionPoller from .connection import ShieldConnectionManager from .const import ( CONF_ADB_ENABLED, CONF_ANDROIDTV_ENTITY, CONF_PROTOCOL_ENABLED, CONF_PROTOCOL_HOST, DOMAIN, SERVICE_ADB_COMMAND, ) from .helpers import discover_adb_host, protocol_cert_paths, resolve_adb_config _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.REMOTE, Platform.BUTTON] async def async_setup(hass: HomeAssistant, config) -> bool: """Register the integration's services (t29 G8, always-on). ``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'. """ from .media_player import _ADB_COMMAND_SCHEMA, _async_adb_command_service hass.services.async_register( DOMAIN, SERVICE_ADB_COMMAND, _async_adb_command_service, schema=_ADB_COMMAND_SCHEMA, ) return True async def _build_poller_factory(hass: HomeAssistant, entry: ConfigEntry): """Poller factory when ADB is configured AND usable today, else None. Returning None leaves the manager passive: the entry still sets up, the entities simply see no ADB state. The Pair ADB button / reconfigure triggers a reload that re-evaluates this. """ if not entry.options.get(CONF_ADB_ENABLED, False): return None adb = resolve_adb_config(hass, entry) if adb is None: _LOGGER.warning( "ADB enabled but no host resolvable for entry %s — continuing " "without ADB state.", entry.title, ) return None # Only auto-connect when a key already exists — an ADB handshake against # an unpaired device is pointless noise (pairing happens via the Pair ADB # button / config flow, untouched). key_path_ready = await hass.async_add_executor_job( os.path.exists, adb.key_path ) if not key_path_ready: _LOGGER.info( "ADB key not found for entry %s — skipping auto-connect. " "Use the Pair ADB button or reconfigure the integration.", entry.title, ) return None return lambda: AdbMediaSessionPoller(adb.host, adb.port, adb.key_path) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nvidia Shield from a config entry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = entry manager = ShieldConnectionManager( hass, entry, poller_factory=await _build_poller_factory(hass, entry), protocol_settings=await _build_protocol_settings(hass, entry), invalid_auth_callback=lambda: entry.async_start_reauth(hass), ) entry.runtime_data = manager await manager.async_start() # Close the persistent socket on Home Assistant stop as well as unload. async def _close_on_hass_stop(_event) -> None: await manager.async_stop() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close_on_hass_stop) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True async def _build_protocol_settings(hass: HomeAssistant, entry: ConfigEntry): """Manager protocol_settings when the transport is enabled & resolvable. Falls back to the androidtv-remote host discovery (the ADB host fallback) until the user pairs, after which the explicit protocol host persists. """ if not entry.options.get(CONF_PROTOCOL_ENABLED, False): return None host = entry.options.get(CONF_PROTOCOL_HOST) or discover_adb_host( hass, entry.data.get(CONF_ANDROIDTV_ENTITY) ) if not host: _LOGGER.info( "Protocol enabled for entry %s but no host resolvable — continuing " "without the remote-protocol transport. Pair via the flow.", entry.title, ) return None certfile, keyfile = await hass.async_add_executor_job(protocol_cert_paths, hass) return { "host": host, "client_name": entry.title or "Home Assistant", "certfile": certfile, "keyfile": keyfile, } async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate v1 entries to v2 (adds the remote-protocol fields). ADB options carry over untouched; the ADB-host discovery fallback keeps serving as the protocol host until pairing populates it. Downgrades (version > 2) are rejected so HA offers a rollback. """ if config_entry.version > 2: return False if config_entry.version < 2: options = {**config_entry.options} options.setdefault(CONF_PROTOCOL_ENABLED, False) options.setdefault(CONF_PROTOCOL_HOST, "") hass.config_entries.async_update_entry( config_entry, options=options, version=2 ) _LOGGER.info( "Migrated %s config entry to version 2", config_entry.title ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: manager = getattr(entry, "runtime_data", None) if manager is not None: await manager.async_stop() entry.runtime_data = None hass.data[DOMAIN].pop(entry.entry_id) return unload_ok async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id)