"""Config flow for Nvidia Shield integration.""" import logging import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers import selector from .adb_session import ensure_adb_key from .connection import ShieldConnectionManager from .const import ( ATTR_MEDIA_DAG_INPUT_MAP, CONF_ADB_ENABLED, CONF_ADB_HOST, CONF_ADB_PORT, CONF_ADB_STORE_APP_INFO, CONF_ANDROIDTV_ENTITY, CONF_JELLYFIN_ENTITY, CONF_POWER_ENTITY, CONF_RECEIVER_ENTITY, CONF_RECEIVER_INPUT, DEFAULT_ADB_PORT, DEFAULT_NAME, DOMAIN, ) from .helpers import discover_adb_host _LOGGER = logging.getLogger(__name__) def _get_receiver_inputs(hass, receiver_entity_id: str | None) -> list[str]: """Read available inputs from a receiver's media_dag_input_map attribute.""" if not receiver_entity_id: return [] state = hass.states.get(receiver_entity_id) if not state: return [] input_map = state.attributes.get(ATTR_MEDIA_DAG_INPUT_MAP, {}) return list(input_map.keys()) if isinstance(input_map, dict) else [] def _find_jellyfin_entity(hass, device_name: str) -> str | None: """Auto-detect the Jellyfin media_player entity for a device.""" registry = er.async_get(hass) device_name_lower = device_name.lower() candidates = [] for entry in registry.entities.values(): if entry.domain != "media_player" or entry.platform != "jellyfin": continue if entry.disabled_by is not None: continue eid_lower = entry.entity_id.lower() name_lower = (entry.original_name or "").lower() if device_name_lower in eid_lower or device_name_lower in name_lower: state = hass.states.get(entry.entity_id) if state and state.state != "unavailable": candidates.append(entry.entity_id) if candidates: if len(candidates) > 1: _LOGGER.warning( "Multiple Jellyfin entities found for %s: %s. Using first.", device_name, candidates, ) return candidates[0] return None def _build_selectors(): """Build reusable selector instances.""" return { "androidtv": selector.EntitySelector( selector.EntitySelectorConfig(domain="media_player", device_class="tv") ), "media_player": selector.EntitySelector( selector.EntitySelectorConfig(domain="media_player") ), "receiver": selector.EntitySelector( selector.EntitySelectorConfig(domain="media_player", device_class="receiver") ), "power": selector.EntitySelector( selector.EntitySelectorConfig(domain=["switch", "binary_sensor"]) ), } class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Nvidia Shield.""" VERSION = 1 def __init__(self): self._user_data = {} async def async_step_user(self, user_input=None): """Step 1: Basic setup.""" errors = {} if user_input is not None: androidtv_entity = user_input[CONF_ANDROIDTV_ENTITY] await self.async_set_unique_id(androidtv_entity) self._abort_if_unique_id_configured() # Auto-detect Jellyfin if not manually specified jellyfin = user_input.get(CONF_JELLYFIN_ENTITY) if not jellyfin: device_name = user_input.get("name", DEFAULT_NAME) atv_state = self.hass.states.get(androidtv_entity) atv_name = (atv_state.attributes.get("friendly_name", "") if atv_state else "") for name in [device_name, atv_name]: if not name: continue found = _find_jellyfin_entity(self.hass, name) if found: jellyfin = found _LOGGER.info("Auto-detected Jellyfin entity: %s", found) break self._user_data = { "name": user_input.get("name", DEFAULT_NAME), CONF_ANDROIDTV_ENTITY: androidtv_entity, CONF_JELLYFIN_ENTITY: jellyfin or "", CONF_RECEIVER_ENTITY: user_input.get(CONF_RECEIVER_ENTITY, ""), CONF_RECEIVER_INPUT: user_input.get(CONF_RECEIVER_INPUT, ""), CONF_POWER_ENTITY: user_input.get(CONF_POWER_ENTITY, ""), CONF_ADB_ENABLED: user_input.get(CONF_ADB_ENABLED, False), CONF_ADB_STORE_APP_INFO: user_input.get(CONF_ADB_STORE_APP_INFO, True), CONF_ADB_HOST: user_input.get(CONF_ADB_HOST, ""), CONF_ADB_PORT: user_input.get(CONF_ADB_PORT, DEFAULT_ADB_PORT), } # If ADB is enabled, go to pairing step if self._user_data[CONF_ADB_ENABLED]: return await self.async_step_adb_pair() return self._create_entry() sel = _build_selectors() schema_dict = { vol.Required("name", default=DEFAULT_NAME): str, vol.Required(CONF_ANDROIDTV_ENTITY): sel["androidtv"], vol.Optional(CONF_JELLYFIN_ENTITY): sel["media_player"], vol.Optional(CONF_RECEIVER_ENTITY): sel["receiver"], } # If there are any receivers with DAG input maps, show a text field # for receiver input (will be a dropdown in options once receiver is set) schema_dict[vol.Optional(CONF_RECEIVER_INPUT)] = str schema_dict[vol.Optional(CONF_POWER_ENTITY)] = sel["power"] schema_dict[vol.Optional(CONF_ADB_ENABLED, default=False)] = bool schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO, default=True)] = bool schema_dict[vol.Optional(CONF_ADB_HOST)] = str schema_dict[vol.Optional(CONF_ADB_PORT, default=DEFAULT_ADB_PORT)] = int return self.async_show_form( step_id="user", data_schema=vol.Schema(schema_dict), errors=errors, ) def _create_entry(self): """Create the config entry from stored user data.""" d = self._user_data return self.async_create_entry( title=d["name"], data={CONF_ANDROIDTV_ENTITY: d[CONF_ANDROIDTV_ENTITY]}, options={ CONF_JELLYFIN_ENTITY: d.get(CONF_JELLYFIN_ENTITY, ""), CONF_RECEIVER_ENTITY: d.get(CONF_RECEIVER_ENTITY, ""), CONF_RECEIVER_INPUT: d.get(CONF_RECEIVER_INPUT, ""), CONF_POWER_ENTITY: d.get(CONF_POWER_ENTITY, ""), CONF_ADB_ENABLED: d.get(CONF_ADB_ENABLED, False), CONF_ADB_STORE_APP_INFO: d.get(CONF_ADB_STORE_APP_INFO, True), CONF_ADB_HOST: d.get(CONF_ADB_HOST, ""), CONF_ADB_PORT: d.get(CONF_ADB_PORT, DEFAULT_ADB_PORT), }, ) def _resolve_adb_host(self) -> str | None: """Resolve ADB host from explicit config or the Android TV entity.""" return self._user_data.get(CONF_ADB_HOST) or discover_adb_host( self.hass, self._user_data.get(CONF_ANDROIDTV_ENTITY) ) async def async_step_adb_pair(self, user_input=None): """Step 2: ADB pairing. User clicks Submit after approving on the Shield.""" errors = {} if user_input is not None: host = self._resolve_adb_host() port = self._user_data.get(CONF_ADB_PORT, DEFAULT_ADB_PORT) if not host: errors["base"] = "adb_no_host" else: key_path = await self.hass.async_add_executor_job( ensure_adb_key, self.hass.config.config_dir ) connected = await ShieldConnectionManager.test_connect( host, port, key_path ) if connected: return self._create_entry() else: errors["base"] = "adb_failed" return self.async_show_form( step_id="adb_pair", data_schema=vol.Schema({}), errors=errors, ) @staticmethod @callback def async_get_options_flow(config_entry): return NvidiaShieldOptionsFlow(config_entry) class NvidiaShieldOptionsFlow(config_entries.OptionsFlow): """Handle options for Nvidia Shield — single unified page.""" def __init__(self, config_entry): self._config_entry = config_entry self._options = dict(config_entry.options) async def async_step_init(self, user_input=None): """Single-page options.""" if user_input is not None: self._options.update(user_input) return self.async_create_entry(title="", data=self._options) sel = _build_selectors() schema_dict = {} # Jellyfin jf = self._options.get(CONF_JELLYFIN_ENTITY) jf_key = vol.Optional(CONF_JELLYFIN_ENTITY) if jf: jf_key = vol.Optional(CONF_JELLYFIN_ENTITY, default=jf) schema_dict[jf_key] = sel["media_player"] # Receiver recv = self._options.get(CONF_RECEIVER_ENTITY) recv_key = vol.Optional(CONF_RECEIVER_ENTITY) if recv: recv_key = vol.Optional(CONF_RECEIVER_ENTITY, default=recv) schema_dict[recv_key] = sel["receiver"] # Receiver input — dropdown from DAG if receiver is set, text otherwise current_input = self._options.get(CONF_RECEIVER_INPUT, "") receiver = self._options.get(CONF_RECEIVER_ENTITY) inputs = _get_receiver_inputs(self.hass, receiver) if inputs: input_sel = selector.SelectSelector( selector.SelectSelectorConfig( options=inputs, mode=selector.SelectSelectorMode.DROPDOWN, custom_value=True, ) ) input_key = vol.Optional(CONF_RECEIVER_INPUT) if current_input: input_key = vol.Optional(CONF_RECEIVER_INPUT, default=current_input) schema_dict[input_key] = input_sel else: schema_dict[vol.Optional( CONF_RECEIVER_INPUT, default=current_input )] = str # Power pwr = self._options.get(CONF_POWER_ENTITY) pwr_key = vol.Optional(CONF_POWER_ENTITY) if pwr: pwr_key = vol.Optional(CONF_POWER_ENTITY, default=pwr) schema_dict[pwr_key] = sel["power"] # ADB options schema_dict[vol.Optional( CONF_ADB_ENABLED, default=self._options.get(CONF_ADB_ENABLED, False), )] = bool schema_dict[vol.Optional( CONF_ADB_STORE_APP_INFO, default=self._options.get(CONF_ADB_STORE_APP_INFO, True), )] = bool # ADB host/port (optional overrides, shown when ADB is enabled) adb_host = self._options.get(CONF_ADB_HOST, "") schema_dict[vol.Optional( CONF_ADB_HOST, default=adb_host, )] = str schema_dict[vol.Optional( CONF_ADB_PORT, default=self._options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT), )] = int return self.async_show_form( step_id="init", data_schema=vol.Schema(schema_dict), )