Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure Philips Hue."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import logging
7 from typing import Any
8 
9 import aiohttp
10 from aiohue import LinkButtonNotPressed, create_app_key
11 from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp
12 from aiohue.util import normalize_bridge_id
13 import slugify as unicode_slug
14 import voluptuous as vol
15 
16 from homeassistant.components import zeroconf
17 from homeassistant.config_entries import (
18  ConfigEntry,
19  ConfigFlow,
20  ConfigFlowResult,
21  OptionsFlow,
22 )
23 from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST
24 from homeassistant.core import callback
25 from homeassistant.helpers import (
26  aiohttp_client,
27  config_validation as cv,
28  device_registry as dr,
29 )
30 
31 from .const import (
32  CONF_ALLOW_HUE_GROUPS,
33  CONF_ALLOW_UNREACHABLE,
34  CONF_IGNORE_AVAILABILITY,
35  DEFAULT_ALLOW_HUE_GROUPS,
36  DEFAULT_ALLOW_UNREACHABLE,
37  DOMAIN,
38 )
39 from .errors import CannotConnect
40 
41 LOGGER = logging.getLogger(__name__)
42 
43 HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com")
44 HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"]
45 HUE_MANUAL_BRIDGE_ID = "manual"
46 
47 
48 class HueFlowHandler(ConfigFlow, domain=DOMAIN):
49  """Handle a Hue config flow."""
50 
51  VERSION = 1
52 
53  @staticmethod
54  @callback
56  config_entry: ConfigEntry,
57  ) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
58  """Get the options flow for this handler."""
59  if config_entry.data.get(CONF_API_VERSION, 1) == 1:
62 
63  def __init__(self) -> None:
64  """Initialize the Hue flow."""
65  self.bridgebridge: DiscoveredHueBridge | None = None
66  self.discovered_bridgesdiscovered_bridges: dict[str, DiscoveredHueBridge] | None = None
67 
68  async def async_step_user(
69  self, user_input: dict[str, Any] | None = None
70  ) -> ConfigFlowResult:
71  """Handle a flow initialized by the user."""
72  # This is for backwards compatibility.
73  return await self.async_step_initasync_step_init(user_input)
74 
75  async def _get_bridge(
76  self, host: str, bridge_id: str | None = None
77  ) -> DiscoveredHueBridge:
78  """Return a DiscoveredHueBridge object."""
79  try:
80  bridge = await discover_bridge(
81  host, websession=aiohttp_client.async_get_clientsession(self.hass)
82  )
83  except aiohttp.ClientError as err:
84  LOGGER.warning(
85  "Error while attempting to retrieve discovery information, "
86  "is there a bridge alive on IP %s ?",
87  host,
88  exc_info=err,
89  )
90  return None
91  if bridge_id is not None:
92  bridge_id = normalize_bridge_id(bridge_id)
93  assert bridge_id == bridge.id
94  return bridge
95 
96  async def async_step_init(
97  self, user_input: dict[str, Any] | None = None
98  ) -> ConfigFlowResult:
99  """Handle a flow start."""
100  # Check if user chooses manual entry
101  if user_input is not None and user_input["id"] == HUE_MANUAL_BRIDGE_ID:
102  return await self.async_step_manualasync_step_manual()
103 
104  if (
105  user_input is not None
106  and self.discovered_bridgesdiscovered_bridges is not None
107  and user_input["id"] in self.discovered_bridgesdiscovered_bridges
108  ):
109  self.bridgebridge = self.discovered_bridgesdiscovered_bridges[user_input["id"]]
110  await self.async_set_unique_idasync_set_unique_id(self.bridgebridge.id, raise_on_progress=False)
111  return await self.async_step_linkasync_step_link()
112 
113  # Find / discover bridges
114  try:
115  async with asyncio.timeout(5):
116  bridges = await discover_nupnp(
117  websession=aiohttp_client.async_get_clientsession(self.hass)
118  )
119  except TimeoutError:
120  bridges = []
121 
122  if bridges:
123  # Find already configured hosts
124  already_configured = self._async_current_ids_async_current_ids(False)
125  bridges = [
126  bridge for bridge in bridges if bridge.id not in already_configured
127  ]
128  self.discovered_bridgesdiscovered_bridges = {bridge.id: bridge for bridge in bridges}
129 
130  if not self.discovered_bridgesdiscovered_bridges:
131  return await self.async_step_manualasync_step_manual()
132 
133  return self.async_show_formasync_show_formasync_show_form(
134  step_id="init",
135  data_schema=vol.Schema(
136  {
137  vol.Required("id"): vol.In(
138  {
139  **{bridge.id: bridge.host for bridge in bridges},
140  HUE_MANUAL_BRIDGE_ID: "Manually add a Hue Bridge",
141  }
142  )
143  }
144  ),
145  )
146 
147  async def async_step_manual(
148  self, user_input: dict[str, Any] | None = None
149  ) -> ConfigFlowResult:
150  """Handle manual bridge setup."""
151  if user_input is None:
152  return self.async_show_formasync_show_formasync_show_form(
153  step_id="manual",
154  data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
155  )
156 
157  self._async_abort_entries_match_async_abort_entries_match({"host": user_input["host"]})
158  if (bridge := await self._get_bridge_get_bridge(user_input[CONF_HOST])) is None:
159  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
160  self.bridgebridge = bridge
161  return await self.async_step_linkasync_step_link()
162 
163  async def async_step_link(
164  self, user_input: dict[str, Any] | None = None
165  ) -> ConfigFlowResult:
166  """Attempt to link with the Hue bridge.
167 
168  Given a configured host, will ask the user to press the link button
169  to connect to the bridge.
170  """
171  if user_input is None:
172  return self.async_show_formasync_show_formasync_show_form(step_id="link")
173 
174  bridge = self.bridgebridge
175  assert bridge is not None
176  errors = {}
177  device_name = unicode_slug.slugify(
178  self.hass.config.location_name, max_length=19
179  )
180 
181  try:
182  app_key = await create_app_key(
183  bridge.host,
184  f"home-assistant#{device_name}",
185  websession=aiohttp_client.async_get_clientsession(self.hass),
186  )
187  except LinkButtonNotPressed:
188  errors["base"] = "register_failed"
189  except CannotConnect:
190  LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host)
191  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
192  except Exception:
193  LOGGER.exception(
194  "Unknown error connecting with Hue bridge at %s", bridge.host
195  )
196  errors["base"] = "linking"
197 
198  if errors:
199  return self.async_show_formasync_show_formasync_show_form(step_id="link", errors=errors)
200 
201  # Can happen if we come from import or manual entry
202  if self.unique_idunique_id is None:
203  await self.async_set_unique_idasync_set_unique_id(
204  normalize_bridge_id(bridge.id), raise_on_progress=False
205  )
206 
207  return self.async_create_entryasync_create_entryasync_create_entry(
208  title=f"Hue Bridge {bridge.id}",
209  data={
210  CONF_HOST: bridge.host,
211  CONF_API_KEY: app_key,
212  CONF_API_VERSION: 2 if bridge.supports_v2 else 1,
213  },
214  )
215 
217  self, discovery_info: zeroconf.ZeroconfServiceInfo
218  ) -> ConfigFlowResult:
219  """Handle a discovered Hue bridge.
220 
221  This flow is triggered by the Zeroconf component. It will check if the
222  host is already configured and delegate to the import step if not.
223  """
224  # Ignore if host is IPv6
225  if discovery_info.ip_address.version == 6:
226  return self.async_abortasync_abortasync_abort(reason="invalid_host")
227 
228  # abort if we already have exactly this bridge id/host
229  # reload the integration if the host got updated
230  bridge_id = normalize_bridge_id(discovery_info.properties["bridgeid"])
231  await self.async_set_unique_idasync_set_unique_id(bridge_id)
232  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
233  updates={CONF_HOST: discovery_info.host}, reload_on_update=True
234  )
235 
236  # we need to query the other capabilities too
237  bridge = await self._get_bridge_get_bridge(
238  discovery_info.host, discovery_info.properties["bridgeid"]
239  )
240  if bridge is None:
241  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
242  self.bridgebridge = bridge
243  return await self.async_step_linkasync_step_link()
244 
246  self, discovery_info: zeroconf.ZeroconfServiceInfo
247  ) -> ConfigFlowResult:
248  """Handle a discovered Hue bridge on HomeKit.
249 
250  The bridge ID communicated over HomeKit differs, so we cannot use that
251  as the unique identifier. Therefore, this method uses discovery without
252  a unique ID.
253  """
254  bridge = await self._get_bridge_get_bridge(discovery_info.host)
255  if bridge is None:
256  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
257  self.bridgebridge = bridge
258  await self._async_handle_discovery_without_unique_id_async_handle_discovery_without_unique_id()
259  return await self.async_step_linkasync_step_link()
260 
261  async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
262  """Import a new bridge as a config entry.
263 
264  This flow is triggered by `async_setup` for both configured and
265  discovered bridges. Triggered for any bridge that does not have a
266  config entry yet (based on host).
267 
268  This flow is also triggered by `async_step_discovery`.
269  """
270  # Check if host exists, abort if so.
271  self._async_abort_entries_match_async_abort_entries_match({"host": import_data["host"]})
272 
273  bridge = await self._get_bridge_get_bridge(import_data["host"])
274  if bridge is None:
275  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
276  self.bridgebridge = bridge
277  return await self.async_step_linkasync_step_link()
278 
279 
281  """Handle Hue options for V1 implementation."""
282 
283  async def async_step_init(
284  self, user_input: dict[str, Any] | None = None
285  ) -> ConfigFlowResult:
286  """Manage Hue options."""
287  if user_input is not None:
288  return self.async_create_entryasync_create_entry(title="", data=user_input)
289 
290  return self.async_show_formasync_show_form(
291  step_id="init",
292  data_schema=vol.Schema(
293  {
294  vol.Optional(
295  CONF_ALLOW_HUE_GROUPS,
296  default=self.config_entryconfig_entryconfig_entry.options.get(
297  CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS
298  ),
299  ): bool,
300  vol.Optional(
301  CONF_ALLOW_UNREACHABLE,
302  default=self.config_entryconfig_entryconfig_entry.options.get(
303  CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE
304  ),
305  ): bool,
306  }
307  ),
308  )
309 
310 
312  """Handle Hue options for V2 implementation."""
313 
314  async def async_step_init(
315  self, user_input: dict[str, Any] | None = None
316  ) -> ConfigFlowResult:
317  """Manage Hue options."""
318  if user_input is not None:
319  return self.async_create_entryasync_create_entry(title="", data=user_input)
320 
321  # create a list of Hue device ID's that the user can select
322  # to ignore availability status
323  dev_reg = dr.async_get(self.hass)
324  entries = dr.async_entries_for_config_entry(dev_reg, self.config_entryconfig_entryconfig_entry.entry_id)
325  dev_ids = {
326  identifier[1]: entry.name
327  for entry in entries
328  for identifier in entry.identifiers
329  if identifier[0] == DOMAIN
330  }
331  # filter any non existing device id's from the list
332  cur_ids = [
333  item
334  for item in self.config_entryconfig_entryconfig_entry.options.get(CONF_IGNORE_AVAILABILITY, [])
335  if item in dev_ids
336  ]
337 
338  return self.async_show_formasync_show_form(
339  step_id="init",
340  data_schema=vol.Schema(
341  {
342  vol.Optional(
343  CONF_IGNORE_AVAILABILITY,
344  default=cur_ids,
345  ): cv.multi_select(dev_ids),
346  }
347  ),
348  )
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:98
ConfigFlowResult async_step_link(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:165
DiscoveredHueBridge _get_bridge(self, str host, str|None bridge_id=None)
Definition: config_flow.py:77
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:218
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
Definition: config_flow.py:261
ConfigFlowResult async_step_manual(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:149
ConfigFlowResult async_step_homekit(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:247
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:70
HueV1OptionsFlowHandler|HueV2OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:57
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:285
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:316
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
set[str|None] _async_current_ids(self, bool include_ignore=True)
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)
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)
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)