Add sensor platform to acaia (#130614)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Josef Zweck 2024-11-18 14:52:49 +01:00 committed by GitHub
parent 1ac0b006b2
commit 4c816f54bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 303 additions and 1 deletions

View File

@ -7,6 +7,7 @@ from .coordinator import AcaiaConfigEntry, AcaiaCoordinator
PLATFORMS = [ PLATFORMS = [
Platform.BUTTON, Platform.BUTTON,
Platform.SENSOR,
] ]

View File

@ -0,0 +1,135 @@
"""Sensor platform for Acaia."""
from collections.abc import Callable
from dataclasses import dataclass
from aioacaia.acaiascale import AcaiaDeviceState, AcaiaScale
from aioacaia.const import UnitMass as AcaiaUnitOfMass
from homeassistant.components.sensor import (
RestoreSensor,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorExtraStoredData,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, UnitOfMass
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import AcaiaConfigEntry
from .entity import AcaiaEntity
@dataclass(kw_only=True, frozen=True)
class AcaiaSensorEntityDescription(SensorEntityDescription):
"""Description for Acaia sensor entities."""
value_fn: Callable[[AcaiaScale], int | float | None]
@dataclass(kw_only=True, frozen=True)
class AcaiaDynamicUnitSensorEntityDescription(AcaiaSensorEntityDescription):
"""Description for Acaia sensor entities with dynamic units."""
unit_fn: Callable[[AcaiaDeviceState], str] | None = None
SENSORS: tuple[AcaiaSensorEntityDescription, ...] = (
AcaiaDynamicUnitSensorEntityDescription(
key="weight",
device_class=SensorDeviceClass.WEIGHT,
native_unit_of_measurement=UnitOfMass.GRAMS,
state_class=SensorStateClass.MEASUREMENT,
unit_fn=lambda data: (
UnitOfMass.OUNCES
if data.units == AcaiaUnitOfMass.OUNCES
else UnitOfMass.GRAMS
),
value_fn=lambda scale: scale.weight,
),
)
RESTORE_SENSORS: tuple[AcaiaSensorEntityDescription, ...] = (
AcaiaSensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda scale: (
scale.device_state.battery_level if scale.device_state else None
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AcaiaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors."""
coordinator = entry.runtime_data
entities: list[SensorEntity] = [
AcaiaSensor(coordinator, entity_description) for entity_description in SENSORS
]
entities.extend(
AcaiaRestoreSensor(coordinator, entity_description)
for entity_description in RESTORE_SENSORS
)
async_add_entities(entities)
class AcaiaSensor(AcaiaEntity, SensorEntity):
"""Representation of an Acaia sensor."""
entity_description: AcaiaDynamicUnitSensorEntityDescription
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity."""
if (
self._scale.device_state is not None
and self.entity_description.unit_fn is not None
):
return self.entity_description.unit_fn(self._scale.device_state)
return self.entity_description.native_unit_of_measurement
@property
def native_value(self) -> int | float | None:
"""Return the state of the entity."""
return self.entity_description.value_fn(self._scale)
class AcaiaRestoreSensor(AcaiaEntity, RestoreSensor):
"""Representation of an Acaia sensor with restore capabilities."""
entity_description: AcaiaSensorEntityDescription
_restored_data: SensorExtraStoredData | None = None
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
self._restored_data = await self.async_get_last_sensor_data()
if self._restored_data is not None:
self._attr_native_value = self._restored_data.native_value
self._attr_native_unit_of_measurement = (
self._restored_data.native_unit_of_measurement
)
if self._scale.device_state is not None:
self._attr_native_value = self.entity_description.value_fn(self._scale)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if self._scale.device_state is not None:
self._attr_native_value = self.entity_description.value_fn(self._scale)
self._async_write_ha_state()
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available or self._restored_data is not None

View File

@ -74,7 +74,7 @@ def mock_scale() -> Generator[MagicMock]:
scale.heartbeat_task = None scale.heartbeat_task = None
scale.process_queue_task = None scale.process_queue_task = None
scale.device_state = AcaiaDeviceState( scale.device_state = AcaiaDeviceState(
battery_level=42, units=AcaiaUnitOfMass.GRAMS battery_level=42, units=AcaiaUnitOfMass.OUNCES
) )
scale.weight = 123.45 scale.weight = 123.45
yield scale yield scale

View File

@ -0,0 +1,103 @@
# serializer version: 1
# name: test_sensors[sensor.lunar_ddeeff_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.lunar_ddeeff_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'acaia',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[sensor.lunar_ddeeff_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'LUNAR-DDEEFF Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.lunar_ddeeff_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '42',
})
# ---
# name: test_sensors[sensor.lunar_ddeeff_weight-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.lunar_ddeeff_weight',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.WEIGHT: 'weight'>,
'original_icon': None,
'original_name': 'Weight',
'platform': 'acaia',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_weight',
'unit_of_measurement': <UnitOfMass.OUNCES: 'oz'>,
})
# ---
# name: test_sensors[sensor.lunar_ddeeff_weight-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'weight',
'friendly_name': 'LUNAR-DDEEFF Weight',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfMass.OUNCES: 'oz'>,
}),
'context': <ANY>,
'entity_id': 'sensor.lunar_ddeeff_weight',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123.45',
})
# ---

View File

@ -0,0 +1,63 @@
"""Test sensors for acaia integration."""
from unittest.mock import MagicMock, patch
from syrupy import SnapshotAssertion
from homeassistant.const import PERCENTAGE, Platform
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import (
MockConfigEntry,
mock_restore_cache_with_extra_data,
snapshot_platform,
)
async def test_sensors(
hass: HomeAssistant,
mock_scale: MagicMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the Acaia sensors."""
with patch("homeassistant.components.acaia.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_restore_state(
hass: HomeAssistant,
mock_scale: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test battery sensor restore state."""
mock_scale.device_state = None
entity_id = "sensor.lunar_ddeeff_battery"
mock_restore_cache_with_extra_data(
hass,
(
(
State(
entity_id,
"1",
),
{
"native_value": 65,
"native_unit_of_measurement": PERCENTAGE,
},
),
),
)
await setup_integration(hass, mock_config_entry)
state = hass.states.get(entity_id)
assert state
assert state.state == "65"