mirror of
https://github.com/home-assistant/core.git
synced 2026-09-05 10:05:52 -05:00
Fix Yale Access Bluetooth key discovery timing issues (#151433)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
fbab53bd0c
commit
d00bf4b014
@@ -19,6 +19,7 @@ from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .config_cache import async_get_validated_config
|
||||
from .const import (
|
||||
CONF_ALWAYS_CONNECTED,
|
||||
CONF_KEY,
|
||||
@@ -96,13 +97,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: YALEXSBLEConfigEntry) ->
|
||||
)
|
||||
|
||||
try:
|
||||
await push_lock.wait_for_first_update(DEVICE_TIMEOUT)
|
||||
except AuthError as ex:
|
||||
raise ConfigEntryAuthFailed(str(ex)) from ex
|
||||
except (YaleXSBLEError, TimeoutError) as ex:
|
||||
raise ConfigEntryNotReady(
|
||||
f"{ex}; Try moving the Bluetooth adapter closer to {local_name}"
|
||||
) from ex
|
||||
await _async_wait_for_first_update(push_lock, local_name)
|
||||
except ConfigEntryAuthFailed:
|
||||
# If key has rotated, try to fetch it from the cache
|
||||
# and update
|
||||
if (validated_config := async_get_validated_config(hass, address)) and (
|
||||
validated_config.key != entry.data[CONF_KEY]
|
||||
or validated_config.slot != entry.data[CONF_SLOT]
|
||||
):
|
||||
assert shutdown_callback is not None
|
||||
shutdown_callback()
|
||||
push_lock.set_lock_key(validated_config.key, validated_config.slot)
|
||||
shutdown_callback = await push_lock.start()
|
||||
await _async_wait_for_first_update(push_lock, local_name)
|
||||
# If we can use the cached key and slot, update the entry.
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
CONF_KEY: validated_config.key,
|
||||
CONF_SLOT: validated_config.slot,
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
entry.runtime_data = YaleXSBLEData(entry.title, push_lock, always_connected)
|
||||
|
||||
@@ -147,6 +165,18 @@ async def _async_update_listener(
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def _async_wait_for_first_update(push_lock: PushLock, local_name: str) -> None:
|
||||
"""Wait for the first update from the push lock."""
|
||||
try:
|
||||
await push_lock.wait_for_first_update(DEVICE_TIMEOUT)
|
||||
except AuthError as ex:
|
||||
raise ConfigEntryAuthFailed(str(ex)) from ex
|
||||
except (YaleXSBLEError, TimeoutError) as ex:
|
||||
raise ConfigEntryNotReady(
|
||||
f"{ex}; Try moving the Bluetooth adapter closer to {local_name}"
|
||||
) from ex
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: YALEXSBLEConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""The Yale Access Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from yalexs_ble import ValidatedLockConfig
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
CONFIG_CACHE: HassKey[dict[str, ValidatedLockConfig]] = HassKey(
|
||||
"yalexs_ble_config_cache"
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_validated_config(
|
||||
hass: HomeAssistant,
|
||||
address: str,
|
||||
config: ValidatedLockConfig,
|
||||
) -> None:
|
||||
"""Add a validated config."""
|
||||
hass.data.setdefault(CONFIG_CACHE, {})[address] = config
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_validated_config(
|
||||
hass: HomeAssistant,
|
||||
address: str,
|
||||
) -> ValidatedLockConfig | None:
|
||||
"""Get the config for a specific address."""
|
||||
return hass.data.get(CONFIG_CACHE, {}).get(address)
|
||||
@@ -33,6 +33,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .config_cache import async_add_validated_config, async_get_validated_config
|
||||
from .const import CONF_ALWAYS_CONNECTED, CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN
|
||||
from .util import async_find_existing_service_info, human_readable_name
|
||||
|
||||
@@ -92,7 +93,10 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
None, discovery_info.name, discovery_info.address
|
||||
),
|
||||
}
|
||||
return await self.async_step_user()
|
||||
if lock_cfg := async_get_validated_config(self.hass, discovery_info.address):
|
||||
self._lock_cfg = lock_cfg
|
||||
return await self.async_step_integration_discovery_confirm()
|
||||
return await self.async_step_key_slot()
|
||||
|
||||
async def async_step_integration_discovery(
|
||||
self, discovery_info: DiscoveryInfoType
|
||||
@@ -105,6 +109,7 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
discovery_info["key"],
|
||||
discovery_info["slot"],
|
||||
)
|
||||
async_add_validated_config(self.hass, lock_cfg.address, lock_cfg)
|
||||
|
||||
address = lock_cfg.address
|
||||
self.local_name = lock_cfg.local_name
|
||||
@@ -232,6 +237,59 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_key_slot(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the key and slot step."""
|
||||
errors: dict[str, str] = {}
|
||||
discovery_info = self._discovery_info
|
||||
assert discovery_info is not None
|
||||
address = discovery_info.address
|
||||
validated_config = async_get_validated_config(self.hass, address)
|
||||
|
||||
if user_input is not None or validated_config:
|
||||
local_name = discovery_info.name
|
||||
if validated_config:
|
||||
key = validated_config.key
|
||||
slot = validated_config.slot
|
||||
title = validated_config.name
|
||||
else:
|
||||
assert user_input is not None
|
||||
key = user_input[CONF_KEY]
|
||||
slot = user_input[CONF_SLOT]
|
||||
title = human_readable_name(None, local_name, address)
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
if not (
|
||||
errors := await async_validate_lock_or_error(
|
||||
local_name, discovery_info.device, key, slot
|
||||
)
|
||||
):
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data={
|
||||
CONF_LOCAL_NAME: discovery_info.name,
|
||||
CONF_ADDRESS: discovery_info.address,
|
||||
CONF_KEY: key,
|
||||
CONF_SLOT: slot,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="key_slot",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_KEY): str,
|
||||
vol.Required(CONF_SLOT): int,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"address": address,
|
||||
"title": self._async_get_name_from_address(address),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -241,47 +299,24 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
self.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()
|
||||
if not (
|
||||
errors := await async_validate_lock_or_error(
|
||||
local_name, discovery_info.device, key, slot
|
||||
)
|
||||
):
|
||||
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,
|
||||
},
|
||||
)
|
||||
self._discovery_info = self._discovered_devices[address]
|
||||
return await self.async_step_key_slot()
|
||||
|
||||
if discovery := self._discovery_info:
|
||||
current_addresses = self._async_current_ids(include_ignore=False)
|
||||
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
|
||||
else:
|
||||
current_addresses = self._async_current_ids(include_ignore=False)
|
||||
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_devices_found")
|
||||
@@ -290,14 +325,12 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
{
|
||||
service_info.address: (
|
||||
f"{service_info.name} ({service_info.address})"
|
||||
service_info.address: self._async_get_name_from_address(
|
||||
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(
|
||||
@@ -306,6 +339,18 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_get_name_from_address(self, address: str) -> str:
|
||||
"""Get the name of a device from its address."""
|
||||
if validated_config := async_get_validated_config(self.hass, address):
|
||||
return f"{validated_config.name} ({address})"
|
||||
if address in self._discovered_devices:
|
||||
service_info = self._discovered_devices[address]
|
||||
return f"{service_info.name} ({service_info.address})"
|
||||
assert self._discovery_info is not None
|
||||
assert self._discovery_info.address == address
|
||||
return f"{self._discovery_info.name} ({address})"
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
||||
@@ -3,18 +3,23 @@
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Check the documentation for how to find the offline key. If you are using the August cloud integration to obtain the key, you may need to reload the August cloud integration while the lock is in Bluetooth range.",
|
||||
"description": "Select the device you want to set up over Bluetooth.",
|
||||
"data": {
|
||||
"address": "Bluetooth address"
|
||||
}
|
||||
},
|
||||
"key_slot": {
|
||||
"description": "Enter the key for the {title} lock with address {address}. If you are using the August or Yale cloud integration to obtain the key, you may be able to avoid this manual setup by reloading the August or Yale cloud integration while the lock is in Bluetooth range.",
|
||||
"data": {
|
||||
"address": "Bluetooth address",
|
||||
"key": "Offline Key (32-byte hex string)",
|
||||
"slot": "Offline Key Slot (Integer between 0 and 255)"
|
||||
}
|
||||
},
|
||||
"reauth_validate": {
|
||||
"description": "Enter the updated key for the {title} lock with address {address}. If you are using the August cloud integration to obtain the key, you may be able to avoid manual reauthentication by reloading the August cloud integration while the lock is in Bluetooth range.",
|
||||
"description": "Enter the updated key for the {title} lock with address {address}. If you are using the August or Yale cloud integration to obtain the key, you may be able to avoid manual re-authentication by reloading the August or Yale cloud integration while the lock is in Bluetooth range.",
|
||||
"data": {
|
||||
"key": "[%key:component::yalexs_ble::config::step::user::data::key%]",
|
||||
"slot": "[%key:component::yalexs_ble::config::step::user::data::slot%]"
|
||||
"key": "[%key:component::yalexs_ble::config::step::key_slot::data::key%]",
|
||||
"slot": "[%key:component::yalexs_ble::config::step::key_slot::data::slot%]"
|
||||
}
|
||||
},
|
||||
"integration_discovery_confirm": {
|
||||
|
||||
@@ -61,6 +61,16 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None:
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "key_slot"
|
||||
assert result2["errors"] == {}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.yalexs_ble.config_flow.PushLock.validate",
|
||||
@@ -70,25 +80,24 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None:
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: slot,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert result2["data"] == {
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert result3["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: slot,
|
||||
}
|
||||
assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -113,6 +122,16 @@ async def test_user_step_from_ignored(hass: HomeAssistant, slot: int) -> None:
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "key_slot"
|
||||
assert result2["errors"] == {}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.yalexs_ble.config_flow.PushLock.validate",
|
||||
@@ -122,25 +141,24 @@ async def test_user_step_from_ignored(hass: HomeAssistant, slot: int) -> None:
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: slot,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert result2["data"] == {
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert result3["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: slot,
|
||||
}
|
||||
assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -198,37 +216,44 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None:
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "dog",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {CONF_KEY: "invalid_key_format"}
|
||||
assert result2["step_id"] == "key_slot"
|
||||
assert result2["errors"] == {}
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "qfd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_KEY: "dog",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["step_id"] == "user"
|
||||
assert result3["step_id"] == "key_slot"
|
||||
assert result3["errors"] == {CONF_KEY: "invalid_key_format"}
|
||||
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "qfd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
assert result4["type"] is FlowResultType.FORM
|
||||
assert result4["step_id"] == "key_slot"
|
||||
assert result4["errors"] == {CONF_KEY: "invalid_key_format"}
|
||||
|
||||
result5 = await hass.config_entries.flow.async_configure(
|
||||
result4["flow_id"],
|
||||
{
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 999,
|
||||
},
|
||||
)
|
||||
assert result4["type"] is FlowResultType.FORM
|
||||
assert result4["step_id"] == "user"
|
||||
assert result4["errors"] == {CONF_SLOT: "invalid_key_index"}
|
||||
assert result5["type"] is FlowResultType.FORM
|
||||
assert result5["step_id"] == "key_slot"
|
||||
assert result5["errors"] == {CONF_SLOT: "invalid_key_index"}
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -239,25 +264,24 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None:
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result5 = await hass.config_entries.flow.async_configure(
|
||||
result4["flow_id"],
|
||||
result6 = await hass.config_entries.flow.async_configure(
|
||||
result5["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result5["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result5["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert result5["data"] == {
|
||||
assert result6["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result6["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert result6["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
}
|
||||
assert result5["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert result6["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -274,23 +298,32 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "key_slot"
|
||||
assert result2["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.yalexs_ble.config_flow.PushLock.validate",
|
||||
side_effect=BleakError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["step_id"] == "key_slot"
|
||||
assert result3["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -301,25 +334,24 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None:
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert result3["data"] == {
|
||||
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result4["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert result4["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
}
|
||||
assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert result4["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -336,23 +368,32 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "key_slot"
|
||||
assert result2["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.yalexs_ble.config_flow.PushLock.validate",
|
||||
side_effect=AuthError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {CONF_KEY: "invalid_auth"}
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["step_id"] == "key_slot"
|
||||
assert result3["errors"] == {CONF_KEY: "invalid_auth"}
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -363,25 +404,24 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None:
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert result3["data"] == {
|
||||
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result4["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert result4["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
}
|
||||
assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert result4["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -398,23 +438,32 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "key_slot"
|
||||
assert result2["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.yalexs_ble.config_flow.PushLock.validate",
|
||||
side_effect=RuntimeError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["step_id"] == "key_slot"
|
||||
assert result3["errors"] == {"base": "unknown"}
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -425,25 +474,24 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None:
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert result3["data"] == {
|
||||
assert result4["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result4["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert result4["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
}
|
||||
assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert result4["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -455,7 +503,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None:
|
||||
data=YALE_ACCESS_LOCK_DISCOVERY_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "key_slot"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with (
|
||||
@@ -470,7 +518,6 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
@@ -478,7 +525,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert result2["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert result2["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
@@ -563,7 +610,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth(
|
||||
data=YALE_ACCESS_LOCK_DISCOVERY_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "key_slot"
|
||||
assert result["errors"] == {}
|
||||
flows = list(hass.config_entries.flow._handler_progress_index[DOMAIN])
|
||||
assert len(flows) == 1
|
||||
@@ -629,6 +676,60 @@ async def test_integration_discovery_takes_precedence_over_bluetooth(
|
||||
assert len(flows) == 0
|
||||
|
||||
|
||||
async def test_bluetooth_discovery_with_cached_config(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test bluetooth discovery when validated config is already in cache."""
|
||||
# First, populate the cache via integration discovery
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
"name": "Front Door",
|
||||
"address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
"key": "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
"slot": 66,
|
||||
"serial": "M1XXX012LU",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
# Now do bluetooth discovery with the cached config
|
||||
with patch(
|
||||
"homeassistant.components.yalexs_ble.PushLock.validate",
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
data=YALE_ACCESS_LOCK_DISCOVERY_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "integration_discovery_confirm"
|
||||
assert result["description_placeholders"] == {
|
||||
"name": "Front Door",
|
||||
"address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
}
|
||||
|
||||
# Confirm the discovery
|
||||
with patch(
|
||||
"homeassistant.components.yalexs_ble.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Front Door"
|
||||
assert result["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
}
|
||||
|
||||
|
||||
async def test_integration_discovery_updates_key_unique_local_name(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -774,7 +875,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres
|
||||
data=LOCK_DISCOVERY_INFO_UUID_ADDRESS,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "key_slot"
|
||||
assert result["errors"] == {}
|
||||
flows = list(hass.config_entries.flow._handler_progress_index[DOMAIN])
|
||||
assert len(flows) == 1
|
||||
@@ -850,7 +951,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_
|
||||
data=OLD_FIRMWARE_LOCK_DISCOVERY_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "key_slot"
|
||||
assert result["errors"] == {}
|
||||
flows = list(hass.config_entries.flow._handler_progress_index[DOMAIN])
|
||||
assert len(flows) == 1
|
||||
@@ -907,6 +1008,15 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle(
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "key_slot"
|
||||
|
||||
user_flow_event = asyncio.Event()
|
||||
valdidate_started = asyncio.Event()
|
||||
|
||||
@@ -926,9 +1036,8 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle(
|
||||
):
|
||||
user_flow_task = asyncio.create_task(
|
||||
hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
},
|
||||
@@ -959,7 +1068,7 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle(
|
||||
user_flow_result = await user_flow_task
|
||||
|
||||
assert user_flow_result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert user_flow_result["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name
|
||||
assert user_flow_result["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)"
|
||||
assert user_flow_result["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
@@ -1033,6 +1142,75 @@ async def test_reauth(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_step_with_cached_config(hass: HomeAssistant) -> None:
|
||||
"""Test user step when config is already cached from integration discovery."""
|
||||
# First, simulate integration discovery to populate the cache
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
"name": "Front Door",
|
||||
"address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
"key": "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
"slot": 66,
|
||||
"serial": "M1XXX012LU",
|
||||
},
|
||||
)
|
||||
assert discovery_result["type"] is FlowResultType.ABORT
|
||||
assert discovery_result["reason"] == "no_devices_found"
|
||||
|
||||
# Now start a user flow - it should use the cached config
|
||||
with patch(
|
||||
"homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info",
|
||||
return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# The dropdown should show "Front Door (AA:BB:CC:DD:EE:FF)" from cached config
|
||||
# This is the line 346 case we're testing
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "key_slot"
|
||||
|
||||
# The key_slot step should auto-complete with cached values
|
||||
# When no user input is provided, it should use the cached config
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.yalexs_ble.config_flow.PushLock.validate",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.yalexs_ble.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
# No user input triggers using cached config
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
None, # None triggers checking for cached config
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "Front Door" # Uses the name from cached config
|
||||
assert result3["data"] == {
|
||||
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
|
||||
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
|
||||
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
|
||||
CONF_SLOT: 66,
|
||||
}
|
||||
assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_options(hass: HomeAssistant) -> None:
|
||||
"""Test options."""
|
||||
entry = MockConfigEntry(
|
||||
|
||||
Reference in New Issue
Block a user