Add dynamic devices management for Alexa Devices (#151975)

This commit is contained in:
Simone Chemelli
2025-09-24 19:00:35 +02:00
committed by GitHub
parent ed5f5d4b33
commit 14d42e43bf
15 changed files with 164 additions and 86 deletions
@@ -94,12 +94,24 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in coordinator.data
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
)
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in new_devices
if sensor_desc.is_supported(
coordinator.data[serial_num], sensor_desc.key
)
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
@@ -57,13 +57,23 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in NOTIFY
for serial_num in coordinator.data
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in NOTIFY
for serial_num in new_devices
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
@@ -53,7 +53,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -62,12 +62,22 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in SENSORS
for serial_num in coordinator.data
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
)
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in SENSORS
for serial_num in new_devices
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonSensorEntity(AmazonEntity, SensorEntity):
@@ -48,12 +48,22 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
for switch_desc in SWITCHES
for serial_num in coordinator.data
if switch_desc.subkey in coordinator.data[serial_num].capabilities
)
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
for switch_desc in SWITCHES
for serial_num in new_devices
if switch_desc.subkey in coordinator.data[serial_num].capabilities
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
+2 -2
View File
@@ -14,7 +14,7 @@ from homeassistant.components.alexa_devices.const import (
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import TEST_DEVICE, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -48,7 +48,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
CONF_SITE: "https://www.amazon.com",
}
client.get_devices_data.return_value = {
TEST_SERIAL_NUMBER: deepcopy(TEST_DEVICE)
TEST_DEVICE_1_SN: deepcopy(TEST_DEVICE_1)
}
client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get(
device.device_type
+29 -6
View File
@@ -4,20 +4,19 @@ 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(
TEST_DEVICE_1_SN = "echo_test_serial_number"
TEST_DEVICE_1_ID = "echo_test_device_id"
TEST_DEVICE_1 = 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],
device_cluster_members=[TEST_DEVICE_1_SN],
online=True,
serial_number=TEST_SERIAL_NUMBER,
serial_number=TEST_DEVICE_1_SN,
software_version="echo_test_software_version",
do_not_disturb=False,
response_style=None,
@@ -30,3 +29,27 @@ TEST_DEVICE = AmazonDevice(
)
},
)
TEST_DEVICE_2_SN = "echo_test_2_serial_number"
TEST_DEVICE_2_ID = "echo_test_2_device_id"
TEST_DEVICE_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=[TEST_DEVICE_2_SN],
online=True,
serial_number=TEST_DEVICE_2_SN,
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"
)
},
)
@@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
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
@@ -83,7 +83,7 @@ async def test_offline_device(
entity_id = "binary_sensor.echo_test_connectivity"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = False
await setup_integration(hass, mock_config_entry)
@@ -92,7 +92,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = True
freezer.tick(SCAN_INTERVAL)
@@ -101,3 +101,39 @@ async def test_offline_device(
assert (state := hass.states.get(entity_id))
assert state.state != STATE_UNAVAILABLE
async def test_dynamic_device(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test device added dynamically."""
entity_id_1 = "binary_sensor.echo_test_connectivity"
entity_id_2 = "binary_sensor.echo_test_2_connectivity"
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: TEST_DEVICE_1,
}
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id_1))
assert state.state == STATE_ON
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: TEST_DEVICE_1,
TEST_DEVICE_2_SN: TEST_DEVICE_2,
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id_1))
assert state.state == STATE_ON
assert (state := hass.states.get(entity_id_2))
assert state.state == STATE_ON
@@ -2,7 +2,6 @@
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
@@ -10,7 +9,7 @@ from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from . import setup_integration
from .const import TEST_DEVICE, TEST_SERIAL_NUMBER
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
@@ -27,28 +26,8 @@ async def test_coordinator_stale_device(
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"
)
},
),
TEST_DEVICE_1_SN: TEST_DEVICE_1,
TEST_DEVICE_2_SN: TEST_DEVICE_2,
}
await setup_integration(hass, mock_config_entry)
@@ -59,7 +38,7 @@ async def test_coordinator_stale_device(
assert state.state == STATE_ON
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_SERIAL_NUMBER: TEST_DEVICE,
TEST_DEVICE_1_SN: TEST_DEVICE_1,
}
freezer.tick(SCAN_INTERVAL)
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from .const import TEST_DEVICE_1_SN
from tests.common import MockConfigEntry
from tests.components.diagnostics import (
@@ -54,9 +54,7 @@ async def test_device_diagnostics(
"""Test Amazon device diagnostics."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_DEVICE_1_SN)})
assert device, repr(device_registry.devices)
assert await get_diagnostics_for_device(
+2 -2
View File
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from .const import TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -31,7 +31,7 @@ async def test_device_info(
"""Test device registry integration."""
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
)
assert device_entry is not None
assert device_entry == snapshot
@@ -18,7 +18,7 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from .const import TEST_DEVICE_1_SN
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -83,7 +83,7 @@ async def test_offline_device(
entity_id = "notify.echo_test_announce"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = False
await setup_integration(hass, mock_config_entry)
@@ -92,7 +92,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = True
freezer.tick(SCAN_INTERVAL)
@@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from .const import TEST_DEVICE_1_SN
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -83,7 +83,7 @@ async def test_offline_device(
entity_id = "sensor.echo_test_temperature"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = False
await setup_integration(hass, mock_config_entry)
@@ -92,7 +92,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = True
freezer.tick(SCAN_INTERVAL)
@@ -133,7 +133,7 @@ async def test_unit_of_measurement(
entity_id = f"sensor.echo_test_{sensor}"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].sensors = {sensor: AmazonDeviceSensor(name=sensor, value=api_value, scale=scale)}
await setup_integration(hass, mock_config_entry)
@@ -19,7 +19,7 @@ from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_DEVICE_ID, TEST_SERIAL_NUMBER
from .const import TEST_DEVICE_1_ID, TEST_DEVICE_1_SN
from tests.common import MockConfigEntry, mock_device_registry
@@ -49,7 +49,7 @@ async def test_send_sound_service(
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
)
assert device_entry
@@ -79,7 +79,7 @@ async def test_send_text_service(
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
)
assert device_entry
@@ -108,7 +108,7 @@ async def test_send_text_service(
),
(
"wrong_sound_name",
TEST_DEVICE_ID,
TEST_DEVICE_1_ID,
"invalid_sound_value",
{
"sound": "wrong_sound_name",
@@ -128,7 +128,7 @@ async def test_invalid_parameters(
"""Test invalid service parameters."""
device_entry = dr.DeviceEntry(
id=TEST_DEVICE_ID, identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
id=TEST_DEVICE_1_ID, identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
)
mock_device_registry(
hass,
@@ -164,7 +164,7 @@ async def test_config_entry_not_loaded(
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
)
assert device_entry
@@ -23,7 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .conftest import TEST_SERIAL_NUMBER
from .conftest import TEST_DEVICE_1_SN
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -67,7 +67,7 @@ async def test_switch_dnd(
assert mock_amazon_devices_client.set_do_not_disturb.call_count == 1
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].do_not_disturb = True
freezer.tick(SCAN_INTERVAL)
@@ -85,7 +85,7 @@ async def test_switch_dnd(
)
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].do_not_disturb = False
freezer.tick(SCAN_INTERVAL)
@@ -108,7 +108,7 @@ async def test_offline_device(
entity_id = "switch.echo_test_do_not_disturb"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = False
await setup_integration(hass, mock_config_entry)
@@ -117,7 +117,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
TEST_DEVICE_1_SN
].online = True
freezer.tick(SCAN_INTERVAL)