Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for roon integration."""
2 
3 import asyncio
4 import logging
5 from typing import Any
6 
7 from roonapi import RoonApi, RoonDiscovery
8 import voluptuous as vol
9 
10 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
11 from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
12 from homeassistant.core import HomeAssistant
13 from homeassistant.exceptions import HomeAssistantError
15 
16 from .const import (
17  AUTHENTICATE_TIMEOUT,
18  CONF_ROON_ID,
19  CONF_ROON_NAME,
20  DEFAULT_NAME,
21  DOMAIN,
22  ROON_APPINFO,
23 )
24 
25 _LOGGER = logging.getLogger(__name__)
26 
27 DATA_SCHEMA = vol.Schema(
28  {
29  vol.Required("host"): cv.string,
30  vol.Required("port", default=9330): cv.port,
31  }
32 )
33 
34 TIMEOUT = 120
35 
36 
37 class RoonHub:
38  """Interact with roon during config flow."""
39 
40  def __init__(self, hass: HomeAssistant) -> None:
41  """Initialise the RoonHub."""
42  self._hass_hass = hass
43 
44  async def discover(self) -> list[tuple[str, int]]:
45  """Try and discover roon servers."""
46 
47  def get_discovered_servers(discovery: RoonDiscovery) -> list[tuple[str, int]]:
48  servers = discovery.all()
49  discovery.stop()
50  return servers
51 
52  discovery = RoonDiscovery(None)
53  servers = await self._hass_hass.async_add_executor_job(
54  get_discovered_servers, discovery
55  )
56  _LOGGER.debug("Servers = %s", servers)
57  return servers
58 
59  async def authenticate(self, host, port, servers):
60  """Authenticate with one or more roon servers."""
61 
62  def stop_apis(apis):
63  for api in apis:
64  api.stop()
65 
66  token = None
67  core_id = None
68  core_name = None
69  secs = 0
70  if host is None:
71  apis = [
72  RoonApi(ROON_APPINFO, None, server[0], server[1], blocking_init=False)
73  for server in servers
74  ]
75  else:
76  apis = [RoonApi(ROON_APPINFO, None, host, port, blocking_init=False)]
77 
78  while secs <= TIMEOUT:
79  # Roon can discover multiple devices - not all of which are proper servers, so try and authenticate with them all.
80  # The user will only enable one - so look for a valid token
81  auth_api = [api for api in apis if api.token is not None]
82 
83  secs += AUTHENTICATE_TIMEOUT
84  if auth_api:
85  core_id = auth_api[0].core_id
86  core_name = auth_api[0].core_name
87  token = auth_api[0].token
88  break
89 
90  await asyncio.sleep(AUTHENTICATE_TIMEOUT)
91 
92  await self._hass_hass.async_add_executor_job(stop_apis, apis)
93 
94  return (token, core_id, core_name)
95 
96 
97 async def discover(hass: HomeAssistant) -> list[tuple[str, int]]:
98  """Connect and authenticate home assistant."""
99 
100  hub = RoonHub(hass)
101  return await hub.discover()
102 
103 
104 async def authenticate(hass: HomeAssistant, host, port, servers):
105  """Connect and authenticate home assistant."""
106 
107  hub = RoonHub(hass)
108  (token, core_id, core_name) = await hub.authenticate(host, port, servers)
109  if token is None:
110  raise InvalidAuth
111 
112  return {
113  CONF_HOST: host,
114  CONF_PORT: port,
115  CONF_ROON_ID: core_id,
116  CONF_ROON_NAME: core_name,
117  CONF_API_KEY: token,
118  }
119 
120 
121 class RoonConfigFlow(ConfigFlow, domain=DOMAIN):
122  """Handle a config flow for roon."""
123 
124  VERSION = 1
125 
126  def __init__(self) -> None:
127  """Initialize the Roon flow."""
128  self._host_host = None
129  self._port_port = None
130  self._servers_servers: list[tuple[str, int]] = []
131 
132  async def async_step_user(
133  self, user_input: dict[str, Any] | None = None
134  ) -> ConfigFlowResult:
135  """Get roon core details via discovery."""
136 
137  self._servers_servers = await discover(self.hass)
138 
139  # We discovered one or more roon - so skip to authentication
140  if self._servers_servers:
141  return await self.async_step_linkasync_step_link()
142 
143  return await self.async_step_fallbackasync_step_fallback()
144 
146  self, user_input: dict[str, Any] | None = None
147  ) -> ConfigFlowResult:
148  """Get host and port details from the user."""
149  errors: dict[str, str] = {}
150 
151  if user_input is not None:
152  self._host_host = user_input["host"]
153  self._port_port = user_input["port"]
154  return await self.async_step_linkasync_step_link()
155 
156  return self.async_show_formasync_show_formasync_show_form(
157  step_id="fallback", data_schema=DATA_SCHEMA, errors=errors
158  )
159 
160  async def async_step_link(
161  self, user_input: dict[str, Any] | None = None
162  ) -> ConfigFlowResult:
163  """Handle linking and authenticating with the roon server."""
164  errors = {}
165  if user_input is not None:
166  # Do not authenticate if the host is already configured
167  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: self._host_host})
168 
169  try:
170  info = await authenticate(
171  self.hass, self._host_host, self._port_port, self._servers_servers
172  )
173 
174  except InvalidAuth:
175  errors["base"] = "invalid_auth"
176  except Exception:
177  _LOGGER.exception("Unexpected exception")
178  errors["base"] = "unknown"
179  else:
180  return self.async_create_entryasync_create_entryasync_create_entry(title=DEFAULT_NAME, data=info)
181 
182  return self.async_show_formasync_show_formasync_show_form(step_id="link", errors=errors)
183 
184 
186  """Error to indicate there is invalid auth."""
ConfigFlowResult async_step_link(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:162
ConfigFlowResult async_step_fallback(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:147
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:134
None __init__(self, HomeAssistant hass)
Definition: config_flow.py:40
def authenticate(self, host, port, servers)
Definition: config_flow.py:59
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
def authenticate(HomeAssistant hass, host, port, servers)
Definition: config_flow.py:104
list[tuple[str, int]] discover(HomeAssistant hass)
Definition: config_flow.py:97