mirror of
https://github.com/home-assistant/core.git
synced 2026-04-29 02:13:44 +02:00
Remove stale devices for Alexa Devices (#151909)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user