"""Config flow for roon integration.""" import asyncio import logging from roonapi import RoonApi, RoonDiscovery import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv from .const import ( AUTHENTICATE_TIMEOUT, CONF_ROON_ID, CONF_ROON_NAME, DEFAULT_NAME, DOMAIN, ROON_APPINFO, ) _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( { vol.Required("host"): cv.string, vol.Required("port", default=9330): cv.port, } ) TIMEOUT = 120 class RoonHub: """Interact with roon during config flow.""" def __init__(self, hass): """Initialise the RoonHub.""" self._hass = hass async def discover(self): """Try and discover roon servers.""" def get_discovered_servers(discovery): servers = discovery.all() discovery.stop() return servers discovery = RoonDiscovery(None) servers = await self._hass.async_add_executor_job( get_discovered_servers, discovery ) _LOGGER.debug("Servers = %s", servers) return servers async def authenticate(self, host, port, servers): """Authenticate with one or more roon servers.""" def stop_apis(apis): for api in apis: api.stop() token = None core_id = None core_name = None secs = 0 if host is None: apis = [ RoonApi(ROON_APPINFO, None, server[0], server[1], blocking_init=False) for server in servers ] else: apis = [RoonApi(ROON_APPINFO, None, host, port, blocking_init=False)] while secs <= TIMEOUT: # Roon can discover multiple devices - not all of which are proper servers, so try and authenticate with them all. # The user will only enable one - so look for a valid token auth_api = [api for api in apis if api.token is not None] secs += AUTHENTICATE_TIMEOUT if auth_api: core_id = auth_api[0].core_id core_name = auth_api[0].core_name token = auth_api[0].token break await asyncio.sleep(AUTHENTICATE_TIMEOUT) await self._hass.async_add_executor_job(stop_apis, apis) return (token, core_id, core_name) async def discover(hass): """Connect and authenticate home assistant.""" hub = RoonHub(hass) servers = await hub.discover() return servers async def authenticate(hass: core.HomeAssistant, host, port, servers): """Connect and authenticate home assistant.""" hub = RoonHub(hass) (token, core_id, core_name) = await hub.authenticate(host, port, servers) if token is None: raise InvalidAuth return { CONF_HOST: host, CONF_PORT: port, CONF_ROON_ID: core_id, CONF_ROON_NAME: core_name, CONF_API_KEY: token, } class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for roon.""" VERSION = 1 def __init__(self): """Initialize the Roon flow.""" self._host = None self._port = None self._servers = [] async def async_step_user(self, user_input=None): """Get roon core details via discovery.""" self._servers = await discover(self.hass) # We discovered one or more roon - so skip to authentication if self._servers: return await self.async_step_link() return await self.async_step_fallback() async def async_step_fallback(self, user_input=None): """Get host and port details from the user.""" errors = {} if user_input is not None: self._host = user_input["host"] self._port = user_input["port"] return await self.async_step_link() return self.async_show_form( step_id="fallback", data_schema=DATA_SCHEMA, errors=errors ) async def async_step_link(self, user_input=None): """Handle linking and authenticting with the roon server.""" errors = {} if user_input is not None: # Do not authenticate if the host is already configured self._async_abort_entries_match({CONF_HOST: self._host}) try: info = await authenticate( self.hass, self._host, self._port, self._servers ) except InvalidAuth: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return self.async_create_entry(title=DEFAULT_NAME, data=info) return self.async_show_form(step_id="link", errors=errors) class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth."""