Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure deCONZ component."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 import logging
8 from pprint import pformat
9 from typing import Any, cast
10 from urllib.parse import urlparse
11 
12 from pydeconz.errors import LinkButtonNotPressed, RequestError, ResponseError
13 from pydeconz.gateway import DeconzSession
14 from pydeconz.utils import (
15  DiscoveredBridge,
16  discovery as deconz_discovery,
17  get_bridge_id as deconz_get_bridge_id,
18  normalize_bridge_id,
19 )
20 import voluptuous as vol
21 
22 from homeassistant.components import ssdp
23 from homeassistant.config_entries import (
24  SOURCE_HASSIO,
25  ConfigEntry,
26  ConfigFlow,
27  ConfigFlowResult,
28  OptionsFlow,
29 )
30 from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
31 from homeassistant.core import HomeAssistant, callback
32 from homeassistant.helpers import aiohttp_client
33 from homeassistant.helpers.service_info.hassio import HassioServiceInfo
34 
35 from .const import (
36  CONF_ALLOW_CLIP_SENSOR,
37  CONF_ALLOW_DECONZ_GROUPS,
38  CONF_ALLOW_NEW_DEVICES,
39  DEFAULT_ALLOW_CLIP_SENSOR,
40  DEFAULT_ALLOW_DECONZ_GROUPS,
41  DEFAULT_ALLOW_NEW_DEVICES,
42  DEFAULT_PORT,
43  DOMAIN,
44  HASSIO_CONFIGURATION_URL,
45  LOGGER,
46 )
47 from .hub import DeconzHub
48 
49 DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de"
50 CONF_SERIAL = "serial"
51 CONF_MANUAL_INPUT = "Manually define gateway"
52 
53 
54 @callback
55 def get_master_hub(hass: HomeAssistant) -> DeconzHub:
56  """Return the gateway which is marked as master."""
57  for hub in hass.data[DOMAIN].values():
58  if hub.master:
59  return cast(DeconzHub, hub)
60  raise ValueError
61 
62 
63 class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
64  """Handle a deCONZ config flow."""
65 
66  VERSION = 1
67 
68  _hassio_discovery: dict[str, Any]
69 
70  bridges: list[DiscoveredBridge]
71  host: str
72  port: int
73  api_key: str
74 
75  @staticmethod
76  @callback
78  config_entry: ConfigEntry,
79  ) -> DeconzOptionsFlowHandler:
80  """Get the options flow for this handler."""
82 
83  def __init__(self) -> None:
84  """Initialize the deCONZ config flow."""
85  self.bridge_idbridge_id = ""
86 
87  async def async_step_user(
88  self, user_input: dict[str, Any] | None = None
89  ) -> ConfigFlowResult:
90  """Handle a deCONZ config flow start.
91 
92  Let user choose between discovered bridges and manual configuration.
93  If no bridge is found allow user to manually input configuration.
94  """
95  if user_input is not None:
96  if user_input[CONF_HOST] == CONF_MANUAL_INPUT:
97  return await self.async_step_manual_inputasync_step_manual_input()
98 
99  for bridge in self.bridgesbridges:
100  if bridge[CONF_HOST] == user_input[CONF_HOST]:
101  self.bridge_idbridge_id = bridge["id"]
102  self.hosthost = bridge[CONF_HOST]
103  self.portport = bridge[CONF_PORT]
104  return await self.async_step_linkasync_step_link()
105 
106  session = aiohttp_client.async_get_clientsession(self.hass)
107 
108  try:
109  async with asyncio.timeout(10):
110  self.bridgesbridges = await deconz_discovery(session)
111 
112  except (TimeoutError, ResponseError):
113  self.bridgesbridges = []
114 
115  if LOGGER.isEnabledFor(logging.DEBUG):
116  LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridgesbridges))
117 
118  if self.bridgesbridges:
119  hosts = [bridge[CONF_HOST] for bridge in self.bridgesbridges]
120 
121  hosts.append(CONF_MANUAL_INPUT)
122 
123  return self.async_show_formasync_show_formasync_show_form(
124  step_id="user",
125  data_schema=vol.Schema({vol.Optional(CONF_HOST): vol.In(hosts)}),
126  )
127 
128  return await self.async_step_manual_inputasync_step_manual_input()
129 
131  self, user_input: dict[str, Any] | None = None
132  ) -> ConfigFlowResult:
133  """Manual configuration."""
134  if user_input:
135  self.hosthost = user_input[CONF_HOST]
136  self.portport = user_input[CONF_PORT]
137  return await self.async_step_linkasync_step_link()
138 
139  return self.async_show_formasync_show_formasync_show_form(
140  step_id="manual_input",
141  data_schema=vol.Schema(
142  {
143  vol.Required(CONF_HOST): str,
144  vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
145  }
146  ),
147  )
148 
149  async def async_step_link(
150  self, user_input: dict[str, Any] | None = None
151  ) -> ConfigFlowResult:
152  """Attempt to link with the deCONZ bridge."""
153  errors: dict[str, str] = {}
154 
155  LOGGER.debug(
156  "Preparing linking with deCONZ gateway %s %d", self.hosthost, self.portport
157  )
158 
159  if user_input is not None:
160  session = aiohttp_client.async_get_clientsession(self.hass)
161  deconz_session = DeconzSession(session, self.hosthost, self.portport)
162 
163  try:
164  async with asyncio.timeout(10):
165  api_key = await deconz_session.get_api_key()
166 
167  except LinkButtonNotPressed:
168  errors["base"] = "linking_not_possible"
169 
170  except (ResponseError, RequestError, TimeoutError):
171  errors["base"] = "no_key"
172 
173  else:
174  self.api_keyapi_key = api_key
175  return await self._create_entry_create_entry()
176 
177  return self.async_show_formasync_show_formasync_show_form(step_id="link", errors=errors)
178 
179  async def _create_entry(self) -> ConfigFlowResult:
180  """Create entry for gateway."""
181  if not self.bridge_idbridge_id:
182  session = aiohttp_client.async_get_clientsession(self.hass)
183 
184  try:
185  async with asyncio.timeout(10):
186  self.bridge_idbridge_id = await deconz_get_bridge_id(
187  session, self.hosthost, self.portport, self.api_keyapi_key
188  )
189  await self.async_set_unique_idasync_set_unique_id(self.bridge_idbridge_id)
190 
191  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
192  updates={
193  CONF_HOST: self.hosthost,
194  CONF_PORT: self.portport,
195  CONF_API_KEY: self.api_keyapi_key,
196  }
197  )
198 
199  except TimeoutError:
200  return self.async_abortasync_abortasync_abort(reason="no_bridges")
201 
202  return self.async_create_entryasync_create_entryasync_create_entry(
203  title=self.bridge_idbridge_id,
204  data={
205  CONF_HOST: self.hosthost,
206  CONF_PORT: self.portport,
207  CONF_API_KEY: self.api_keyapi_key,
208  },
209  )
210 
211  async def async_step_reauth(
212  self, entry_data: Mapping[str, Any]
213  ) -> ConfigFlowResult:
214  """Trigger a reauthentication flow."""
215  self.context["title_placeholders"] = {CONF_HOST: entry_data[CONF_HOST]}
216 
217  self.hosthost = entry_data[CONF_HOST]
218  self.portport = entry_data[CONF_PORT]
219 
220  return await self.async_step_linkasync_step_link()
221 
222  async def async_step_ssdp(
223  self, discovery_info: ssdp.SsdpServiceInfo
224  ) -> ConfigFlowResult:
225  """Handle a discovered deCONZ bridge."""
226  if LOGGER.isEnabledFor(logging.DEBUG):
227  LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info))
228 
229  self.bridge_idbridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
230  parsed_url = urlparse(discovery_info.ssdp_location)
231 
232  entry = await self.async_set_unique_idasync_set_unique_id(self.bridge_idbridge_id)
233  if entry and entry.source == SOURCE_HASSIO:
234  return self.async_abortasync_abortasync_abort(reason="already_configured")
235 
236  self.hosthost = cast(str, parsed_url.hostname)
237  self.portport = cast(int, parsed_url.port)
238 
239  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
240  updates={
241  CONF_HOST: self.hosthost,
242  CONF_PORT: self.portport,
243  }
244  )
245 
246  self.context.update(
247  {
248  "title_placeholders": {"host": self.hosthost},
249  "configuration_url": f"http://{self.host}:{self.port}",
250  }
251  )
252 
253  return await self.async_step_linkasync_step_link()
254 
255  async def async_step_hassio(
256  self, discovery_info: HassioServiceInfo
257  ) -> ConfigFlowResult:
258  """Prepare configuration for a Hass.io deCONZ bridge.
259 
260  This flow is triggered by the discovery component.
261  """
262  if LOGGER.isEnabledFor(logging.DEBUG):
263  LOGGER.debug("deCONZ HASSIO discovery %s", pformat(discovery_info.config))
264 
265  self.bridge_idbridge_id = normalize_bridge_id(discovery_info.config[CONF_SERIAL])
266  await self.async_set_unique_idasync_set_unique_id(self.bridge_idbridge_id)
267 
268  self.hosthost = discovery_info.config[CONF_HOST]
269  self.portport = discovery_info.config[CONF_PORT]
270  self.api_keyapi_key = discovery_info.config[CONF_API_KEY]
271 
272  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
273  updates={
274  CONF_HOST: self.hosthost,
275  CONF_PORT: self.portport,
276  CONF_API_KEY: self.api_keyapi_key,
277  }
278  )
279 
280  self.context["configuration_url"] = HASSIO_CONFIGURATION_URL
281  self._hassio_discovery_hassio_discovery = discovery_info.config
282 
283  return await self.async_step_hassio_confirmasync_step_hassio_confirm()
284 
286  self, user_input: dict[str, Any] | None = None
287  ) -> ConfigFlowResult:
288  """Confirm a Hass.io discovery."""
289 
290  if user_input is not None:
291  return await self._create_entry_create_entry()
292 
293  return self.async_show_formasync_show_formasync_show_form(
294  step_id="hassio_confirm",
295  description_placeholders={"addon": self._hassio_discovery_hassio_discovery["addon"]},
296  )
297 
298 
300  """Handle deCONZ options."""
301 
302  gateway: DeconzHub
303 
304  async def async_step_init(
305  self, user_input: dict[str, Any] | None = None
306  ) -> ConfigFlowResult:
307  """Manage the deCONZ options."""
308  return await self.async_step_deconz_devicesasync_step_deconz_devices()
309 
311  self, user_input: dict[str, Any] | None = None
312  ) -> ConfigFlowResult:
313  """Manage the deconz devices options."""
314  if user_input is not None:
315  return self.async_create_entryasync_create_entry(data=self.config_entryconfig_entryconfig_entry.options | user_input)
316 
317  schema_options = {}
318  for option, default in (
319  (CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR),
320  (CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS),
321  (CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES),
322  ):
323  schema_options[
324  vol.Optional(
325  option,
326  default=self.config_entryconfig_entryconfig_entry.options.get(option, default),
327  )
328  ] = bool
329 
330  return self.async_show_formasync_show_form(
331  step_id="deconz_devices",
332  data_schema=vol.Schema(schema_options),
333  )
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:224
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:89
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:213
ConfigFlowResult async_step_manual_input(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:132
ConfigFlowResult async_step_hassio_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:287
ConfigFlowResult async_step_hassio(self, HassioServiceInfo discovery_info)
Definition: config_flow.py:257
DeconzOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:79
ConfigFlowResult async_step_link(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:151
ConfigFlowResult async_step_deconz_devices(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:312
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:306
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
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)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=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)
None config_entry(self, ConfigEntry value)
_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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
DeconzHub get_master_hub(HomeAssistant hass)
Definition: config_flow.py:55
IssData update(pyiss.ISS iss)
Definition: __init__.py:33