From 0d37319ba9f27b6cd63ea59b43833b479c97566b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Sat, 30 May 2026 16:49:36 +0200 Subject: [PATCH] Improve Avea Bluetooth discovery flow (#172623) --- homeassistant/components/avea/config_flow.py | 19 +++-- tests/components/avea/test_config_flow.py | 77 ++++++++++++++++++-- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/avea/config_flow.py b/homeassistant/components/avea/config_flow.py index 5a098f2b93b6..13b13be3d62f 100644 --- a/homeassistant/components/avea/config_flow.py +++ b/homeassistant/components/avea/config_flow.py @@ -8,6 +8,7 @@ import avea from bleak.exc import BleakError import voluptuous as vol +from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, @@ -66,6 +67,15 @@ def _is_avea_discovery(discovery_info: BluetoothServiceInfoBleak) -> bool: return AVEA_SERVICE_UUID in discovery_info.service_uuids +def _discovery_label(discovery_info: BluetoothServiceInfoBleak) -> str: + """Return a label for a discovered Avea bulb.""" + if ( + name := _normalize_name(discovery_info.name) + ) and name != discovery_info.address: + return f"{name} ({discovery_info.address})" + return discovery_info.address + + class AveaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Avea.""" @@ -150,6 +160,7 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN): if discovery := self._discovery_info: self._discovered_devices[discovery.address] = discovery else: + await bluetooth.async_request_active_scan(self.hass) current_addresses = self._async_current_ids(include_ignore=False) for discovery in async_discovered_service_info(self.hass): if ( @@ -165,11 +176,10 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN): if self._discovery_info: disc = self._discovery_info - label = f"{disc.name or disc.address} ({disc.address})" data_schema = vol.Schema( { vol.Required(CONF_ADDRESS, default=disc.address): vol.In( - {disc.address: label} + {disc.address: _discovery_label(disc)} ) } ) @@ -178,10 +188,7 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN): { vol.Required(CONF_ADDRESS): vol.In( { - service_info.address: ( - f"{service_info.name or service_info.address}" - f" ({service_info.address})" - ) + service_info.address: _discovery_label(service_info) for service_info in self._discovered_devices.values() } ), diff --git a/tests/components/avea/test_config_flow.py b/tests/components/avea/test_config_flow.py index 3cdfeb26b325..ca1ed8885240 100644 --- a/tests/components/avea/test_config_flow.py +++ b/tests/components/avea/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.avea.const import DOMAIN +from homeassistant.components.avea.const import AVEA_SERVICE_UUID, DOMAIN from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant @@ -12,7 +12,11 @@ from homeassistant.data_entry_flow import FlowResultType from . import AVEA_DISCOVERY_INFO, NOT_AVEA_DISCOVERY_INFO -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.components.bluetooth import ( + generate_advertisement_data, + generate_ble_device, + inject_bluetooth_service_info, +) pytestmark = pytest.mark.usefixtures("enable_bluetooth") @@ -35,13 +39,22 @@ async def test_user_step_success(hass: HomeAssistant) -> None: inject_bluetooth_service_info(hass, AVEA_DISCOVERY_INFO) await hass.async_block_till_done(wait_background_tasks=True) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + with patch( + "homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan" + ) as mock_request_active_scan: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} + assert result["data_schema"].schema[CONF_ADDRESS].container == { + AVEA_DISCOVERY_INFO.address: ( + f"{AVEA_DISCOVERY_INFO.name} ({AVEA_DISCOVERY_INFO.address})" + ) + } + mock_request_active_scan.assert_awaited_once_with(hass) with ( patch( @@ -67,14 +80,62 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: inject_bluetooth_service_info(hass, NOT_AVEA_DISCOVERY_INFO) await hass.async_block_till_done(wait_background_tasks=True) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + with patch( + "homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan" + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" +async def test_user_step_unnamed_device_label(hass: HomeAssistant) -> None: + """Test unnamed discovered devices are shown without duplicating the address.""" + discovery_info = type(AVEA_DISCOVERY_INFO)( + name=AVEA_DISCOVERY_INFO.address, + address=AVEA_DISCOVERY_INFO.address, + rssi=-60, + manufacturer_data={}, + service_uuids=[AVEA_SERVICE_UUID], + service_data={}, + source="local", + device=generate_ble_device( + address=AVEA_DISCOVERY_INFO.address, name=AVEA_DISCOVERY_INFO.address + ), + advertisement=generate_advertisement_data( + local_name=AVEA_DISCOVERY_INFO.address, + manufacturer_data={}, + service_data={}, + service_uuids=[AVEA_SERVICE_UUID], + ), + time=0, + connectable=True, + tx_power=-127, + ) + + with ( + patch( + "homeassistant.components.avea.config_flow.async_discovered_service_info", + return_value=[discovery_info], + ), + patch( + "homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan" + ) as mock_request_active_scan, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["data_schema"].schema[CONF_ADDRESS].container == { + AVEA_DISCOVERY_INFO.address: AVEA_DISCOVERY_INFO.address + } + mock_request_active_scan.assert_awaited_once_with(hass) + + async def test_user_step_cannot_connect_recovers(hass: HomeAssistant) -> None: """Test the user step recovers after a cannot connect error.""" inject_bluetooth_service_info(hass, AVEA_DISCOVERY_INFO)