diff --git a/homeassistant/components/alexa_devices/binary_sensor.py b/homeassistant/components/alexa_devices/binary_sensor.py index a7df8b9e8558..bd7b41c6d5d2 100644 --- a/homeassistant/components/alexa_devices/binary_sensor.py +++ b/homeassistant/components/alexa_devices/binary_sensor.py @@ -127,6 +127,7 @@ async def async_setup_entry( def _check_device() -> None: current_devices = set(coordinator.data) + known_devices.intersection_update(current_devices) new_devices = current_devices - known_devices if new_devices: known_devices.update(new_devices) diff --git a/homeassistant/components/alexa_devices/button.py b/homeassistant/components/alexa_devices/button.py index a9712201ca50..73cac4afb037 100644 --- a/homeassistant/components/alexa_devices/button.py +++ b/homeassistant/components/alexa_devices/button.py @@ -51,6 +51,7 @@ async def async_setup_entry( def _check_routines_devices() -> None: current_routines = set(coordinator.api.routines) + known_routines.intersection_update(current_routines) new_routines = current_routines - known_routines if new_routines: known_routines.update(new_routines) @@ -59,6 +60,7 @@ async def async_setup_entry( ) current_devices = set(coordinator.data) + known_devices.intersection_update(current_devices) new_devices = current_devices - known_devices if new_devices: known_devices.update(new_devices) diff --git a/homeassistant/components/alexa_devices/event.py b/homeassistant/components/alexa_devices/event.py index e31f87cd8ec7..16a0b9f47e65 100644 --- a/homeassistant/components/alexa_devices/event.py +++ b/homeassistant/components/alexa_devices/event.py @@ -47,6 +47,7 @@ async def async_setup_entry( def _check_device() -> None: current_devices = set(coordinator.data) + known_devices.intersection_update(current_devices) new_devices = current_devices - known_devices if new_devices: known_devices.update(new_devices) diff --git a/homeassistant/components/alexa_devices/media_player.py b/homeassistant/components/alexa_devices/media_player.py index ca47db6c4cfc..8fe0c38359f7 100644 --- a/homeassistant/components/alexa_devices/media_player.py +++ b/homeassistant/components/alexa_devices/media_player.py @@ -50,6 +50,7 @@ async def async_setup_entry( """Add entities for newly discovered devices.""" new_entities: list[AlexaDevicesMediaPlayer] = [] + known_devices.intersection_update(coordinator.data) for serial_num, device in coordinator.data.items(): if serial_num in known_devices or not device.media_player_supported: continue diff --git a/homeassistant/components/alexa_devices/notify.py b/homeassistant/components/alexa_devices/notify.py index ebd4e0c8451f..a7ad2c52e374 100644 --- a/homeassistant/components/alexa_devices/notify.py +++ b/homeassistant/components/alexa_devices/notify.py @@ -63,6 +63,7 @@ async def async_setup_entry( def _check_device() -> None: current_devices = set(coordinator.data) + known_devices.intersection_update(current_devices) new_devices = current_devices - known_devices if new_devices: known_devices.update(new_devices) diff --git a/homeassistant/components/alexa_devices/select.py b/homeassistant/components/alexa_devices/select.py index 49c559c1583d..9027659978fe 100644 --- a/homeassistant/components/alexa_devices/select.py +++ b/homeassistant/components/alexa_devices/select.py @@ -63,6 +63,7 @@ async def async_setup_entry( def _check_device() -> None: current_devices = set(coordinator.data) + known_devices.intersection_update(current_devices) new_devices = current_devices - known_devices if new_devices: known_devices.update(new_devices) diff --git a/homeassistant/components/alexa_devices/sensor.py b/homeassistant/components/alexa_devices/sensor.py index 648954360e0c..c443c9a37c02 100644 --- a/homeassistant/components/alexa_devices/sensor.py +++ b/homeassistant/components/alexa_devices/sensor.py @@ -159,6 +159,7 @@ async def async_setup_entry( def _check_device() -> None: current_devices = set(coordinator.data) + known_devices.intersection_update(current_devices) new_devices = current_devices - known_devices if new_devices: known_devices.update(new_devices) diff --git a/homeassistant/components/alexa_devices/switch.py b/homeassistant/components/alexa_devices/switch.py index e5031d18773e..05af1ab42912 100644 --- a/homeassistant/components/alexa_devices/switch.py +++ b/homeassistant/components/alexa_devices/switch.py @@ -101,6 +101,7 @@ async def async_setup_entry( def _check_device() -> None: current_devices = set(coordinator.data) + known_devices.intersection_update(current_devices) new_devices = current_devices - known_devices if new_devices: known_devices.update(new_devices) diff --git a/homeassistant/components/alexa_devices/todo.py b/homeassistant/components/alexa_devices/todo.py index 5b9190a07039..449e0660b5cb 100644 --- a/homeassistant/components/alexa_devices/todo.py +++ b/homeassistant/components/alexa_devices/todo.py @@ -40,6 +40,7 @@ async def async_setup_entry( def _check_lists() -> None: current_list_ids = {todo_list.id for todo_list in coordinator.api.todo_lists} + known_list_ids.intersection_update(current_list_ids) new_list_ids = current_list_ids - known_list_ids if new_list_ids: known_list_ids.update(new_list_ids) diff --git a/tests/components/alexa_devices/__init__.py b/tests/components/alexa_devices/__init__.py index 24348248e0c9..2e1e8a39f322 100644 --- a/tests/components/alexa_devices/__init__.py +++ b/tests/components/alexa_devices/__init__.py @@ -1,8 +1,15 @@ """Tests for the Alexa Devices integration.""" +from collections.abc import Mapping +from unittest.mock import AsyncMock + +from aioamazondevices.structures import AmazonDevice +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: @@ -11,3 +18,33 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + + +async def assert_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + config_entry: MockConfigEntry, + entity_id: str, + devices_with: Mapping[str, AmazonDevice], + devices_without: Mapping[str, AmazonDevice], +) -> None: + """Assert an entity is recreated when its device is removed and re-added.""" + mock_amazon_devices_client.get_devices_data.return_value = dict(devices_with) + await setup_integration(hass, config_entry) + + assert hass.states.get(entity_id) is not None + + mock_amazon_devices_client.get_devices_data.return_value = dict(devices_without) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id) is None + + mock_amazon_devices_client.get_devices_data.return_value = dict(devices_with) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id) is not None diff --git a/tests/components/alexa_devices/test_binary_sensor.py b/tests/components/alexa_devices/test_binary_sensor.py index e6384e1fbd98..483e5ffc97d3 100644 --- a/tests/components/alexa_devices/test_binary_sensor.py +++ b/tests/components/alexa_devices/test_binary_sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import setup_integration +from . import assert_device_removed_and_readded, setup_integration from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -142,6 +142,24 @@ async def test_dynamic_device( assert state.state == STATE_ON +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entities are recreated when a device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id="binary_sensor.echo_test_2_connectivity", + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1, TEST_DEVICE_2_SN: TEST_DEVICE_2}, + devices_without={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + ) + + @pytest.mark.parametrize( "key", [ diff --git a/tests/components/alexa_devices/test_button.py b/tests/components/alexa_devices/test_button.py index 265ae612b208..8d96c9b1ef2c 100644 --- a/tests/components/alexa_devices/test_button.py +++ b/tests/components/alexa_devices/test_button.py @@ -13,8 +13,14 @@ 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_DEVICE_1, TEST_USERNAME +from . import assert_device_removed_and_readded, setup_integration +from .const import ( + TEST_DEVICE_1, + TEST_DEVICE_1_SN, + TEST_DEVICE_2, + TEST_DEVICE_2_SN, + TEST_USERNAME, +) from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -96,6 +102,55 @@ async def test_dynamic_entities( assert hass.states.get(entity_id) is None +async def test_routine_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test routine button is recreated when removed and re-added.""" + + entity_id = f"button.{slugify(TEST_USERNAME)}_test_routine" + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get(entity_id) is not None + + mock_amazon_devices_client.routines = [] + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id) is None + + mock_amazon_devices_client.routines = ["Test Routine"] + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id) is not None + + +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test device button is recreated when its device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id="button.echo_test_2_restart", + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1, TEST_DEVICE_2_SN: TEST_DEVICE_2}, + devices_without={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + ) + + async def test_restart_button( hass: HomeAssistant, mock_amazon_devices_client: AsyncMock, diff --git a/tests/components/alexa_devices/test_event.py b/tests/components/alexa_devices/test_event.py index ddf90e744a54..3cc5b70de786 100644 --- a/tests/components/alexa_devices/test_event.py +++ b/tests/components/alexa_devices/test_event.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -9,8 +10,14 @@ from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration -from .const import TEST_DEVICE_1_SN, TEST_VOCAL_RECORD_EVENT +from . import assert_device_removed_and_readded, setup_integration +from .const import ( + TEST_DEVICE_1, + TEST_DEVICE_1_SN, + TEST_DEVICE_2, + TEST_DEVICE_2_SN, + TEST_VOCAL_RECORD_EVENT, +) from tests.common import MockConfigEntry, snapshot_platform @@ -69,3 +76,21 @@ async def test_no_vocal_record_skips_event_trigger( assert (state := hass.states.get(ENTITY_ID)) assert state.state == STATE_UNKNOWN assert state.attributes.get("event_type") is None + + +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entities are recreated when a device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id="event.echo_test_2_voice_event", + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1, TEST_DEVICE_2_SN: TEST_DEVICE_2}, + devices_without={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + ) diff --git a/tests/components/alexa_devices/test_media_player.py b/tests/components/alexa_devices/test_media_player.py index c470eeafa473..65680fa9a0e7 100644 --- a/tests/components/alexa_devices/test_media_player.py +++ b/tests/components/alexa_devices/test_media_player.py @@ -39,8 +39,8 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration -from .const import TEST_DEVICE_1_SN +from . import assert_device_removed_and_readded, setup_integration +from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -259,6 +259,24 @@ async def test_offline_device_recovers( assert state.state != STATE_UNAVAILABLE +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entity is recreated when a device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id=ENTITY_ID, + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + devices_without={}, + ) + + @pytest.mark.parametrize( ("player_state", "expected_ha_state"), [ diff --git a/tests/components/alexa_devices/test_notify.py b/tests/components/alexa_devices/test_notify.py index 3132b967487a..485bb9ce354d 100644 --- a/tests/components/alexa_devices/test_notify.py +++ b/tests/components/alexa_devices/test_notify.py @@ -18,8 +18,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from . import setup_integration -from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN +from . import assert_device_removed_and_readded, setup_integration +from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -104,6 +104,24 @@ async def test_offline_device( assert state.state != STATE_UNAVAILABLE +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entities are recreated when a device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id="notify.echo_test_2_announce", + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1, TEST_DEVICE_2_SN: TEST_DEVICE_2}, + devices_without={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + ) + + async def test_announce_unavailable_when_communications_off( hass: HomeAssistant, freezer: FrozenDateTimeFactory, diff --git a/tests/components/alexa_devices/test_select.py b/tests/components/alexa_devices/test_select.py index 55dc4b2ccb05..b86a382956d3 100644 --- a/tests/components/alexa_devices/test_select.py +++ b/tests/components/alexa_devices/test_select.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er -from . import setup_integration +from . import assert_device_removed_and_readded, setup_integration from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -106,6 +106,24 @@ async def test_offline_device( assert state.state == STATE_UNAVAILABLE +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entities are recreated when a device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id=ENTITY_ID, + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + devices_without={}, + ) + + async def test_service_select_option( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/alexa_devices/test_sensor.py b/tests/components/alexa_devices/test_sensor.py index 58930f78b3fe..902024dd81da 100644 --- a/tests/components/alexa_devices/test_sensor.py +++ b/tests/components/alexa_devices/test_sensor.py @@ -18,8 +18,8 @@ from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration -from .const import TEST_DEVICE_1_SN +from . import assert_device_removed_and_readded, setup_integration +from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -103,6 +103,24 @@ async def test_offline_device( assert state.state != STATE_UNAVAILABLE +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entities are recreated when a device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id="sensor.echo_test_2_temperature", + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1, TEST_DEVICE_2_SN: TEST_DEVICE_2}, + devices_without={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + ) + + @pytest.mark.parametrize( ("sensor", "api_value", "scale", "state_value", "unit"), [ diff --git a/tests/components/alexa_devices/test_switch.py b/tests/components/alexa_devices/test_switch.py index f2cb257575bd..4d45f52a992b 100644 --- a/tests/components/alexa_devices/test_switch.py +++ b/tests/components/alexa_devices/test_switch.py @@ -24,7 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration +from . import assert_device_removed_and_readded, setup_integration from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -164,6 +164,24 @@ async def test_offline_device( assert state.state != STATE_UNAVAILABLE +async def test_device_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entities are recreated when a device is removed and re-added.""" + await assert_device_removed_and_readded( + hass, + freezer, + mock_amazon_devices_client, + mock_config_entry, + entity_id=ENTITY_ID, + devices_with={TEST_DEVICE_1_SN: TEST_DEVICE_1}, + devices_without={}, + ) + + async def test_switch_communication( hass: HomeAssistant, freezer: FrozenDateTimeFactory, diff --git a/tests/components/alexa_devices/test_todo.py b/tests/components/alexa_devices/test_todo.py index 64663d74ab8f..0ca91203e580 100644 --- a/tests/components/alexa_devices/test_todo.py +++ b/tests/components/alexa_devices/test_todo.py @@ -570,6 +570,37 @@ async def test_dynamic_entities( assert hass.states.get(entity_id) is None +async def test_list_removed_and_readded( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entity is recreated when a list is removed and re-added.""" + mock_amazon_devices_client.todo_lists = [MOCK_TODO_LIST] + mock_amazon_devices_client.get_todo_list_items = AsyncMock(return_value={}) + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get(MOCK_TODO_LIST_ENTITY_ID) is not None + + mock_amazon_devices_client.todo_lists = [] + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(MOCK_TODO_LIST_ENTITY_ID) is None + + mock_amazon_devices_client.todo_lists = [MOCK_TODO_LIST] + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(MOCK_TODO_LIST_ENTITY_ID) is not None + + async def test_dynamic_add_list_and_add_item( hass: HomeAssistant, freezer: FrozenDateTimeFactory,