Entries took a while to setup because of the async_wait_init_flow_finish call in _async_setup_component The delay was so long that users thought the integration was broken We had a wait in place for advertisements to arrive during discovery in case the lock was not yet seen. Since integration discovery is deferred until after startup this wait it no longer needed
256 lines
9.7 KiB
Python
256 lines
9.7 KiB
Python
"""Config flow for Yale Access Bluetooth integration."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from bleak_retry_connector import BleakError, BLEDevice
|
|
import voluptuous as vol
|
|
from yalexs_ble import (
|
|
AuthError,
|
|
DisconnectedError,
|
|
PushLock,
|
|
ValidatedLockConfig,
|
|
local_name_is_unique,
|
|
)
|
|
from yalexs_ble.const import YALE_MFR_ID
|
|
|
|
from homeassistant import config_entries, data_entry_flow
|
|
from homeassistant.components.bluetooth import (
|
|
BluetoothServiceInfoBleak,
|
|
async_discovered_service_info,
|
|
)
|
|
from homeassistant.const import CONF_ADDRESS
|
|
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.typing import DiscoveryInfoType
|
|
|
|
from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN
|
|
from .util import async_find_existing_service_info, human_readable_name
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def validate_lock(
|
|
local_name: str, device: BLEDevice, key: str, slot: int
|
|
) -> None:
|
|
"""Validate a lock."""
|
|
if len(key) != 32:
|
|
raise InvalidKeyFormat
|
|
try:
|
|
bytes.fromhex(key)
|
|
except ValueError as ex:
|
|
raise InvalidKeyFormat from ex
|
|
if not isinstance(slot, int) or slot < 0 or slot > 255:
|
|
raise InvalidKeyIndex
|
|
await PushLock(local_name, device.address, device, key, slot).validate()
|
|
|
|
|
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|
"""Handle a config flow for Yale Access Bluetooth."""
|
|
|
|
VERSION = 1
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the config flow."""
|
|
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
|
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
|
|
self._lock_cfg: ValidatedLockConfig | None = None
|
|
|
|
async def async_step_bluetooth(
|
|
self, discovery_info: BluetoothServiceInfoBleak
|
|
) -> FlowResult:
|
|
"""Handle the bluetooth discovery step."""
|
|
await self.async_set_unique_id(discovery_info.address)
|
|
self._abort_if_unique_id_configured()
|
|
self.context["local_name"] = discovery_info.name
|
|
self._discovery_info = discovery_info
|
|
self.context["title_placeholders"] = {
|
|
"name": human_readable_name(
|
|
None, discovery_info.name, discovery_info.address
|
|
),
|
|
}
|
|
return await self.async_step_user()
|
|
|
|
async def async_step_integration_discovery(
|
|
self, discovery_info: DiscoveryInfoType
|
|
) -> FlowResult:
|
|
"""Handle a discovered integration."""
|
|
lock_cfg = ValidatedLockConfig(
|
|
discovery_info["name"],
|
|
discovery_info["address"],
|
|
discovery_info["serial"],
|
|
discovery_info["key"],
|
|
discovery_info["slot"],
|
|
)
|
|
|
|
address = lock_cfg.address
|
|
local_name = lock_cfg.local_name
|
|
hass = self.hass
|
|
|
|
# We do not want to raise on progress as integration_discovery takes
|
|
# precedence over other discovery flows since we already have the keys.
|
|
#
|
|
# After we do discovery we will abort the flows that do not have the keys
|
|
# below unless the user is already setting them up.
|
|
await self.async_set_unique_id(address, raise_on_progress=False)
|
|
new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot}
|
|
self._abort_if_unique_id_configured(updates=new_data)
|
|
for entry in self._async_current_entries():
|
|
if (
|
|
local_name_is_unique(lock_cfg.local_name)
|
|
and entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name
|
|
):
|
|
if hass.config_entries.async_update_entry(
|
|
entry, data={**entry.data, **new_data}
|
|
):
|
|
hass.async_create_task(
|
|
hass.config_entries.async_reload(entry.entry_id)
|
|
)
|
|
raise AbortFlow(reason="already_configured")
|
|
|
|
self._discovery_info = async_find_existing_service_info(
|
|
hass, local_name, address
|
|
)
|
|
if not self._discovery_info:
|
|
return self.async_abort(reason="no_devices_found")
|
|
|
|
# Integration discovery should abort other flows unless they
|
|
# are already in the process of being set up since this discovery
|
|
# will already have all the keys and the user can simply confirm.
|
|
for progress in self._async_in_progress(include_uninitialized=True):
|
|
context = progress["context"]
|
|
if (
|
|
local_name_is_unique(local_name)
|
|
and context.get("local_name") == local_name
|
|
) or context.get("unique_id") == address:
|
|
if context.get("active"):
|
|
# The user has already started interacting with this flow
|
|
# and entered the keys. We abort the discovery flow since
|
|
# we assume they do not want to use the discovered keys for
|
|
# some reason.
|
|
raise data_entry_flow.AbortFlow("already_in_progress")
|
|
hass.config_entries.flow.async_abort(progress["flow_id"])
|
|
|
|
self._lock_cfg = lock_cfg
|
|
self.context["title_placeholders"] = {
|
|
"name": human_readable_name(
|
|
lock_cfg.name, lock_cfg.local_name, self._discovery_info.address
|
|
)
|
|
}
|
|
return await self.async_step_integration_discovery_confirm()
|
|
|
|
async def async_step_integration_discovery_confirm(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> FlowResult:
|
|
"""Handle a confirmation of discovered integration."""
|
|
assert self._discovery_info is not None
|
|
assert self._lock_cfg is not None
|
|
if user_input is not None:
|
|
return self.async_create_entry(
|
|
title=self._lock_cfg.name,
|
|
data={
|
|
CONF_LOCAL_NAME: self._discovery_info.name,
|
|
CONF_ADDRESS: self._discovery_info.address,
|
|
CONF_KEY: self._lock_cfg.key,
|
|
CONF_SLOT: self._lock_cfg.slot,
|
|
},
|
|
)
|
|
|
|
self._set_confirm_only()
|
|
return self.async_show_form(
|
|
step_id="integration_discovery_confirm",
|
|
description_placeholders={
|
|
"name": self._lock_cfg.name,
|
|
"address": self._discovery_info.address,
|
|
},
|
|
)
|
|
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> FlowResult:
|
|
"""Handle the user step to pick discovered device."""
|
|
errors: dict[str, str] = {}
|
|
|
|
if user_input is not None:
|
|
self.context["active"] = True
|
|
address = user_input[CONF_ADDRESS]
|
|
discovery_info = self._discovered_devices[address]
|
|
local_name = discovery_info.name
|
|
key = user_input[CONF_KEY]
|
|
slot = user_input[CONF_SLOT]
|
|
await self.async_set_unique_id(
|
|
discovery_info.address, raise_on_progress=False
|
|
)
|
|
self._abort_if_unique_id_configured()
|
|
try:
|
|
await validate_lock(local_name, discovery_info.device, key, slot)
|
|
except InvalidKeyFormat:
|
|
errors[CONF_KEY] = "invalid_key_format"
|
|
except InvalidKeyIndex:
|
|
errors[CONF_SLOT] = "invalid_key_index"
|
|
except (DisconnectedError, AuthError, ValueError):
|
|
errors[CONF_KEY] = "invalid_auth"
|
|
except BleakError:
|
|
errors["base"] = "cannot_connect"
|
|
except Exception: # pylint: disable=broad-except
|
|
_LOGGER.exception("Unexpected error")
|
|
errors["base"] = "unknown"
|
|
else:
|
|
return self.async_create_entry(
|
|
title=local_name,
|
|
data={
|
|
CONF_LOCAL_NAME: discovery_info.name,
|
|
CONF_ADDRESS: discovery_info.address,
|
|
CONF_KEY: key,
|
|
CONF_SLOT: slot,
|
|
},
|
|
)
|
|
|
|
if discovery := self._discovery_info:
|
|
self._discovered_devices[discovery.address] = discovery
|
|
else:
|
|
current_addresses = self._async_current_ids()
|
|
current_unique_names = {
|
|
entry.data.get(CONF_LOCAL_NAME)
|
|
for entry in self._async_current_entries()
|
|
if local_name_is_unique(entry.data.get(CONF_LOCAL_NAME))
|
|
}
|
|
for discovery in async_discovered_service_info(self.hass):
|
|
if (
|
|
discovery.address in current_addresses
|
|
or discovery.name in current_unique_names
|
|
or discovery.address in self._discovered_devices
|
|
or YALE_MFR_ID not in discovery.manufacturer_data
|
|
):
|
|
continue
|
|
self._discovered_devices[discovery.address] = discovery
|
|
|
|
if not self._discovered_devices:
|
|
return self.async_abort(reason="no_unconfigured_devices")
|
|
|
|
data_schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_ADDRESS): vol.In(
|
|
{
|
|
service_info.address: f"{service_info.name} ({service_info.address})"
|
|
for service_info in self._discovered_devices.values()
|
|
}
|
|
),
|
|
vol.Required(CONF_KEY): str,
|
|
vol.Required(CONF_SLOT): int,
|
|
}
|
|
)
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=data_schema,
|
|
errors=errors,
|
|
)
|
|
|
|
|
|
class InvalidKeyFormat(HomeAssistantError):
|
|
"""Invalid key format."""
|
|
|
|
|
|
class InvalidKeyIndex(HomeAssistantError):
|
|
"""Invalid key index."""
|