Add sensor platform to Alexa Devices (#146469)

* Add sensor platform to Amazon Devices

* fix merge after rename

* fix requirements

* cleanup

* Revert "cleanup"

This reverts commit f34892da8a9cc1836870ceef8f8e48ca946b3ff6.

* tests

* move logic in sensor entity description

* update tests

* apply review comment

* apply review comments
This commit is contained in:
Simone Chemelli 2025-06-23 01:01:15 +03:00 committed by GitHub
parent 25968925e7
commit b47706f360
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 292 additions and 2 deletions

View File

@ -8,6 +8,7 @@ from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.NOTIFY, Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
] ]

View File

@ -0,0 +1,88 @@
"""Support for sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Final
from aioamazondevices.api import AmazonDevice
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AmazonSensorEntityDescription(SensorEntityDescription):
"""Amazon Devices sensor entity description."""
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
SENSORS: Final = (
AmazonSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device, _key: (
UnitOfTemperature.CELSIUS
if device.sensors[_key].scale == "CELSIUS"
else UnitOfTemperature.FAHRENHEIT
),
),
AmazonSensorEntityDescription(
key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Amazon Devices sensors based on a config 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
)
class AmazonSensorEntity(AmazonEntity, SensorEntity):
"""Sensor device."""
entity_description: AmazonSensorEntityDescription
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor."""
if self.entity_description.native_unit_of_measurement_fn:
return self.entity_description.native_unit_of_measurement_fn(
self.device, self.entity_description.key
)
return super().native_unit_of_measurement
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.device.sensors[self.entity_description.key].value

View File

@ -3,7 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aioamazondevices.api import AmazonDevice from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL from aioamazondevices.const import DEVICE_TYPE_TO_MODEL
import pytest import pytest
@ -58,7 +58,11 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
bluetooth_state=True, bluetooth_state=True,
entity_id="11111111-2222-3333-4444-555555555555", entity_id="11111111-2222-3333-4444-555555555555",
appliance_id="G1234567890123456789012345678A", appliance_id="G1234567890123456789012345678A",
sensors={}, sensors={
"temperature": AmazonDeviceSensor(
name="temperature", value="22.5", scale="CELSIUS"
)
},
) )
} }
client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get( client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get(

View File

@ -0,0 +1,54 @@
# serializer version: 1
# name: test_all_entities[sensor.echo_test_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.echo_test_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'alexa_devices',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'echo_test_serial_number-temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[sensor.echo_test_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Echo Test Temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.echo_test_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.5',
})
# ---

View File

@ -0,0 +1,143 @@
"""Tests for the Alexa Devices sensor platform."""
from typing import Any
from unittest.mock import AsyncMock, patch
from aioamazondevices.api import AmazonDeviceSensor
from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
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_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
"side_effect",
[
CannotConnect,
CannotRetrieveData,
CannotAuthenticate,
],
)
async def test_coordinator_data_update_fails(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
side_effect: Exception,
) -> None:
"""Test coordinator data update exceptions."""
entity_id = "sensor.echo_test_temperature"
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == "22.5"
mock_amazon_devices_client.get_devices_data.side_effect = side_effect
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
async def test_offline_device(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test offline device handling."""
entity_id = "sensor.echo_test_temperature"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id))
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize(
("sensor", "api_value", "scale", "state_value", "unit"),
[
(
"temperature",
"86",
"FAHRENHEIT",
"30.0", # State machine converts to °C
"°C", # State machine converts to °C
),
("temperature", "22.5", "CELSIUS", "22.5", "°C"),
("illuminance", "800", None, "800", "lx"),
],
)
async def test_unit_of_measurement(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
sensor: str,
api_value: Any,
scale: str | None,
state_value: Any,
unit: str | None,
) -> None:
"""Test sensor unit of measurement handling."""
entity_id = f"sensor.echo_test_{sensor}"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].sensors = {sensor: AmazonDeviceSensor(name=sensor, value=api_value, scale=scale)}
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == state_value
assert state.attributes["unit_of_measurement"] == unit