Skip to content

Commit 6a482b1

Browse files
authored
Remove stale devices for Alexa Devices (#151909)
1 parent 6f00f8a commit 6a482b1

File tree

5 files changed

+132
-27
lines changed

5 files changed

+132
-27
lines changed

homeassistant/components/alexa_devices/coordinator.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
1515
from homeassistant.core import HomeAssistant
1616
from homeassistant.exceptions import ConfigEntryAuthFailed
17+
from homeassistant.helpers import device_registry as dr
1718
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
1819

1920
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
@@ -48,12 +49,13 @@ def __init__(
4849
entry.data[CONF_PASSWORD],
4950
entry.data[CONF_LOGIN_DATA],
5051
)
52+
self.previous_devices: set[str] = set()
5153

5254
async def _async_update_data(self) -> dict[str, AmazonDevice]:
5355
"""Update device data."""
5456
try:
5557
await self.api.login_mode_stored_data()
56-
return await self.api.get_devices_data()
58+
data = await self.api.get_devices_data()
5759
except CannotConnect as err:
5860
raise UpdateFailed(
5961
translation_domain=DOMAIN,
@@ -72,3 +74,31 @@ async def _async_update_data(self) -> dict[str, AmazonDevice]:
7274
translation_key="invalid_auth",
7375
translation_placeholders={"error": repr(err)},
7476
) from err
77+
else:
78+
current_devices = set(data.keys())
79+
if stale_devices := self.previous_devices - current_devices:
80+
await self._async_remove_device_stale(stale_devices)
81+
82+
self.previous_devices = current_devices
83+
return data
84+
85+
async def _async_remove_device_stale(
86+
self,
87+
stale_devices: set[str],
88+
) -> None:
89+
"""Remove stale device."""
90+
device_registry = dr.async_get(self.hass)
91+
92+
for serial_num in stale_devices:
93+
_LOGGER.debug(
94+
"Detected change in devices: serial %s removed",
95+
serial_num,
96+
)
97+
device = device_registry.async_get_device(
98+
identifiers={(DOMAIN, serial_num)}
99+
)
100+
if device:
101+
device_registry.async_update_device(
102+
device_id=device.id,
103+
remove_config_entry_id=self.config_entry.entry_id,
104+
)

homeassistant/components/alexa_devices/quality_scale.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ rules:
6464
repair-issues:
6565
status: exempt
6666
comment: no known use cases for repair issues or flows, yet
67-
stale-devices:
68-
status: todo
69-
comment: automate the cleanup process
67+
stale-devices: done
7068

7169
# Platinum
7270
async-dependency: done

tests/components/alexa_devices/conftest.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Alexa Devices tests configuration."""
22

33
from collections.abc import Generator
4+
from copy import deepcopy
45
from unittest.mock import AsyncMock, patch
56

6-
from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
77
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL
88
import pytest
99

@@ -14,7 +14,7 @@
1414
)
1515
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
1616

17-
from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
17+
from .const import TEST_DEVICE, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
1818

1919
from tests.common import MockConfigEntry
2020

@@ -47,27 +47,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
4747
"customer_info": {"user_id": TEST_USERNAME},
4848
}
4949
client.get_devices_data.return_value = {
50-
TEST_SERIAL_NUMBER: AmazonDevice(
51-
account_name="Echo Test",
52-
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
53-
device_family="mine",
54-
device_type="echo",
55-
device_owner_customer_id="amazon_ower_id",
56-
device_cluster_members=[TEST_SERIAL_NUMBER],
57-
online=True,
58-
serial_number=TEST_SERIAL_NUMBER,
59-
software_version="echo_test_software_version",
60-
do_not_disturb=False,
61-
response_style=None,
62-
bluetooth_state=True,
63-
entity_id="11111111-2222-3333-4444-555555555555",
64-
appliance_id="G1234567890123456789012345678A",
65-
sensors={
66-
"temperature": AmazonDeviceSensor(
67-
name="temperature", value="22.5", scale="CELSIUS"
68-
)
69-
},
70-
)
50+
TEST_SERIAL_NUMBER: deepcopy(TEST_DEVICE)
7151
}
7252
client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get(
7353
device.device_type
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
"""Alexa Devices tests const."""
22

3+
from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
4+
35
TEST_CODE = "023123"
46
TEST_PASSWORD = "fake_password"
57
TEST_SERIAL_NUMBER = "echo_test_serial_number"
68
TEST_USERNAME = "[email protected]"
79

810
TEST_DEVICE_ID = "echo_test_device_id"
11+
12+
TEST_DEVICE = AmazonDevice(
13+
account_name="Echo Test",
14+
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
15+
device_family="mine",
16+
device_type="echo",
17+
device_owner_customer_id="amazon_ower_id",
18+
device_cluster_members=[TEST_SERIAL_NUMBER],
19+
online=True,
20+
serial_number=TEST_SERIAL_NUMBER,
21+
software_version="echo_test_software_version",
22+
do_not_disturb=False,
23+
response_style=None,
24+
bluetooth_state=True,
25+
entity_id="11111111-2222-3333-4444-555555555555",
26+
appliance_id="G1234567890123456789012345678A",
27+
sensors={
28+
"temperature": AmazonDeviceSensor(
29+
name="temperature", value="22.5", scale="CELSIUS"
30+
)
31+
},
32+
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Tests for the Alexa Devices coordinator."""
2+
3+
from unittest.mock import AsyncMock
4+
5+
from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
6+
from freezegun.api import FrozenDateTimeFactory
7+
8+
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
9+
from homeassistant.const import STATE_ON
10+
from homeassistant.core import HomeAssistant
11+
12+
from . import setup_integration
13+
from .const import TEST_DEVICE, TEST_SERIAL_NUMBER
14+
15+
from tests.common import MockConfigEntry, async_fire_time_changed
16+
17+
18+
async def test_coordinator_stale_device(
19+
hass: HomeAssistant,
20+
freezer: FrozenDateTimeFactory,
21+
mock_amazon_devices_client: AsyncMock,
22+
mock_config_entry: MockConfigEntry,
23+
) -> None:
24+
"""Test coordinator data update removes stale Alexa devices."""
25+
26+
entity_id_0 = "binary_sensor.echo_test_connectivity"
27+
entity_id_1 = "binary_sensor.echo_test_2_connectivity"
28+
29+
mock_amazon_devices_client.get_devices_data.return_value = {
30+
TEST_SERIAL_NUMBER: TEST_DEVICE,
31+
"echo_test_2_serial_number_2": AmazonDevice(
32+
account_name="Echo Test 2",
33+
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
34+
device_family="mine",
35+
device_type="echo",
36+
device_owner_customer_id="amazon_ower_id",
37+
device_cluster_members=["echo_test_2_serial_number_2"],
38+
online=True,
39+
serial_number="echo_test_2_serial_number_2",
40+
software_version="echo_test_2_software_version",
41+
do_not_disturb=False,
42+
response_style=None,
43+
bluetooth_state=True,
44+
entity_id="11111111-2222-3333-4444-555555555555",
45+
appliance_id="G1234567890123456789012345678A",
46+
sensors={
47+
"temperature": AmazonDeviceSensor(
48+
name="temperature", value="22.5", scale="CELSIUS"
49+
)
50+
},
51+
),
52+
}
53+
54+
await setup_integration(hass, mock_config_entry)
55+
56+
assert (state := hass.states.get(entity_id_0))
57+
assert state.state == STATE_ON
58+
assert (state := hass.states.get(entity_id_1))
59+
assert state.state == STATE_ON
60+
61+
mock_amazon_devices_client.get_devices_data.return_value = {
62+
TEST_SERIAL_NUMBER: TEST_DEVICE,
63+
}
64+
65+
freezer.tick(SCAN_INTERVAL)
66+
async_fire_time_changed(hass)
67+
await hass.async_block_till_done()
68+
69+
assert (state := hass.states.get(entity_id_0))
70+
assert state.state == STATE_ON
71+
72+
# Entity is removed
73+
assert not hass.states.get(entity_id_1)

0 commit comments

Comments
 (0)