Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Dormakaba dKey integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 
9 from bleak import BleakError
10 from py_dormakaba_dkey import DKEYLock, device_filter, errors as dkey_errors
11 import voluptuous as vol
12 
14  BluetoothServiceInfoBleak,
15  async_discovered_service_info,
16  async_last_service_info,
17 )
18 from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
19 from homeassistant.const import CONF_ADDRESS
20 
21 from .const import CONF_ASSOCIATION_DATA, DOMAIN
22 
23 _LOGGER = logging.getLogger(__name__)
24 
25 STEP_ASSOCIATE_SCHEMA = vol.Schema(
26  {
27  vol.Required("activation_code"): str,
28  }
29 )
30 
31 
32 class DormkabaConfigFlow(ConfigFlow, domain=DOMAIN):
33  """Handle a config flow for Dormakaba dKey."""
34 
35  VERSION = 1
36 
37  def __init__(self) -> None:
38  """Initialize the config flow."""
39  self._lock_lock: DKEYLock | None = None
40  # Populated by user step
41  self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
42  # Populated by bluetooth, reauth_confirm and user steps
43  self._discovery_info_discovery_info: BluetoothServiceInfoBleak | None = None
44 
45  async def async_step_user(
46  self, user_input: dict[str, Any] | None = None
47  ) -> ConfigFlowResult:
48  """Handle the user step to pick discovered device."""
49  errors: dict[str, str] = {}
50 
51  if user_input is not None:
52  address = user_input[CONF_ADDRESS]
53  await self.async_set_unique_idasync_set_unique_id(address, raise_on_progress=False)
54  # Guard against the user selecting a device which has been configured by
55  # another flow.
56  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
57  self._discovery_info_discovery_info = self._discovered_devices[address]
58  return await self.async_step_associateasync_step_associate()
59 
60  current_addresses = self._async_current_ids_async_current_ids()
61  for discovery in async_discovered_service_info(self.hass):
62  if (
63  discovery.address in current_addresses
64  or discovery.address in self._discovered_devices
65  or not device_filter(discovery.advertisement)
66  ):
67  continue
68  self._discovered_devices[discovery.address] = discovery
69 
70  if not self._discovered_devices:
71  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
72 
73  data_schema = vol.Schema(
74  {
75  vol.Required(CONF_ADDRESS): vol.In(
76  {
77  service_info.address: (
78  f"{service_info.name} ({service_info.address})"
79  )
80  for service_info in self._discovered_devices.values()
81  }
82  ),
83  }
84  )
85  return self.async_show_formasync_show_formasync_show_form(
86  step_id="user",
87  data_schema=data_schema,
88  errors=errors,
89  )
90 
92  self, discovery_info: BluetoothServiceInfoBleak
93  ) -> ConfigFlowResult:
94  """Handle the Bluetooth discovery step."""
95  await self.async_set_unique_idasync_set_unique_id(discovery_info.address)
96  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
97  self._discovery_info_discovery_info = discovery_info
98  name = self._discovery_info_discovery_info.name or self._discovery_info_discovery_info.address
99  self.context["title_placeholders"] = {"name": name}
100  return await self.async_step_bluetooth_confirmasync_step_bluetooth_confirm()
101 
103  self, user_input: dict[str, Any] | None = None
104  ) -> ConfigFlowResult:
105  """Handle bluetooth confirm step."""
106  # mypy is not aware that we can't get here without having these set already
107  assert self._discovery_info_discovery_info is not None
108 
109  if user_input is None:
110  name = self._discovery_info_discovery_info.name or self._discovery_info_discovery_info.address
111  return self.async_show_formasync_show_formasync_show_form(
112  step_id="bluetooth_confirm",
113  description_placeholders={"name": name},
114  )
115 
116  return await self.async_step_associateasync_step_associate()
117 
118  async def async_step_reauth(
119  self, entry_data: Mapping[str, Any]
120  ) -> ConfigFlowResult:
121  """Handle reauthorization request."""
122  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
123 
125  self, user_input: dict[str, Any] | None = None
126  ) -> ConfigFlowResult:
127  """Handle reauthorization flow."""
128  errors = {}
129 
130  if user_input is not None:
131  if (
132  discovery_info := async_last_service_info(
133  self.hass, self._get_reauth_entry_get_reauth_entry().data[CONF_ADDRESS], True
134  )
135  ) is None:
136  errors = {"base": "no_longer_in_range"}
137  else:
138  self._discovery_info_discovery_info = discovery_info
139  return await self.async_step_associateasync_step_associate()
140 
141  return self.async_show_formasync_show_formasync_show_form(
142  step_id="reauth_confirm", data_schema=vol.Schema({}), errors=errors
143  )
144 
146  self, user_input: dict[str, Any] | None = None
147  ) -> ConfigFlowResult:
148  """Handle associate step."""
149  # mypy is not aware that we can't get here without having these set already
150  assert self._discovery_info_discovery_info is not None
151 
152  if user_input is None:
153  return self.async_show_formasync_show_formasync_show_form(
154  step_id="associate", data_schema=STEP_ASSOCIATE_SCHEMA
155  )
156 
157  errors = {}
158  if not self._lock_lock:
159  self._lock_lock = DKEYLock(self._discovery_info_discovery_info.device)
160  lock = self._lock_lock
161 
162  try:
163  association_data = await lock.associate(user_input["activation_code"])
164  except BleakError as err:
165  _LOGGER.warning("BleakError", exc_info=err)
166  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
167  except dkey_errors.InvalidActivationCode:
168  errors["base"] = "invalid_code"
169  except dkey_errors.WrongActivationCode:
170  errors["base"] = "wrong_code"
171  except Exception:
172  _LOGGER.exception("Unexpected exception")
173  return self.async_abortasync_abortasync_abort(reason="unknown")
174  else:
175  data = {
176  CONF_ADDRESS: self._discovery_info_discovery_info.device.address,
177  CONF_ASSOCIATION_DATA: association_data.to_json(),
178  }
179  if self.sourcesourcesourcesource == SOURCE_REAUTH:
180  return self.async_update_reload_and_abortasync_update_reload_and_abort(
181  self._get_reauth_entry_get_reauth_entry(), data=data
182  )
183 
184  return self.async_create_entryasync_create_entryasync_create_entry(
185  title=lock.device_info.device_name
186  or lock.device_info.device_id
187  or lock.name,
188  data=data,
189  )
190 
191  return self.async_show_formasync_show_formasync_show_form(
192  step_id="associate", data_schema=STEP_ASSOCIATE_SCHEMA, errors=errors
193  )
ConfigFlowResult async_step_bluetooth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:104
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:47
ConfigFlowResult async_step_bluetooth(self, BluetoothServiceInfoBleak discovery_info)
Definition: config_flow.py:93
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:126
ConfigFlowResult async_step_associate(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:147
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:120
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_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
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)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
Iterable[BluetoothServiceInfoBleak] async_discovered_service_info(HomeAssistant hass, bool connectable=True)
Definition: api.py:72
BluetoothServiceInfoBleak|None async_last_service_info(HomeAssistant hass, str address, bool connectable=True)
Definition: api.py:80