diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index b871f2eb23a..51a939391a8 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -40,6 +40,7 @@ PLATFORMS = [ Platform.CALENDAR, Platform.NUMBER, Platform.SELECT, + Platform.SENSOR, Platform.SWITCH, Platform.UPDATE, ] diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index 7f22be34d3c..2964f48ecbd 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -72,6 +72,14 @@ } } }, + "sensor": { + "coffee_boiler_ready_time": { + "default": "mdi:av-timer" + }, + "steam_boiler_ready_time": { + "default": "mdi:av-timer" + } + }, "switch": { "main": { "default": "mdi:power", diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py new file mode 100644 index 00000000000..17f11534483 --- /dev/null +++ b/homeassistant/components/lamarzocco/sensor.py @@ -0,0 +1,115 @@ +"""Sensor platform for La Marzocco espresso machines.""" + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime +from typing import cast + +from pylamarzocco.const import ModelName, WidgetType +from pylamarzocco.models import ( + BaseWidgetOutput, + CoffeeBoiler, + SteamBoilerLevel, + SteamBoilerTemperature, +) + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import LaMarzoccoConfigEntry +from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class LaMarzoccoSensorEntityDescription( + LaMarzoccoEntityDescription, + SensorEntityDescription, +): + """Description of a La Marzocco sensor.""" + + value_fn: Callable[ + [dict[WidgetType, BaseWidgetOutput]], StateType | datetime | None + ] + + +ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( + LaMarzoccoSensorEntityDescription( + key="coffee_boiler_ready_time", + translation_key="coffee_boiler_ready_time", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=( + lambda config: cast( + CoffeeBoiler, config[WidgetType.CM_COFFEE_BOILER] + ).ready_start_time + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), + LaMarzoccoSensorEntityDescription( + key="steam_boiler_ready_time", + translation_key="steam_boiler_ready_time", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=( + lambda config: cast( + SteamBoilerLevel, config[WidgetType.CM_STEAM_BOILER_LEVEL] + ).ready_start_time + ), + entity_category=EntityCategory.DIAGNOSTIC, + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in (ModelName.LINEA_MICRA, ModelName.LINEA_MINI_R) + ), + ), + LaMarzoccoSensorEntityDescription( + key="steam_boiler_ready_time", + translation_key="steam_boiler_ready_time", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=( + lambda config: cast( + SteamBoilerTemperature, config[WidgetType.CM_STEAM_BOILER_TEMPERATURE] + ).ready_start_time + ), + entity_category=EntityCategory.DIAGNOSTIC, + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in (ModelName.GS3_AV, ModelName.GS3_MP, ModelName.LINEA_MINI) + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LaMarzoccoConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up sensor entities.""" + coordinator = entry.runtime_data.config_coordinator + + async_add_entities( + LaMarzoccoSensorEntity(coordinator, description) + for description in ENTITIES + if description.supported_fn(coordinator) + ) + + +class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity): + """Sensor representing espresso machine water reservoir status.""" + + entity_description: LaMarzoccoSensorEntityDescription + + @property + def native_value(self) -> StateType | datetime | None: + """Return value of the sensor.""" + return self.entity_description.value_fn( + self.coordinator.device.dashboard.config + ) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 43f3a14db6f..7a77b8ad72c 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -140,6 +140,14 @@ } } }, + "sensor": { + "coffee_boiler_ready_time": { + "name": "Coffee boiler ready time" + }, + "steam_boiler_ready_time": { + "name": "Steam boiler ready time" + } + }, "switch": { "auto_on_off": { "name": "Auto on/off ({id})" diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..311e7416b1c --- /dev/null +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_sensors[sensor.gs012345_coffee_boiler_ready_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_coffee_boiler_ready_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Coffee boiler ready time', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'coffee_boiler_ready_time', + 'unique_id': 'GS012345_coffee_boiler_ready_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.gs012345_coffee_boiler_ready_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'GS012345 Coffee boiler ready time', + }), + 'context': , + 'entity_id': 'sensor.gs012345_coffee_boiler_ready_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.gs012345_steam_boiler_ready_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_steam_boiler_ready_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Steam boiler ready time', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'steam_boiler_ready_time', + 'unique_id': 'GS012345_steam_boiler_ready_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.gs012345_steam_boiler_ready_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'GS012345 Steam boiler ready time', + }), + 'context': , + 'entity_id': 'sensor.gs012345_steam_boiler_ready_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/lamarzocco/test_sensor.py b/tests/components/lamarzocco/test_sensor.py new file mode 100644 index 00000000000..0b050dd7788 --- /dev/null +++ b/tests/components/lamarzocco/test_sensor.py @@ -0,0 +1,52 @@ +"""Tests for La Marzocco sensors.""" + +from unittest.mock import MagicMock, patch + +from pylamarzocco.const import ModelName +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import async_init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensors( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the La Marzocco sensors.""" + + with patch("homeassistant.components.lamarzocco.PLATFORMS", [Platform.SENSOR]): + await async_init_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "device_fixture", + [ModelName.GS3_AV, ModelName.GS3_MP, ModelName.LINEA_MINI, ModelName.LINEA_MICRA], +) +async def test_steam_ready_entity_for_all_machines( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the La Marzocco steam ready sensor for all machines.""" + + serial_number = mock_lamarzocco.serial_number + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get(f"sensor.{serial_number}_steam_boiler_ready_time") + + assert state + + entry = entity_registry.async_get(state.entity_id) + assert entry