"""Config flow for Emby Media Player integration.""" from __future__ import annotations import logging from typing import Any import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow, ) from homeassistant.core import callback from homeassistant.helpers import instance_id from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( BooleanSelector, NumberSelector, NumberSelectorConfig, NumberSelectorMode, SelectSelector, SelectSelectorConfig, SelectSelectorMode, TextSelector, TextSelectorConfig, TextSelectorType, ) from .api import EmbyApiClient, EmbyAuthenticationError, EmbyConnectionError from .const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL, CONF_USER_ID, CONF_VERIFY_SSL, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, ) _LOGGER = logging.getLogger(__name__) def _make_temp_device_id(prefix: str) -> str: """Build a temporary device id used during config flow.""" return f"hass-config-{prefix[:12]}" class EmbyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Emby Media Player.""" VERSION = 1 def __init__(self) -> None: """Initialize the config flow.""" self._host: str | None = None self._port: int = DEFAULT_PORT self._api_key: str | None = None self._ssl: bool = DEFAULT_SSL self._verify_ssl: bool = DEFAULT_VERIFY_SSL self._users: list[dict[str, Any]] = [] self._server_info: dict[str, Any] = {} self._reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step - server connection.""" errors: dict[str, str] = {} if user_input is not None: self._host = user_input[CONF_HOST].strip() self._port = int(user_input.get(CONF_PORT, DEFAULT_PORT)) self._api_key = user_input[CONF_API_KEY].strip() self._ssl = user_input.get(CONF_SSL, DEFAULT_SSL) self._verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) errors = await self._probe() if not errors and self._users: return await self.async_step_user_select() if not errors: errors["base"] = "no_users" return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( CONF_HOST, default=self._host or vol.UNDEFINED ): TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)), vol.Optional( CONF_PORT, default=self._port ): NumberSelector( NumberSelectorConfig( min=1, max=65535, mode=NumberSelectorMode.BOX ) ), vol.Required(CONF_API_KEY): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), vol.Optional(CONF_SSL, default=self._ssl): BooleanSelector(), vol.Optional( CONF_VERIFY_SSL, default=self._verify_ssl ): BooleanSelector(), } ), errors=errors, ) async def _probe(self) -> dict[str, str]: """Try to connect & list users with the current self.* settings.""" assert self._host is not None assert self._api_key is not None errors: dict[str, str] = {} session = async_get_clientsession(self.hass) hass_uuid = await instance_id.async_get(self.hass) api = EmbyApiClient( host=self._host, port=self._port, api_key=self._api_key, ssl=self._ssl, verify_ssl=self._verify_ssl, session=session, device_id=_make_temp_device_id(hass_uuid), ) try: self._server_info = await api.test_connection() self._users = await api.get_users() except EmbyAuthenticationError: errors["base"] = "invalid_auth" except EmbyConnectionError: errors["base"] = "cannot_connect" except Exception: # noqa: BLE001 _LOGGER.exception("Unexpected error during Emby config probe") errors["base"] = "unknown" return errors async def async_step_user_select( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle user selection step.""" errors: dict[str, str] = {} if user_input is not None: user_id = user_input[CONF_USER_ID] user_name = next( (u["Name"] for u in self._users if u["Id"] == user_id), "Unknown", ) server_id = self._server_info.get("Id", self._host) await self.async_set_unique_id(f"{server_id}_{user_id}") self._abort_if_unique_id_configured() server_name = self._server_info.get("ServerName", self._host) return self.async_create_entry( title=f"{server_name} ({user_name})", data={ CONF_HOST: self._host, CONF_PORT: self._port, CONF_API_KEY: self._api_key, CONF_SSL: self._ssl, CONF_VERIFY_SSL: self._verify_ssl, CONF_USER_ID: user_id, }, options={ CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, }, ) user_options = [ {"value": user["Id"], "label": user.get("Name", "Unknown")} for user in self._users ] return self.async_show_form( step_id="user_select", data_schema=vol.Schema( { vol.Required(CONF_USER_ID): SelectSelector( SelectSelectorConfig( options=user_options, mode=SelectSelectorMode.DROPDOWN, ) ), } ), errors=errors, ) # ------------------------------------------------------------------------- # Reauthentication # ------------------------------------------------------------------------- def _resolve_reauth_entry(self) -> ConfigEntry | None: """Resolve the reauth target entry from context. Uses ``_get_reauth_entry`` when available (HA 2024.11+), falling back to the documented context dict. """ getter = getattr(self, "_get_reauth_entry", None) if callable(getter): try: return getter() except Exception as err: # noqa: BLE001 - version-specific _LOGGER.debug( "_get_reauth_entry helper unavailable, falling back: %s", err, ) entry_id = self.context.get("entry_id") if not entry_id: return None return self.hass.config_entries.async_get_entry(entry_id) async def async_step_reauth( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the reauth flow.""" self._reauth_entry = self._resolve_reauth_entry() if self._reauth_entry is not None: self._host = self._reauth_entry.data.get(CONF_HOST) self._port = int( self._reauth_entry.data.get(CONF_PORT, DEFAULT_PORT) ) self._ssl = self._reauth_entry.data.get(CONF_SSL, DEFAULT_SSL) self._verify_ssl = self._reauth_entry.data.get( CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm reauthentication with a new API key.""" errors: dict[str, str] = {} if user_input is not None and self._reauth_entry is not None: self._api_key = user_input[CONF_API_KEY].strip() errors = await self._probe() if not errors: new_data = { **self._reauth_entry.data, CONF_API_KEY: self._api_key, } # Prefer the unified helper when available; fall back manually. helper = getattr( self, "async_update_reload_and_abort", None ) if callable(helper): return helper( self._reauth_entry, data=new_data, reason="reauth_successful", ) self.hass.config_entries.async_update_entry( self._reauth_entry, data=new_data ) await self.hass.config_entries.async_reload( self._reauth_entry.entry_id ) return self.async_abort(reason="reauth_successful") return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema( { vol.Required(CONF_API_KEY): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), } ), errors=errors, description_placeholders={ "host": self._host or "", }, ) @staticmethod @callback def async_get_options_flow( config_entry: ConfigEntry, ) -> EmbyOptionsFlow: """Get the options flow for this handler.""" return EmbyOptionsFlow(config_entry) class EmbyOptionsFlow(OptionsFlow): """Handle options flow for Emby Media Player.""" def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self._config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) current_interval = self._config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ) return self.async_show_form( step_id="init", data_schema=vol.Schema( { vol.Optional( CONF_SCAN_INTERVAL, default=current_interval ): NumberSelector( NumberSelectorConfig( min=5, max=60, step=1, mode=NumberSelectorMode.SLIDER, unit_of_measurement="seconds", ) ), } ), )