From f743def5f189686e98bfd3ccb6778ac1835a498a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 13 Apr 2026 17:47:07 +0000 Subject: [PATCH 1/5] Add routine management to Alexa Devices --- .../components/alexa_devices/__init__.py | 1 + .../components/alexa_devices/button.py | 59 +++++++++++++++++++ .../components/alexa_devices/entity.py | 38 ++++++++---- .../components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/alexa_devices/conftest.py | 2 +- .../alexa_devices/snapshots/test_button.ambr | 51 ++++++++++++++++ tests/components/alexa_devices/test_button.py | 53 +++++++++++++++++ 9 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/alexa_devices/button.py create mode 100644 tests/components/alexa_devices/snapshots/test_button.ambr create mode 100644 tests/components/alexa_devices/test_button.py diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index af0a3d7818cc34..4e51047696937d 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -11,6 +11,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/alexa_devices/button.py b/homeassistant/components/alexa_devices/button.py new file mode 100644 index 00000000000000..597cf88acf7bba --- /dev/null +++ b/homeassistant/components/alexa_devices/button.py @@ -0,0 +1,59 @@ +"""Support for buttons.""" + +from homeassistant.components.button import ButtonEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util import slugify + +from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator +from .entity import AmazonEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AmazonConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up button entities for Alexa Devices.""" + coordinator = entry.runtime_data + + known_routines: set[str] = set() + + def _check_routines() -> None: + current_routines = set(coordinator.api.routines) + new_routines = current_routines - known_routines + if new_routines: + known_routines.update(new_routines) + async_add_entities( + AmazonRoutineButton(coordinator, routine) for routine in new_routines + ) + + _check_routines() + entry.async_on_unload(coordinator.async_add_listener(_check_routines)) + + +class AmazonRoutineButton(AmazonEntity, ButtonEntity): + """Button entity for Alexa routine.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None: + """Initialize the routine button entity.""" + self._coordinator = coordinator + self._routine = routine + super().__init__( + coordinator, + coordinator.config_entry.title, + EntityDescription(key=slugify(routine), name=routine), + connect_to_hub=True, + ) + + @property + def available(self) -> bool: + """Routines are always available.""" + return True + + async def async_press(self) -> None: + """Handle button press action.""" + await self._coordinator.api.call_routine(self._routine) diff --git a/homeassistant/components/alexa_devices/entity.py b/homeassistant/components/alexa_devices/entity.py index 21b01e26f6ccb8..0176436106e56c 100644 --- a/homeassistant/components/alexa_devices/entity.py +++ b/homeassistant/components/alexa_devices/entity.py @@ -2,9 +2,10 @@ from aioamazondevices.structures import AmazonDevice -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import slugify from .const import DOMAIN from .coordinator import AmazonDevicesCoordinator @@ -20,20 +21,30 @@ def __init__( coordinator: AmazonDevicesCoordinator, serial_num: str, description: EntityDescription, + connect_to_hub: bool = False, ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._serial_num = serial_num - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, serial_num)}, - name=self.device.account_name, - model=self.device.model, - model_id=self.device.device_type, - manufacturer=self.device.manufacturer or "Amazon", - hw_version=self.device.hardware_version, - sw_version=self.device.software_version, - serial_number=serial_num, - ) + if connect_to_hub: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, service_device_id(coordinator))}, + manufacturer="Amazon", + name=coordinator.config_entry.title, + entry_type=DeviceEntryType.SERVICE, + ) + else: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_num)}, + name=self.device.account_name, + model=self.device.model, + model_id=self.device.device_type, + manufacturer=self.device.manufacturer or "Amazon", + hw_version=self.device.hardware_version, + sw_version=self.device.software_version, + serial_number=serial_num, + via_device=(DOMAIN, service_device_id(coordinator)), + ) self.entity_description = description self._attr_unique_id = f"{serial_num}-{description.key}" @@ -50,3 +61,8 @@ def available(self) -> bool: and self._serial_num in self.coordinator.data and self.device.online ) + + +def service_device_id(coordinator: AmazonDevicesCoordinator) -> str: + """Return service device id.""" + return slugify(f"{coordinator.config_entry.title}_service_device") diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index eccb7a524504e9..ed17385e825e5c 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "platinum", - "requirements": ["aioamazondevices==13.4.0"] + "requirements": ["aioamazondevices==13.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 068fbdc6eed81f..ca17324d56e301 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.5 # homeassistant.components.alexa_devices -aioamazondevices==13.4.0 +aioamazondevices==13.4.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06602034de967c..d0a0ed77922a8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.5 # homeassistant.components.alexa_devices -aioamazondevices==13.4.0 +aioamazondevices==13.4.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index d40b562a56e1f5..f2296ca83ae128 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -59,7 +59,7 @@ def mock_config_entry() -> MockConfigEntry: """Mock a config entry.""" return MockConfigEntry( domain=DOMAIN, - title="Amazon Test Account", + title=TEST_USERNAME, data={ CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, diff --git a/tests/components/alexa_devices/snapshots/test_button.ambr b/tests/components/alexa_devices/snapshots/test_button.ambr new file mode 100644 index 00000000000000..05edef66642a57 --- /dev/null +++ b/tests/components/alexa_devices/snapshots/test_button.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_all_entities[button.fake_email_gmail_com_test_routine-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.fake_email_gmail_com_test_routine', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Test Routine', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Test Routine', + 'platform': 'alexa_devices', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'fake_email@gmail.com-test_routine', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[button.fake_email_gmail_com_test_routine-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'fake_email@gmail.com Test Routine', + }), + 'context': , + 'entity_id': 'button.fake_email_gmail_com_test_routine', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/alexa_devices/test_button.py b/tests/components/alexa_devices/test_button.py new file mode 100644 index 00000000000000..3e778c1aecd424 --- /dev/null +++ b/tests/components/alexa_devices/test_button.py @@ -0,0 +1,53 @@ +"""Test Alexa Devices button entities.""" + +from unittest.mock import AsyncMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util import slugify + +from . import setup_integration +from .const import TEST_USERNAME + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + + mock_amazon_devices_client.routines = ["Test Routine"] + + with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_pressing_routine_button( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test routine run button.""" + + mock_amazon_devices_client.routines = ["Test Routine"] + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: f"button.{slugify(TEST_USERNAME)}_test_routine"}, + blocking=True, + ) + mock_amazon_devices_client.call_routine.assert_called_once() From 22debd0bae2a91bcca5f58246f171465b1d34f3d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 13 Apr 2026 18:50:23 +0000 Subject: [PATCH 2/5] missing PARALLEL_UPDATES --- homeassistant/components/alexa_devices/button.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/alexa_devices/button.py b/homeassistant/components/alexa_devices/button.py index 597cf88acf7bba..d565de54e47a99 100644 --- a/homeassistant/components/alexa_devices/button.py +++ b/homeassistant/components/alexa_devices/button.py @@ -9,6 +9,9 @@ from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator from .entity import AmazonEntity +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, From f5d3ca421a1f705fa8a2ff09ec81d6c49f62a008 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 14 Apr 2026 17:11:58 +0000 Subject: [PATCH 3/5] apply review comments --- .../components/alexa_devices/button.py | 10 +--- .../components/alexa_devices/entity.py | 55 ++++++++++++------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/alexa_devices/button.py b/homeassistant/components/alexa_devices/button.py index d565de54e47a99..d584b5b61a1625 100644 --- a/homeassistant/components/alexa_devices/button.py +++ b/homeassistant/components/alexa_devices/button.py @@ -7,7 +7,7 @@ from homeassistant.util import slugify from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator -from .entity import AmazonEntity +from .entity import AmazonServiceEntity # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -36,7 +36,7 @@ def _check_routines() -> None: entry.async_on_unload(coordinator.async_add_listener(_check_routines)) -class AmazonRoutineButton(AmazonEntity, ButtonEntity): +class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity): """Button entity for Alexa routine.""" _attr_has_entity_name = True @@ -49,14 +49,8 @@ def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None: coordinator, coordinator.config_entry.title, EntityDescription(key=slugify(routine), name=routine), - connect_to_hub=True, ) - @property - def available(self) -> bool: - """Routines are always available.""" - return True - async def async_press(self) -> None: """Handle button press action.""" await self._coordinator.api.call_routine(self._routine) diff --git a/homeassistant/components/alexa_devices/entity.py b/homeassistant/components/alexa_devices/entity.py index 0176436106e56c..83f637dff06bd0 100644 --- a/homeassistant/components/alexa_devices/entity.py +++ b/homeassistant/components/alexa_devices/entity.py @@ -21,30 +21,20 @@ def __init__( coordinator: AmazonDevicesCoordinator, serial_num: str, description: EntityDescription, - connect_to_hub: bool = False, ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._serial_num = serial_num - if connect_to_hub: - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, service_device_id(coordinator))}, - manufacturer="Amazon", - name=coordinator.config_entry.title, - entry_type=DeviceEntryType.SERVICE, - ) - else: - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, serial_num)}, - name=self.device.account_name, - model=self.device.model, - model_id=self.device.device_type, - manufacturer=self.device.manufacturer or "Amazon", - hw_version=self.device.hardware_version, - sw_version=self.device.software_version, - serial_number=serial_num, - via_device=(DOMAIN, service_device_id(coordinator)), - ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_num)}, + name=self.device.account_name, + model=self.device.model, + model_id=self.device.device_type, + manufacturer=self.device.manufacturer or "Amazon", + hw_version=self.device.hardware_version, + sw_version=self.device.software_version, + serial_number=serial_num, + ) self.entity_description = description self._attr_unique_id = f"{serial_num}-{description.key}" @@ -63,6 +53,31 @@ def available(self) -> bool: ) +class AmazonServiceEntity(CoordinatorEntity[AmazonDevicesCoordinator]): + """Defines Alexa Devices entity for service device.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AmazonDevicesCoordinator, + serial_num: str, + description: EntityDescription, + ) -> None: + """Initialize the service entity.""" + + super().__init__(coordinator) + self._serial_num = serial_num + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, service_device_id(coordinator))}, + manufacturer="Amazon", + name=coordinator.config_entry.title, + entry_type=DeviceEntryType.SERVICE, + ) + self.entity_description = description + self._attr_unique_id = f"{serial_num}-{description.key}" + + def service_device_id(coordinator: AmazonDevicesCoordinator) -> str: """Return service device id.""" return slugify(f"{coordinator.config_entry.title}_service_device") From 4de2c75a996690f66ca7d4b1973dd04648670dd0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 14 Apr 2026 19:10:09 +0000 Subject: [PATCH 4/5] code cleanup, remote stale routines, improve tests --- .../components/alexa_devices/button.py | 1 - .../components/alexa_devices/coordinator.py | 39 ++++++++++++-- .../components/alexa_devices/entity.py | 6 +-- tests/components/alexa_devices/conftest.py | 1 + .../alexa_devices/snapshots/test_button.ambr | 2 +- tests/components/alexa_devices/test_button.py | 53 +++++++++++++++++-- 6 files changed, 89 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alexa_devices/button.py b/homeassistant/components/alexa_devices/button.py index d584b5b61a1625..9a735f550fc1e1 100644 --- a/homeassistant/components/alexa_devices/button.py +++ b/homeassistant/components/alexa_devices/button.py @@ -47,7 +47,6 @@ def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None: self._routine = routine super().__init__( coordinator, - coordinator.config_entry.title, EntityDescription(key=slugify(routine), name=routine), ) diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index 8988d3e13cf785..054152f463fb1f 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -12,12 +12,13 @@ from aiohttp import ClientSession from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import slugify from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN @@ -64,6 +65,13 @@ def __init__( for identifier_domain, identifier in device.identifiers if identifier_domain == DOMAIN } + self.previous_routines: set[str] = { + routine.unique_id + for routine in er.async_entries_for_config_entry( + er.async_get(hass), entry.entry_id + ) + if routine.domain == Platform.BUTTON + } async def _async_update_data(self) -> dict[str, AmazonDevice]: """Update device data.""" @@ -92,8 +100,13 @@ async def _async_update_data(self) -> dict[str, AmazonDevice]: current_devices = set(data.keys()) if stale_devices := self.previous_devices - current_devices: await self._async_remove_device_stale(stale_devices) - self.previous_devices = current_devices + + current_routines = {slugify(routine) for routine in self.api.routines} + if stale_routines := self.previous_routines - current_routines: + await self._async_remove_routine_stale(stale_routines) + self.previous_routines = current_routines + return data async def _async_remove_device_stale( @@ -116,3 +129,23 @@ async def _async_remove_device_stale( device_id=device.id, remove_config_entry_id=self.config_entry.entry_id, ) + + async def _async_remove_routine_stale( + self, + stale_routines: set[str], + ) -> None: + """Remove stale routine.""" + entity_registry = er.async_get(self.hass) + + for routine in stale_routines: + _LOGGER.debug( + "Detected change in routines: routine %s removed", + routine, + ) + entity_id = entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{slugify(self.config_entry.title)}-{slugify(routine)}", + ) + if entity_id: + entity_registry.async_remove(entity_id) diff --git a/homeassistant/components/alexa_devices/entity.py b/homeassistant/components/alexa_devices/entity.py index 83f637dff06bd0..ff51c807ff2d6f 100644 --- a/homeassistant/components/alexa_devices/entity.py +++ b/homeassistant/components/alexa_devices/entity.py @@ -61,13 +61,11 @@ class AmazonServiceEntity(CoordinatorEntity[AmazonDevicesCoordinator]): def __init__( self, coordinator: AmazonDevicesCoordinator, - serial_num: str, description: EntityDescription, ) -> None: """Initialize the service entity.""" super().__init__(coordinator) - self._serial_num = serial_num self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, service_device_id(coordinator))}, manufacturer="Amazon", @@ -75,7 +73,9 @@ def __init__( entry_type=DeviceEntryType.SERVICE, ) self.entity_description = description - self._attr_unique_id = f"{serial_num}-{description.key}" + self._attr_unique_id = ( + f"{slugify(coordinator.config_entry.title)}-{description.key}" + ) def service_device_id(coordinator: AmazonDevicesCoordinator) -> str: diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index f2296ca83ae128..49729de4530734 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -50,6 +50,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: client.get_devices_data.return_value = { TEST_DEVICE_1_SN: deepcopy(TEST_DEVICE_1) } + client.routines = ["Test Routine"] client.send_sound_notification = AsyncMock() yield client diff --git a/tests/components/alexa_devices/snapshots/test_button.ambr b/tests/components/alexa_devices/snapshots/test_button.ambr index 05edef66642a57..dd0dc0d0da021f 100644 --- a/tests/components/alexa_devices/snapshots/test_button.ambr +++ b/tests/components/alexa_devices/snapshots/test_button.ambr @@ -32,7 +32,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'fake_email@gmail.com-test_routine', + 'unique_id': 'fake_email_gmail_com-test_routine', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/alexa_devices/test_button.py b/tests/components/alexa_devices/test_button.py index 3e778c1aecd424..a1bba9b7e1a94d 100644 --- a/tests/components/alexa_devices/test_button.py +++ b/tests/components/alexa_devices/test_button.py @@ -2,8 +2,11 @@ from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant @@ -13,7 +16,7 @@ from . import setup_integration from .const import TEST_USERNAME -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_all_entities( @@ -25,8 +28,6 @@ async def test_all_entities( ) -> None: """Test all entities.""" - mock_amazon_devices_client.routines = ["Test Routine"] - with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.BUTTON]): await setup_integration(hass, mock_config_entry) @@ -40,8 +41,6 @@ async def test_pressing_routine_button( ) -> None: """Test routine run button.""" - mock_amazon_devices_client.routines = ["Test Routine"] - await setup_integration(hass, mock_config_entry) await hass.services.async_call( @@ -51,3 +50,47 @@ async def test_pressing_routine_button( blocking=True, ) mock_amazon_devices_client.call_routine.assert_called_once() + + +@pytest.mark.parametrize( + ("initial_routine", "updated_routines"), + [ + (["Test Routine"], ["Test Routine", "New Routine"]), # Add a routine + (["Test Routine", "New Routine"], ["Test Routine"]), # Remove a routine + (["Test Routine"], []), # Remove all routines + ], +) +async def test_dynamic_entities( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + initial_routine: list[str], + updated_routines: list[str], +) -> None: + """Test entities are dynamically created and deleted.""" + + mock_amazon_devices_client.routines = initial_routine + + await setup_integration(hass, mock_config_entry) + + # Check initial routine(s) exist + for routine in initial_routine: + entity_id = f"button.{slugify(TEST_USERNAME)}_{slugify(routine)}" + assert hass.states.get(entity_id) is not None + + mock_amazon_devices_client.routines = updated_routines + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # After update, check which routines should exist + for routine in updated_routines: + entity_id = f"button.{slugify(TEST_USERNAME)}_{slugify(routine)}" + assert hass.states.get(entity_id) is not None + + # Check routines that were removed no longer exist + for routine in set(initial_routine) - set(updated_routines): + entity_id = f"button.{slugify(TEST_USERNAME)}_{slugify(routine)}" + assert hass.states.get(entity_id) is None From 85128d4789ccaa6d91f0865dec724eabe5c3ea6b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 18 Apr 2026 22:08:25 +0000 Subject: [PATCH 5/5] fix id and related tests --- .../components/alexa_devices/coordinator.py | 2 +- homeassistant/components/alexa_devices/entity.py | 5 ++--- tests/components/alexa_devices/conftest.py | 12 +++++++++--- tests/components/alexa_devices/const.py | 1 + .../alexa_devices/snapshots/test_button.ambr | 2 +- .../alexa_devices/snapshots/test_diagnostics.ambr | 2 +- tests/components/alexa_devices/test_config_flow.py | 14 +++++++------- tests/components/alexa_devices/test_init.py | 4 ++-- 8 files changed, 24 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index 054152f463fb1f..a5414722baa806 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -145,7 +145,7 @@ async def _async_remove_routine_stale( entity_id = entity_registry.async_get_entity_id( Platform.BUTTON, DOMAIN, - f"{slugify(self.config_entry.title)}-{slugify(routine)}", + f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}", ) if entity_id: entity_registry.async_remove(entity_id) diff --git a/homeassistant/components/alexa_devices/entity.py b/homeassistant/components/alexa_devices/entity.py index ff51c807ff2d6f..57a67d9d31f876 100644 --- a/homeassistant/components/alexa_devices/entity.py +++ b/homeassistant/components/alexa_devices/entity.py @@ -69,15 +69,14 @@ def __init__( self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, service_device_id(coordinator))}, manufacturer="Amazon", - name=coordinator.config_entry.title, entry_type=DeviceEntryType.SERVICE, ) self.entity_description = description self._attr_unique_id = ( - f"{slugify(coordinator.config_entry.title)}-{description.key}" + f"{slugify(coordinator.config_entry.unique_id)}-{description.key}" ) def service_device_id(coordinator: AmazonDevicesCoordinator) -> str: """Return service device id.""" - return slugify(f"{coordinator.config_entry.title}_service_device") + return slugify(f"{coordinator.config_entry.unique_id}_service_device") diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index 49729de4530734..b2cdeddacab1ea 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -13,7 +13,13 @@ ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USERNAME +from .const import ( + TEST_DEVICE_1, + TEST_DEVICE_1_SN, + TEST_PASSWORD, + TEST_USER_ID, + TEST_USERNAME, +) from tests.common import MockConfigEntry @@ -44,7 +50,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: client = mock_client.return_value client.login = AsyncMock() client.login.login_mode_interactive.return_value = { - "customer_info": {"user_id": TEST_USERNAME}, + "customer_info": {"user_id": TEST_USER_ID}, CONF_SITE: "https://www.amazon.com", } client.get_devices_data.return_value = { @@ -69,7 +75,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_SITE: "https://www.amazon.com", }, }, - unique_id=TEST_USERNAME, + unique_id=TEST_USER_ID, version=1, minor_version=3, ) diff --git a/tests/components/alexa_devices/const.py b/tests/components/alexa_devices/const.py index 77eee5133b1cee..e01647a71dcfa7 100644 --- a/tests/components/alexa_devices/const.py +++ b/tests/components/alexa_devices/const.py @@ -12,6 +12,7 @@ TEST_CODE = "023123" TEST_PASSWORD = "fake_password" TEST_USERNAME = "fake_email@gmail.com" +TEST_USER_ID = "amzn1.account.fake_user_id" TEST_DEVICE_1_SN = "echo_test_serial_number" TEST_DEVICE_1_ID = "echo_test_device_id" diff --git a/tests/components/alexa_devices/snapshots/test_button.ambr b/tests/components/alexa_devices/snapshots/test_button.ambr index dd0dc0d0da021f..ce237adb111924 100644 --- a/tests/components/alexa_devices/snapshots/test_button.ambr +++ b/tests/components/alexa_devices/snapshots/test_button.ambr @@ -32,7 +32,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'fake_email_gmail_com-test_routine', + 'unique_id': 'amzn1_account_fake_user_id-test_routine', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr index 7388d97d158016..8c30470005f643 100644 --- a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr +++ b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr @@ -97,7 +97,7 @@ 'subentries': list([ ]), 'title': '**REDACTED**', - 'unique_id': 'fake_email@gmail.com', + 'unique_id': 'amzn1.account.fake_user_id', 'version': 1, }), }) diff --git a/tests/components/alexa_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py index 1368b357610467..7b19e28c080eac 100644 --- a/tests/components/alexa_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import TEST_CODE, TEST_PASSWORD, TEST_USERNAME +from .const import TEST_CODE, TEST_PASSWORD, TEST_USER_ID, TEST_USERNAME from tests.common import MockConfigEntry @@ -51,11 +51,11 @@ async def test_full_flow( CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: { - "customer_info": {"user_id": TEST_USERNAME}, + "customer_info": {"user_id": TEST_USER_ID}, CONF_SITE: "https://www.amazon.com", }, } - assert result["result"].unique_id == TEST_USERNAME + assert result["result"].unique_id == TEST_USER_ID mock_amazon_devices_client.login.login_mode_interactive.assert_called_once_with( "023123" ) @@ -170,7 +170,7 @@ async def test_reauth_successful( CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: "other_fake_password", CONF_LOGIN_DATA: { - "customer_info": {"user_id": TEST_USERNAME}, + "customer_info": {"user_id": TEST_USER_ID}, CONF_SITE: "https://www.amazon.com", }, } @@ -228,7 +228,7 @@ async def test_reauth_not_successful( CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: "fake_password", CONF_LOGIN_DATA: { - "customer_info": {"user_id": TEST_USERNAME}, + "customer_info": {"user_id": TEST_USER_ID}, CONF_SITE: "https://www.amazon.com", }, } @@ -268,7 +268,7 @@ async def test_reconfigure_successful( CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: new_password, CONF_LOGIN_DATA: { - "customer_info": {"user_id": TEST_USERNAME}, + "customer_info": {"user_id": TEST_USER_ID}, CONF_SITE: "https://www.amazon.com", }, } @@ -327,7 +327,7 @@ async def test_reconfigure_fails( CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: { - "customer_info": {"user_id": TEST_USERNAME}, + "customer_info": {"user_id": TEST_USER_ID}, CONF_SITE: "https://www.amazon.com", }, } diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index 0b20b1fe239e7d..623c1a7315df54 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -16,7 +16,7 @@ from homeassistant.helpers import device_registry as dr from . import setup_integration -from .const import TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USERNAME +from .const import TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USER_ID, TEST_USERNAME from tests.common import MockConfigEntry @@ -109,7 +109,7 @@ async def test_migrate_entry( CONF_PASSWORD: TEST_PASSWORD, **(extra_data), }, - unique_id=TEST_USERNAME, + unique_id=TEST_USER_ID, version=1, minor_version=minor_version, )