Remove stale devices for Alexa Devices (#151909)

This commit is contained in:
Simone Chemelli
2025-09-10 19:20:40 +02:00
committed by GitHub
parent 6f00f8a920
commit 6a482b1a3e
5 changed files with 132 additions and 27 deletions
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
@@ -48,12 +49,13 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA],
)
self.previous_devices: set[str] = set()
async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""
try:
await self.api.login_mode_stored_data()
return await self.api.get_devices_data()
data = await self.api.get_devices_data()
except CannotConnect as err:
raise UpdateFailed(
translation_domain=DOMAIN,
@@ -72,3 +74,31 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
else:
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
return data
async def _async_remove_device_stale(
self,
stale_devices: set[str],
) -> None:
"""Remove stale device."""
device_registry = dr.async_get(self.hass)
for serial_num in stale_devices:
_LOGGER.debug(
"Detected change in devices: serial %s removed",
serial_num,
)
device = device_registry.async_get_device(
identifiers={(DOMAIN, serial_num)}
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
@@ -64,9 +64,7 @@ rules:
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices:
status: todo
comment: automate the cleanup process
stale-devices: done
# Platinum
async-dependency: done
+3 -23
View File
@@ -1,9 +1,9 @@
"""Alexa Devices tests configuration."""
from collections.abc import Generator
from copy import deepcopy
from unittest.mock import AsyncMock, patch
from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL
import pytest
@@ -14,7 +14,7 @@ from homeassistant.components.alexa_devices.const import (
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from .const import TEST_DEVICE, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -47,27 +47,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
"customer_info": {"user_id": TEST_USERNAME},
}
client.get_devices_data.return_value = {
TEST_SERIAL_NUMBER: AmazonDevice(
account_name="Echo Test",
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
device_family="mine",
device_type="echo",
device_owner_customer_id="amazon_ower_id",
device_cluster_members=[TEST_SERIAL_NUMBER],
online=True,
serial_number=TEST_SERIAL_NUMBER,
software_version="echo_test_software_version",
do_not_disturb=False,
response_style=None,
bluetooth_state=True,
entity_id="11111111-2222-3333-4444-555555555555",
appliance_id="G1234567890123456789012345678A",
sensors={
"temperature": AmazonDeviceSensor(
name="temperature", value="22.5", scale="CELSIUS"
)
},
)
TEST_SERIAL_NUMBER: deepcopy(TEST_DEVICE)
}
client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get(
device.device_type
+24
View File
@@ -1,8 +1,32 @@
"""Alexa Devices tests const."""
from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
TEST_CODE = "023123"
TEST_PASSWORD = "fake_password"
TEST_SERIAL_NUMBER = "echo_test_serial_number"
TEST_USERNAME = "fake_email@gmail.com"
TEST_DEVICE_ID = "echo_test_device_id"
TEST_DEVICE = AmazonDevice(
account_name="Echo Test",
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
device_family="mine",
device_type="echo",
device_owner_customer_id="amazon_ower_id",
device_cluster_members=[TEST_SERIAL_NUMBER],
online=True,
serial_number=TEST_SERIAL_NUMBER,
software_version="echo_test_software_version",
do_not_disturb=False,
response_style=None,
bluetooth_state=True,
entity_id="11111111-2222-3333-4444-555555555555",
appliance_id="G1234567890123456789012345678A",
sensors={
"temperature": AmazonDeviceSensor(
name="temperature", value="22.5", scale="CELSIUS"
)
},
)
@@ -0,0 +1,73 @@
"""Tests for the Alexa Devices coordinator."""
from unittest.mock import AsyncMock
from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from . import setup_integration
from .const import TEST_DEVICE, TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_coordinator_stale_device(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test coordinator data update removes stale Alexa devices."""
entity_id_0 = "binary_sensor.echo_test_connectivity"
entity_id_1 = "binary_sensor.echo_test_2_connectivity"
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_SERIAL_NUMBER: TEST_DEVICE,
"echo_test_2_serial_number_2": AmazonDevice(
account_name="Echo Test 2",
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
device_family="mine",
device_type="echo",
device_owner_customer_id="amazon_ower_id",
device_cluster_members=["echo_test_2_serial_number_2"],
online=True,
serial_number="echo_test_2_serial_number_2",
software_version="echo_test_2_software_version",
do_not_disturb=False,
response_style=None,
bluetooth_state=True,
entity_id="11111111-2222-3333-4444-555555555555",
appliance_id="G1234567890123456789012345678A",
sensors={
"temperature": AmazonDeviceSensor(
name="temperature", value="22.5", scale="CELSIUS"
)
},
),
}
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id_0))
assert state.state == STATE_ON
assert (state := hass.states.get(entity_id_1))
assert state.state == STATE_ON
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_SERIAL_NUMBER: TEST_DEVICE,
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id_0))
assert state.state == STATE_ON
# Entity is removed
assert not hass.states.get(entity_id_1)