From 69d9fa89b78c5ed1c05da5c3096af6b30b9ab8fb Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 6 Oct 2025 10:57:21 +0200 Subject: [PATCH] Remove stale entities from Alexa Devices (#153759) --- .../components/alexa_devices/binary_sensor.py | 48 +++++++++++++++++ .../components/alexa_devices/switch.py | 9 +++- .../components/alexa_devices/utils.py | 20 +++++++ .../alexa_devices/test_binary_sensor.py | 52 ++++++++++++++++++- tests/components/alexa_devices/test_utils.py | 40 ++++++++++++++ 5 files changed, 167 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa_devices/binary_sensor.py b/homeassistant/components/alexa_devices/binary_sensor.py index 8347fa34423..dd8be49f521 100644 --- a/homeassistant/components/alexa_devices/binary_sensor.py +++ b/homeassistant/components/alexa_devices/binary_sensor.py @@ -18,7 +18,9 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +import homeassistant.helpers.entity_registry as er +from .const import _LOGGER, DOMAIN from .coordinator import AmazonConfigEntry from .entity import AmazonEntity from .utils import async_update_unique_id @@ -58,6 +60,40 @@ BINARY_SENSORS: Final = ( ), ) +DEPRECATED_BINARY_SENSORS: Final = ( + AmazonBinarySensorEntityDescription( + key="bluetooth", + entity_category=EntityCategory.DIAGNOSTIC, + translation_key="bluetooth", + is_on_fn=lambda device, key: False, + ), + AmazonBinarySensorEntityDescription( + key="babyCryDetectionState", + translation_key="baby_cry_detection", + is_on_fn=lambda device, key: False, + ), + AmazonBinarySensorEntityDescription( + key="beepingApplianceDetectionState", + translation_key="beeping_appliance_detection", + is_on_fn=lambda device, key: False, + ), + AmazonBinarySensorEntityDescription( + key="coughDetectionState", + translation_key="cough_detection", + is_on_fn=lambda device, key: False, + ), + AmazonBinarySensorEntityDescription( + key="dogBarkDetectionState", + translation_key="dog_bark_detection", + is_on_fn=lambda device, key: False, + ), + AmazonBinarySensorEntityDescription( + key="waterSoundsDetectionState", + translation_key="water_sounds_detection", + is_on_fn=lambda device, key: False, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -68,6 +104,8 @@ async def async_setup_entry( coordinator = entry.runtime_data + entity_registry = er.async_get(hass) + # Replace unique id for "detectionState" binary sensor await async_update_unique_id( hass, @@ -77,6 +115,16 @@ async def async_setup_entry( "detectionState", ) + # Clean up deprecated sensors + for sensor_desc in DEPRECATED_BINARY_SENSORS: + for serial_num in coordinator.data: + unique_id = f"{serial_num}-{sensor_desc.key}" + if entity_id := entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, unique_id + ): + _LOGGER.debug("Removing deprecated entity %s", entity_id) + entity_registry.async_remove(entity_id) + known_devices: set[str] = set() def _check_device() -> None: diff --git a/homeassistant/components/alexa_devices/switch.py b/homeassistant/components/alexa_devices/switch.py index 003f5762079..5acaa8c2dd0 100644 --- a/homeassistant/components/alexa_devices/switch.py +++ b/homeassistant/components/alexa_devices/switch.py @@ -18,7 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import AmazonConfigEntry from .entity import AmazonEntity -from .utils import alexa_api_call, async_update_unique_id +from .utils import ( + alexa_api_call, + async_remove_dnd_from_virtual_group, + async_update_unique_id, +) PARALLEL_UPDATES = 1 @@ -60,6 +64,9 @@ async def async_setup_entry( hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd" ) + # Remove DND switch from virtual groups + await async_remove_dnd_from_virtual_group(hass, coordinator) + known_devices: set[str] = set() def _check_device() -> None: diff --git a/homeassistant/components/alexa_devices/utils.py b/homeassistant/components/alexa_devices/utils.py index f8898aa5fe4..3fbba539a6a 100644 --- a/homeassistant/components/alexa_devices/utils.py +++ b/homeassistant/components/alexa_devices/utils.py @@ -4,8 +4,10 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from typing import Any, Concatenate +from aioamazondevices.const import SPEAKER_GROUP_FAMILY from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er @@ -61,3 +63,21 @@ async def async_update_unique_id( # Update the registry with the new unique_id entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + + +async def async_remove_dnd_from_virtual_group( + hass: HomeAssistant, + coordinator: AmazonDevicesCoordinator, +) -> None: + """Remove entity DND from virtual group.""" + entity_registry = er.async_get(hass) + + for serial_num in coordinator.data: + unique_id = f"{serial_num}-do_not_disturb" + entity_id = entity_registry.async_get_entity_id( + DOMAIN, SWITCH_DOMAIN, unique_id + ) + is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY + if entity_id and is_group: + entity_registry.async_remove(entity_id) + _LOGGER.debug("Removed DND switch from virtual group %s", entity_id) diff --git a/tests/components/alexa_devices/test_binary_sensor.py b/tests/components/alexa_devices/test_binary_sensor.py index bcb89664da4..99f29342b36 100644 --- a/tests/components/alexa_devices/test_binary_sensor.py +++ b/tests/components/alexa_devices/test_binary_sensor.py @@ -11,10 +11,12 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.alexa_devices.const import DOMAIN from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from . import setup_integration from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN @@ -137,3 +139,51 @@ async def test_dynamic_device( assert (state := hass.states.get(entity_id_2)) assert state.state == STATE_ON + + +@pytest.mark.parametrize( + "key", + [ + "bluetooth", + "babyCryDetectionState", + "beepingApplianceDetectionState", + "coughDetectionState", + "dogBarkDetectionState", + "waterSoundsDetectionState", + ], +) +async def test_deprecated_sensor_removal( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + key: str, +) -> None: + """Test deprecated sensors are removed.""" + + mock_config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="Amazon", + model="Echo Dot", + entry_type=dr.DeviceEntryType.SERVICE, + ) + + entity = entity_registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + unique_id=f"{TEST_DEVICE_1_SN}-{key}", + device_id=device.id, + config_entry=mock_config_entry, + has_entity_name=True, + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity2 = entity_registry.async_get(entity.entity_id) + assert entity2 is None diff --git a/tests/components/alexa_devices/test_utils.py b/tests/components/alexa_devices/test_utils.py index 020971d8f76..3424227673c 100644 --- a/tests/components/alexa_devices/test_utils.py +++ b/tests/components/alexa_devices/test_utils.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock +from aioamazondevices.const import SPEAKER_GROUP_FAMILY, SPEAKER_GROUP_MODEL from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData import pytest @@ -94,3 +95,42 @@ async def test_alexa_unique_id_migration( assert migrated_entity is not None assert migrated_entity.config_entry_id == mock_config_entry.entry_id assert migrated_entity.unique_id == f"{TEST_DEVICE_1_SN}-dnd" + + +async def test_alexa_dnd_group_removal( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test dnd switch is removed for Speaker Groups.""" + + mock_config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, mock_config_entry.entry_id)}, + name=mock_config_entry.title, + manufacturer="Amazon", + model=SPEAKER_GROUP_MODEL, + entry_type=dr.DeviceEntryType.SERVICE, + ) + + entity = entity_registry.async_get_or_create( + DOMAIN, + SWITCH_DOMAIN, + unique_id=f"{TEST_DEVICE_1_SN}-do_not_disturb", + device_id=device.id, + config_entry=mock_config_entry, + has_entity_name=True, + ) + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_DEVICE_1_SN + ].device_family = SPEAKER_GROUP_FAMILY + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.states.get(entity.entity_id)