Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Tradfri."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import Any
7 from uuid import uuid4
8 
9 from pytradfri import Gateway, RequestError
10 from pytradfri.api.aiocoap_api import APIFactory
11 import voluptuous as vol
12 
13 from homeassistant.components import zeroconf
14 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
15 from homeassistant.const import CONF_HOST
16 from homeassistant.core import HomeAssistant
17 
18 from .const import CONF_GATEWAY_ID, CONF_IDENTITY, CONF_KEY, DOMAIN
19 
20 KEY_SECURITY_CODE = "security_code"
21 
22 
23 class AuthError(Exception):
24  """Exception if authentication occurs."""
25 
26  def __init__(self, code: str) -> None:
27  """Initialize exception."""
28  super().__init__()
29  self.codecode = code
30 
31 
32 class FlowHandler(ConfigFlow, domain=DOMAIN):
33  """Handle a config flow."""
34 
35  VERSION = 1
36 
37  def __init__(self) -> None:
38  """Initialize flow."""
39  self._host_host: str | None = None
40 
41  async def async_step_user(
42  self, user_input: dict[str, Any] | None = None
43  ) -> ConfigFlowResult:
44  """Handle a flow initialized by the user."""
45  return await self.async_step_authasync_step_auth()
46 
47  async def async_step_auth(
48  self, user_input: dict[str, Any] | None = None
49  ) -> ConfigFlowResult:
50  """Handle the authentication with a gateway."""
51  errors: dict[str, str] = {}
52 
53  if user_input is not None:
54  host = user_input.get(CONF_HOST, self._host_host)
55  try:
56  auth = await authenticate(
57  self.hass, host, user_input[KEY_SECURITY_CODE]
58  )
59 
60  return await self._entry_from_data_entry_from_data(auth)
61 
62  except AuthError as err:
63  errors["base"] = err.code
64  else:
65  user_input = {}
66 
67  fields = {}
68 
69  if self._host_host is None:
70  fields[vol.Required(CONF_HOST, default=user_input.get(CONF_HOST))] = str
71 
72  fields[
73  vol.Required(KEY_SECURITY_CODE, default=user_input.get(KEY_SECURITY_CODE))
74  ] = str
75 
76  return self.async_show_formasync_show_formasync_show_form(
77  step_id="auth", data_schema=vol.Schema(fields), errors=errors
78  )
79 
80  async def async_step_homekit(
81  self, discovery_info: zeroconf.ZeroconfServiceInfo
82  ) -> ConfigFlowResult:
83  """Handle homekit discovery."""
84  await self.async_set_unique_idasync_set_unique_id(
85  discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID]
86  )
87  self._abort_if_unique_id_configured_abort_if_unique_id_configured({CONF_HOST: discovery_info.host})
88 
89  host = discovery_info.host
90 
91  for entry in self._async_current_entries_async_current_entries():
92  if entry.data.get(CONF_HOST) != host:
93  continue
94 
95  # Backwards compat, we update old entries
96  if not entry.unique_id:
97  self.hass.config_entries.async_update_entry(
98  entry,
99  unique_id=discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID],
100  )
101 
102  return self.async_abortasync_abortasync_abort(reason="already_configured")
103 
104  self._host_host = host
105  return await self.async_step_authasync_step_auth()
106 
107  async def _entry_from_data(self, data: dict[str, Any]) -> ConfigFlowResult:
108  """Create an entry from data."""
109  host = data[CONF_HOST]
110  gateway_id = data[CONF_GATEWAY_ID]
111 
112  same_hub_entries = [
113  entry.entry_id
114  for entry in self._async_current_entries_async_current_entries()
115  if entry.data.get(CONF_GATEWAY_ID) == gateway_id
116  or entry.data.get(CONF_HOST) == host
117  ]
118 
119  if same_hub_entries:
120  await asyncio.wait(
121  [
122  asyncio.create_task(self.hass.config_entries.async_remove(entry_id))
123  for entry_id in same_hub_entries
124  ]
125  )
126 
127  return self.async_create_entryasync_create_entryasync_create_entry(title=host, data=data)
128 
129 
130 async def authenticate(
131  hass: HomeAssistant, host: str, security_code: str
132 ) -> dict[str, str | bool]:
133  """Authenticate with a Tradfri hub."""
134 
135  identity = uuid4().hex
136 
137  api_factory = await APIFactory.init(host, psk_id=identity)
138 
139  try:
140  async with asyncio.timeout(5):
141  key = await api_factory.generate_psk(security_code)
142  except RequestError as err:
143  raise AuthError("invalid_security_code") from err
144  except TimeoutError as err:
145  raise AuthError("timeout") from err
146  finally:
147  await api_factory.shutdown()
148  if key is None:
149  raise AuthError("cannot_authenticate")
150  return await get_gateway_info(hass, host, identity, key)
151 
152 
154  hass: HomeAssistant, host: str, identity: str, key: str
155 ) -> dict[str, str | bool]:
156  """Return info for the gateway."""
157 
158  try:
159  factory = await APIFactory.init(host, psk_id=identity, psk=key)
160 
161  api = factory.request
162  gateway = Gateway()
163  gateway_info_result = await api(gateway.get_gateway_info())
164 
165  await factory.shutdown()
166  except (OSError, RequestError) as err:
167  # We're also catching OSError as PyTradfri doesn't catch that one yet
168  # Upstream PR: https://github.com/ggravlingen/pytradfri/pull/189
169  raise AuthError("cannot_connect") from err
170 
171  return {
172  CONF_HOST: host,
173  CONF_IDENTITY: identity,
174  CONF_KEY: key,
175  CONF_GATEWAY_ID: gateway_info_result.id,
176  }
ConfigFlowResult async_step_auth(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:49
ConfigFlowResult _entry_from_data(self, dict[str, Any] data)
Definition: config_flow.py:107
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:43
ConfigFlowResult async_step_homekit(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:82
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=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)
_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)
dict[str, str|bool] authenticate(HomeAssistant hass, str host, str security_code)
Definition: config_flow.py:132
dict[str, str|bool] get_gateway_info(HomeAssistant hass, str host, str identity, str key)
Definition: config_flow.py:155