Simplify user flow

This commit is contained in:
Erik 2023-02-11 17:09:54 +01:00
parent 9a088fa8f1
commit 0ef9d1c6bb
5 changed files with 68 additions and 191 deletions

View file

@ -29,7 +29,9 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"no_adapters": "No unconfigured Bluetooth adapters found. There are {ignored_adapters} ignored adapters."
"no_adapters": "No unconfigured Bluetooth adapters found. There are {ignored_adapters} ignored adapters.",
"no_additional_devices_found": "No additional matching Bluetooth devices found",
"no_devices_found": "No matching Bluetooth devices found"
}
},
"options": {

View file

@ -5,13 +5,13 @@ import logging
from typing import Any
from bleak import BleakError
from py_dormakaba_dkey import DKEYLock, device_filter, errors as dkey_errors
from py_dormakaba_dkey import DKEYLock, errors as dkey_errors
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
async_rediscover_address,
)
from homeassistant.const import CONF_ADDRESS
from homeassistant.data_entry_flow import FlowResult
@ -43,48 +43,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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] = {}
"""Handle the user step.
if user_input is not None:
address = user_input[CONF_ADDRESS]
await self.async_set_unique_id(address, raise_on_progress=False)
# Guard against the user selecting a device which has been configured by
# another flow.
self._abort_if_unique_id_configured()
self._discovery_info = self._discovered_devices[address]
return await self.async_step_associate()
current_addresses = self._async_current_ids()
for discovery in async_discovered_service_info(self.hass):
if (
discovery.address in current_addresses
or discovery.address in self._discovered_devices
or not device_filter(discovery.advertisement)
):
continue
self._discovered_devices[discovery.address] = discovery
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
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()
}
),
}
)
return self.async_show_form(
step_id="user",
data_schema=data_schema,
errors=errors,
)
The user will be shown a list of matching discovery flows automatically,
so we just abort a user flow.
"""
if self._async_in_progress(include_uninitialized=True):
return self.async_abort(reason="no_additional_devices_found")
return self.async_abort(reason="no_devices_found")
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
@ -155,3 +121,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="associate", data_schema=STEP_ASSOCIATE_SCHEMA, errors=errors
)
async def async_step_unignore(self, user_input: dict[str, Any]) -> FlowResult:
"""Unignore an ignored bluetooth discovery flow."""
async_rediscover_address(self.hass, user_input["unique_id"])
return self.async_abort(reason="already_in_progress")

View file

@ -2,12 +2,6 @@
"config": {
"flow_title": "[%key:component::bluetooth::config::flow_title%]",
"step": {
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:component::bluetooth::config::step::user::data::address%]"
}
},
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
@ -25,7 +19,8 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"no_additional_devices_found": "[%key:component::bluetooth::config::abort::no_devices_found%]",
"no_devices_found": "[%key:component::bluetooth::config::abort::no_devices_found%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}

View file

@ -20,21 +20,3 @@ DKEY_DISCOVERY_INFO = BluetoothServiceInfoBleak(
time=0,
connectable=True,
)
NOT_DKEY_DISCOVERY_INFO = BluetoothServiceInfoBleak(
name="Not",
address="AA:BB:CC:DD:EE:F2",
rssi=-60,
manufacturer_data={
33: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9",
21: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0",
},
service_uuids=[],
service_data={},
source="local",
device=BLEDevice(address="AA:BB:CC:DD:EE:F2", name="Aug"),
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
)

View file

@ -10,134 +10,37 @@ from homeassistant import config_entries
from homeassistant.components.dormakaba_dkey.const import DOMAIN
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult, FlowResultType
from homeassistant.data_entry_flow import FlowResultType
from . import DKEY_DISCOVERY_INFO, NOT_DKEY_DISCOVERY_INFO
from . import DKEY_DISCOVERY_INFO
from tests.common import MockConfigEntry
async def test_user_step_success(hass: HomeAssistant) -> None:
"""Test user step success path."""
with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_discovered_service_info",
return_value=[NOT_DKEY_DISCOVERY_INFO, DKEY_DISCOVERY_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_ADDRESS: DKEY_DISCOVERY_INFO.address,
},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "associate"
assert result["errors"] is None
await _test_common_success(hass, result)
async def test_user_step_no_devices_found(hass: HomeAssistant) -> None:
"""Test user step with no devices found."""
with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_discovered_service_info",
return_value=[NOT_DKEY_DISCOVERY_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None:
"""Test user step with only existing devices found."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_ADDRESS: DKEY_DISCOVERY_INFO.address,
},
unique_id=DKEY_DISCOVERY_INFO.address,
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_discovered_service_info",
return_value=[DKEY_DISCOVERY_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_user_step_device_added_between_steps_1(hass: HomeAssistant) -> None:
"""Test the device gets added via another flow between steps."""
with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_discovered_service_info",
return_value=[DKEY_DISCOVERY_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=DKEY_DISCOVERY_INFO.address,
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"address": DKEY_DISCOVERY_INFO.address},
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_async_step_user_takes_precedence_over_discovery(hass):
"""Test manual setup takes precedence over discovery."""
result = await hass.config_entries.flow.async_init(
async def test_user_step(hass: HomeAssistant) -> None:
"""Test user step when there is a discovery flow."""
await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=DKEY_DISCOVERY_INFO,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "bluetooth_confirm"
with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_discovered_service_info",
return_value=[DKEY_DISCOVERY_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
)
assert result["type"] == FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_ADDRESS: DKEY_DISCOVERY_INFO.address,
},
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "associate"
assert result["errors"] is None
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "no_additional_devices_found"
return
await _test_common_success(hass, result)
# Verify the discovery flow was aborted
assert not hass.config_entries.flow.async_progress(DOMAIN)
async def test_user_step_no_discovery_flow(hass: HomeAssistant) -> None:
"""Test user step with no discovery flows."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
return
async def test_bluetooth_step_success(hass: HomeAssistant) -> None:
@ -156,12 +59,6 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None:
assert result["step_id"] == "associate"
assert result["errors"] is None
await _test_common_success(hass, result)
async def _test_common_success(hass: HomeAssistant, result: FlowResult) -> None:
"""Test bluetooth and user flow success paths."""
with patch(
"homeassistant.components.dormakaba_dkey.config_flow.DKEYLock.associate",
return_value=AssociationData(b"1234", b"AABBCCDD"),
@ -294,3 +191,33 @@ async def test_bluetooth_step_cannot_associate(hass: HomeAssistant, exc, error)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "associate"
assert result["errors"] == {"base": error}
async def test_unignore_flow(hass: HomeAssistant) -> None:
"""Test a config flow started by unignoring a device."""
# Create ignored entry
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IGNORE},
data={
"unique_id": DKEY_DISCOVERY_INFO.address,
"title": DKEY_DISCOVERY_INFO.name,
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["context"]["unique_id"] == DKEY_DISCOVERY_INFO.address
# Unignore and expect rediscover call to bluetooth
with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_rediscover_address",
) as rediscover_mock:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_UNIGNORE},
data={"unique_id": DKEY_DISCOVERY_INFO.address},
)
rediscover_mock.assert_called_once_with(hass, DKEY_DISCOVERY_INFO.address)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_in_progress"