hass-core/homeassistant/components/yalexs_ble/config_flow.py
J. Nick Koston fec887420d
Fix delay setting up new Yale Access Bluetooth entries (#83683)
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
2022-12-09 20:55:06 -05:00

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."""