diff --git a/homeassistant/components/acaia/__init__.py b/homeassistant/components/acaia/__init__.py index dfdb4cb935d..f6eccc63b08 100644 --- a/homeassistant/components/acaia/__init__.py +++ b/homeassistant/components/acaia/__init__.py @@ -7,6 +7,7 @@ from .coordinator import AcaiaConfigEntry, AcaiaCoordinator PLATFORMS = [ Platform.BUTTON, + Platform.SENSOR, ] diff --git a/homeassistant/components/acaia/sensor.py b/homeassistant/components/acaia/sensor.py new file mode 100644 index 00000000000..49ee101b4a2 --- /dev/null +++ b/homeassistant/components/acaia/sensor.py @@ -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 diff --git a/tests/components/acaia/conftest.py b/tests/components/acaia/conftest.py index 1dc6ff31051..7e3c19c6c5a 100644 --- a/tests/components/acaia/conftest.py +++ b/tests/components/acaia/conftest.py @@ -74,7 +74,7 @@ def mock_scale() -> Generator[MagicMock]: scale.heartbeat_task = None scale.process_queue_task = None scale.device_state = AcaiaDeviceState( - battery_level=42, units=AcaiaUnitOfMass.GRAMS + battery_level=42, units=AcaiaUnitOfMass.OUNCES ) scale.weight = 123.45 yield scale diff --git a/tests/components/acaia/snapshots/test_sensor.ambr b/tests/components/acaia/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..46995877b4f --- /dev/null +++ b/tests/components/acaia/snapshots/test_sensor.ambr @@ -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': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lunar_ddeeff_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.lunar_ddeeff_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42', + }) +# --- +# name: test_sensors[sensor.lunar_ddeeff_weight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lunar_ddeeff_weight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + }) +# --- +# name: test_sensors[sensor.lunar_ddeeff_weight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'LUNAR-DDEEFF Weight', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lunar_ddeeff_weight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123.45', + }) +# --- diff --git a/tests/components/acaia/test_sensor.py b/tests/components/acaia/test_sensor.py new file mode 100644 index 00000000000..2f5a851121c --- /dev/null +++ b/tests/components/acaia/test_sensor.py @@ -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"