1 """Config flow for Yale Access Bluetooth integration."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
7 from typing
import Any, Self
9 from bleak_retry_connector
import BleakError, BLEDevice
10 import voluptuous
as vol
11 from yalexs_ble
import (
18 from yalexs_ble.const
import YALE_MFR_ID
21 BluetoothServiceInfoBleak,
22 async_ble_device_from_address,
23 async_discovered_service_info,
36 from .const
import CONF_ALWAYS_CONNECTED, CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN
37 from .util
import async_find_existing_service_info, human_readable_name
39 _LOGGER = logging.getLogger(__name__)
43 local_name: str, device: BLEDevice, key: str, slot: int
45 """Validate the lock and return errors if any."""
47 return {CONF_KEY:
"invalid_key_format"}
51 return {CONF_KEY:
"invalid_key_format"}
52 if not isinstance(slot, int)
or not 0 <= slot <= 255:
53 return {CONF_SLOT:
"invalid_key_index"}
55 await PushLock(local_name, device.address, device, key, slot).
validate()
56 except (DisconnectedError, AuthError, ValueError):
57 return {CONF_KEY:
"invalid_auth"}
59 return {
"base":
"cannot_connect"}
61 _LOGGER.exception(
"Unexpected error")
62 return {
"base":
"unknown"}
67 """Handle a config flow for Yale Access Bluetooth."""
71 _address: str |
None =
None
72 _local_name_is_unique =
False
74 local_name: str |
None =
None
77 """Initialize the config flow."""
78 self.
_discovery_info_discovery_info: BluetoothServiceInfoBleak |
None =
None
79 self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
80 self.
_lock_cfg_lock_cfg: ValidatedLockConfig |
None =
None
83 self, discovery_info: BluetoothServiceInfoBleak
84 ) -> ConfigFlowResult:
85 """Handle the bluetooth discovery step."""
90 self.context[
"title_placeholders"] = {
92 None, discovery_info.name, discovery_info.address
98 self, discovery_info: DiscoveryInfoType
99 ) -> ConfigFlowResult:
100 """Handle a discovered integration."""
101 lock_cfg = ValidatedLockConfig(
102 discovery_info[
"name"],
103 discovery_info[
"address"],
104 discovery_info[
"serial"],
105 discovery_info[
"key"],
106 discovery_info[
"slot"],
109 address = lock_cfg.address
110 self.
local_namelocal_name = lock_cfg.local_name
119 new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot}
124 and entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name
127 entry, data={**entry.data, **new_data}, reason=
"already_configured"
137 if self.hass.config_entries.flow.async_has_matching_flow(self):
141 self.context[
"title_placeholders"] = {
143 lock_cfg.name, lock_cfg.local_name, self.
_discovery_info_discovery_info.address
149 """Return True if other_flow is matching this flow."""
155 )
or other_flow.unique_id == self.
_address_address:
156 if other_flow.active:
162 self.hass.config_entries.flow.async_abort(other_flow.flow_id)
167 self, user_input: dict[str, Any] |
None =
None
168 ) -> ConfigFlowResult:
169 """Handle a confirmation of discovered integration."""
171 assert self.
_lock_cfg_lock_cfg
is not None
172 if user_input
is not None:
185 step_id=
"integration_discovery_confirm",
186 description_placeholders={
193 self, entry_data: Mapping[str, Any]
194 ) -> ConfigFlowResult:
195 """Handle configuration by re-auth."""
199 self, user_input: dict[str, Any] |
None =
None
200 ) -> ConfigFlowResult:
201 """Handle reauth and validation."""
204 if user_input
is not None:
207 self.hass, reauth_entry.data[CONF_ADDRESS],
True
210 errors = {
"base":
"no_longer_in_range"}
213 reauth_entry.data[CONF_LOCAL_NAME],
215 user_input[CONF_KEY],
216 user_input[CONF_SLOT],
220 reauth_entry, data_updates=user_input
224 step_id=
"reauth_validate",
225 data_schema=vol.Schema(
226 {vol.Required(CONF_KEY): str, vol.Required(CONF_SLOT): int}
228 description_placeholders={
229 "address": reauth_entry.data[CONF_ADDRESS],
230 "title": reauth_entry.title,
236 self, user_input: dict[str, Any] |
None =
None
237 ) -> ConfigFlowResult:
238 """Handle the user step to pick discovered device."""
239 errors: dict[str, str] = {}
241 if user_input
is not None:
243 address = user_input[CONF_ADDRESS]
244 discovery_info = self._discovered_devices[address]
245 local_name = discovery_info.name
246 key = user_input[CONF_KEY]
247 slot = user_input[CONF_SLOT]
249 discovery_info.address, raise_on_progress=
False
254 local_name, discovery_info.device, key, slot
260 CONF_LOCAL_NAME: discovery_info.name,
261 CONF_ADDRESS: discovery_info.address,
268 self._discovered_devices[discovery.address] = discovery
271 current_unique_names = {
272 entry.data.get(CONF_LOCAL_NAME)
274 if local_name_is_unique(entry.data.get(CONF_LOCAL_NAME))
278 discovery.address
in current_addresses
279 or discovery.name
in current_unique_names
280 or discovery.address
in self._discovered_devices
281 or YALE_MFR_ID
not in discovery.manufacturer_data
284 self._discovered_devices[discovery.address] = discovery
286 if not self._discovered_devices:
289 data_schema = vol.Schema(
291 vol.Required(CONF_ADDRESS): vol.In(
293 service_info.address: (
294 f
"{service_info.name} ({service_info.address})"
296 for service_info
in self._discovered_devices.values()
299 vol.Required(CONF_KEY): str,
300 vol.Required(CONF_SLOT): int,
305 data_schema=data_schema,
312 config_entry: ConfigEntry,
313 ) -> YaleXSBLEOptionsFlowHandler:
314 """Get the options flow for this handler."""
319 """Handle YaleXSBLE options."""
322 self, user_input: dict[str, Any] |
None =
None
323 ) -> ConfigFlowResult:
324 """Manage the YaleXSBLE options."""
328 self, user_input: dict[str, Any] |
None =
None
329 ) -> ConfigFlowResult:
330 """Manage the YaleXSBLE devices options."""
331 if user_input
is not None:
333 data={CONF_ALWAYS_CONNECTED: user_input[CONF_ALWAYS_CONNECTED]},
337 step_id=
"device_options",
338 data_schema=vol.Schema(
341 CONF_ALWAYS_CONNECTED,
343 CONF_ALWAYS_CONNECTED,
False
ConfigFlowResult async_step_device_options(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
bool is_matching(self, Self other_flow)
bool _local_name_is_unique
ConfigFlowResult async_step_integration_discovery(self, DiscoveryInfoType discovery_info)
YaleXSBLEOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_integration_discovery_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_reauth_validate(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_bluetooth(self, BluetoothServiceInfoBleak discovery_info)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry _get_reauth_entry(self)
None _set_confirm_only(self)
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=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_step_user(self, dict[str, Any]|None user_input=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)
ConfigEntry config_entry(self)
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)
BLEDevice|None async_ble_device_from_address(HomeAssistant hass, str address, bool connectable=True)
Iterable[BluetoothServiceInfoBleak] async_discovered_service_info(HomeAssistant hass, bool connectable=True)
str human_readable_name(str hostname, str vendor, str mac_address)
dict[str, Any] validate(SchemaCommonFlowHandler handler, dict[str, Any] user_input)
dict[str, str] async_validate_lock_or_error(str local_name, BLEDevice device, str key, int slot)
BluetoothServiceInfoBleak|None async_find_existing_service_info(HomeAssistant hass, str local_name, str address)