alexa_devices: remove stale values from known entities (#182401)

This commit is contained in:
jameson_uk
2026-09-19 16:00:54 +02:00
committed by GitHub
parent a8d46dbf1f
commit 70671b8a6d
19 changed files with 280 additions and 14 deletions
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
+38 -1
View File
@@ -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
@@ -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",
[
+57 -2
View File
@@ -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,
+27 -2
View File
@@ -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},
)
@@ -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"),
[
+20 -2
View File
@@ -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,
+19 -1
View File
@@ -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,
+20 -2
View File
@@ -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"),
[
+19 -1
View File
@@ -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,
@@ -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,