diff --git a/homeassistant/components/tedee/__init__.py b/homeassistant/components/tedee/__init__.py index 2ba6131d00c..7940d594d71 100644 --- a/homeassistant/components/tedee/__init__.py +++ b/homeassistant/components/tedee/__init__.py @@ -10,6 +10,7 @@ from .coordinator import TedeeApiCoordinator PLATFORMS = [ Platform.LOCK, + Platform.SENSOR, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tedee/entity.py b/homeassistant/components/tedee/entity.py index fa80ffcb24c..86baa81b452 100644 --- a/homeassistant/components/tedee/entity.py +++ b/homeassistant/components/tedee/entity.py @@ -4,6 +4,7 @@ from pytedee_async.lock import TedeeLock from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -38,3 +39,19 @@ class TedeeEntity(CoordinatorEntity[TedeeApiCoordinator]): """Handle updated data from the coordinator.""" self._lock = self.coordinator.data[self._lock.lock_id] super()._handle_coordinator_update() + + +class TedeeDescriptionEntity(TedeeEntity): + """Base class for Tedee device entities.""" + + entity_description: EntityDescription + + def __init__( + self, + lock: TedeeLock, + coordinator: TedeeApiCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize Tedee device entity.""" + super().__init__(lock, coordinator, entity_description.key) + self.entity_description = entity_description diff --git a/homeassistant/components/tedee/sensor.py b/homeassistant/components/tedee/sensor.py new file mode 100644 index 00000000000..9eb61e624c7 --- /dev/null +++ b/homeassistant/components/tedee/sensor.py @@ -0,0 +1,74 @@ +"""Tedee sensor entities.""" +from collections.abc import Callable +from dataclasses import dataclass + +from pytedee_async import TedeeLock + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import TedeeDescriptionEntity + + +@dataclass(frozen=True, kw_only=True) +class TedeeSensorEntityDescription(SensorEntityDescription): + """Describes Tedee sensor entity.""" + + value_fn: Callable[[TedeeLock], float | None] + + +ENTITIES: tuple[TedeeSensorEntityDescription, ...] = ( + TedeeSensorEntityDescription( + key="battery_sensor", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda lock: lock.battery_level, + ), + TedeeSensorEntityDescription( + key="pullspring_duration", + translation_key="pullspring_duration", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + state_class=SensorStateClass.TOTAL, + icon="mdi:timer-lock-open", + value_fn=lambda lock: lock.duration_pullspring, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Tedee sensor entity.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + for entity_description in ENTITIES: + async_add_entities( + [ + TedeeSensorEntity(lock, coordinator, entity_description) + for lock in coordinator.data.values() + ] + ) + + +class TedeeSensorEntity(TedeeDescriptionEntity, SensorEntity): + """Tedee sensor entity.""" + + entity_description: TedeeSensorEntityDescription + + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._lock) diff --git a/homeassistant/components/tedee/strings.json b/homeassistant/components/tedee/strings.json index e9286d894aa..7a1df7d3875 100644 --- a/homeassistant/components/tedee/strings.json +++ b/homeassistant/components/tedee/strings.json @@ -21,5 +21,12 @@ "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "entity": { + "sensor": { + "pullspring_duration": { + "name": "Pullspring duration" + } + } } } diff --git a/tests/components/tedee/snapshots/test_sensor.ambr b/tests/components/tedee/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..a74ee38bff0 --- /dev/null +++ b/tests/components/tedee/snapshots/test_sensor.ambr @@ -0,0 +1,98 @@ +# serializer version: 1 +# name: test_sensors[entry-battery] + 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.lock_1a2b_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tedee', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12345-battery_sensor', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[entry-pullspring_duration] + 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.lock_1a2b_pullspring_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:timer-lock-open', + 'original_name': 'Pullspring duration', + 'platform': 'tedee', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pullspring_duration', + 'unique_id': '12345-pullspring_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[state-battery] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Lock-1A2B Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.lock_1a2b_battery', + 'last_changed': , + 'last_updated': , + 'state': '70', + }) +# --- +# name: test_sensors[state-pullspring_duration] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Lock-1A2B Pullspring duration', + 'icon': 'mdi:timer-lock-open', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lock_1a2b_pullspring_duration', + 'last_changed': , + 'last_updated': , + 'state': '2', + }) +# --- diff --git a/tests/components/tedee/test_sensor.py b/tests/components/tedee/test_sensor.py new file mode 100644 index 00000000000..95cde20a82f --- /dev/null +++ b/tests/components/tedee/test_sensor.py @@ -0,0 +1,36 @@ +"""Tests for the Tedee Sensors.""" + + +from unittest.mock import MagicMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +pytestmark = pytest.mark.usefixtures("init_integration") + + +SENSORS = ( + "battery", + "pullspring_duration", +) + + +async def test_sensors( + hass: HomeAssistant, + mock_tedee: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test tedee sensors.""" + for key in SENSORS: + state = hass.states.get(f"sensor.lock_1a2b_{key}") + assert state + assert state == snapshot(name=f"state-{key}") + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot(name=f"entry-{key}")