Add statistic entities to lamarzocco (#143415)

* Bump pylamarzocco to 2.0.0b2

* Add statistic entities to lamarzocco

* add icons

* Update coordinator.py

* update uom

* Update homeassistant/components/lamarzocco/sensor.py

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* revert cups

* remove unnecessary call (for now)

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
This commit is contained in:
Josef Zweck 2025-04-29 20:49:26 +02:00 committed by GitHub
parent 05f393560f
commit d657298791
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 364 additions and 2 deletions

View File

@ -32,6 +32,7 @@ from .coordinator import (
LaMarzoccoRuntimeData,
LaMarzoccoScheduleUpdateCoordinator,
LaMarzoccoSettingsUpdateCoordinator,
LaMarzoccoStatisticsUpdateCoordinator,
)
PLATFORMS = [
@ -140,12 +141,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
LaMarzoccoConfigUpdateCoordinator(hass, entry, device),
LaMarzoccoSettingsUpdateCoordinator(hass, entry, device),
LaMarzoccoScheduleUpdateCoordinator(hass, entry, device),
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
)
await asyncio.gather(
coordinators.config_coordinator.async_config_entry_first_refresh(),
coordinators.settings_coordinator.async_config_entry_first_refresh(),
coordinators.schedule_coordinator.async_config_entry_first_refresh(),
coordinators.statistics_coordinator.async_config_entry_first_refresh(),
)
entry.runtime_data = coordinators

View File

@ -22,6 +22,7 @@ from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=15)
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
_LOGGER = logging.getLogger(__name__)
@ -32,6 +33,7 @@ class LaMarzoccoRuntimeData:
config_coordinator: LaMarzoccoConfigUpdateCoordinator
settings_coordinator: LaMarzoccoSettingsUpdateCoordinator
schedule_coordinator: LaMarzoccoScheduleUpdateCoordinator
statistics_coordinator: LaMarzoccoStatisticsUpdateCoordinator
type LaMarzoccoConfigEntry = ConfigEntry[LaMarzoccoRuntimeData]
@ -130,3 +132,14 @@ class LaMarzoccoScheduleUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Fetch data from API endpoint."""
await self.device.get_schedule()
_LOGGER.debug("Current schedule: %s", self.device.schedule.to_dict())
class LaMarzoccoStatisticsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco statistics."""
_default_update_interval = STATISTICS_UPDATE_INTERVAL
async def _internal_async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.device.get_coffee_and_flush_counter()
_LOGGER.debug("Current statistics: %s", self.device.statistics.to_dict())

View File

@ -81,6 +81,12 @@
},
"steam_boiler_ready_time": {
"default": "mdi:av-timer"
},
"total_coffees_made": {
"default": "mdi:coffee"
},
"total_flushes_done": {
"default": "mdi:water-pump"
}
},
"switch": {

View File

@ -9,6 +9,7 @@ from pylamarzocco.const import ModelName, WidgetType
from pylamarzocco.models import (
BackFlush,
BaseWidgetOutput,
CoffeeAndFlushCounter,
CoffeeBoiler,
SteamBoilerLevel,
SteamBoilerTemperature,
@ -18,6 +19,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
@ -98,6 +100,31 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
),
)
STATISTIC_ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
LaMarzoccoSensorEntityDescription(
key="drink_stats_coffee",
translation_key="total_coffees_made",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=(
lambda statistics: cast(
CoffeeAndFlushCounter, statistics[WidgetType.COFFEE_AND_FLUSH_COUNTER]
).total_coffee
),
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoSensorEntityDescription(
key="drink_stats_flushing",
translation_key="total_flushes_done",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=(
lambda statistics: cast(
CoffeeAndFlushCounter, statistics[WidgetType.COFFEE_AND_FLUSH_COUNTER]
).total_flush
),
entity_category=EntityCategory.DIAGNOSTIC,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@ -107,15 +134,21 @@ async def async_setup_entry(
"""Set up sensor entities."""
coordinator = entry.runtime_data.config_coordinator
async_add_entities(
entities = [
LaMarzoccoSensorEntity(coordinator, description)
for description in ENTITIES
if description.supported_fn(coordinator)
]
entities.extend(
LaMarzoccoStatisticSensorEntity(coordinator, description)
for description in STATISTIC_ENTITIES
if description.supported_fn(coordinator)
)
async_add_entities(entities)
class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity):
"""Sensor representing espresso machine water reservoir status."""
"""Sensor for La Marzocco."""
entity_description: LaMarzoccoSensorEntityDescription
@ -125,3 +158,14 @@ class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity):
return self.entity_description.value_fn(
self.coordinator.device.dashboard.config
)
class LaMarzoccoStatisticSensorEntity(LaMarzoccoSensorEntity):
"""Sensor for La Marzocco statistics."""
@property
def native_value(self) -> StateType | datetime | None:
"""Return the value of the sensor."""
return self.entity_description.value_fn(
self.coordinator.device.statistics.widgets
)

View File

@ -147,6 +147,14 @@
"steam_boiler_ready_time": {
"name": "Steam boiler ready time"
},
"total_coffees_made": {
"name": "Total coffees made",
"unit_of_measurement": "coffees"
},
"total_flushes_done": {
"name": "Total flushes done",
"unit_of_measurement": "flushes"
},
"last_cleaning_time": {
"name": "Last cleaning time"
}

View File

@ -10,6 +10,7 @@ from pylamarzocco.models import (
ThingDashboardConfig,
ThingSchedulingSettings,
ThingSettings,
ThingStatistics,
)
import pytest
@ -91,6 +92,7 @@ def mock_lamarzocco(device_fixture: ModelName) -> Generator[MagicMock]:
config = load_json_object_fixture("config_gs3.json", DOMAIN)
schedule = load_json_object_fixture("schedule.json", DOMAIN)
settings = load_json_object_fixture("settings.json", DOMAIN)
statistics = load_json_object_fixture("statistics.json", DOMAIN)
with (
patch(
@ -104,6 +106,7 @@ def mock_lamarzocco(device_fixture: ModelName) -> Generator[MagicMock]:
machine_mock.dashboard = ThingDashboardConfig.from_dict(config)
machine_mock.schedule = ThingSchedulingSettings.from_dict(schedule)
machine_mock.settings = ThingSettings.from_dict(settings)
machine_mock.statistics = ThingStatistics.from_dict(statistics)
machine_mock.dashboard.model_name = device_fixture
machine_mock.to_dict.return_value = {
"serial_number": machine_mock.serial_number,

View File

@ -0,0 +1,183 @@
{
"serialNumber": "MR123456",
"type": "CoffeeMachine",
"name": "MR123456",
"location": null,
"modelCode": "LINEAMICRA",
"modelName": "LINEA MICRA",
"connected": true,
"connectionDate": 1742526019892,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": false,
"coffeeStation": null,
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png",
"bleAuthToken": null,
"firmwares": null,
"selectedWidgetCodes": ["COFFEE_AND_FLUSH_TREND", "LAST_COFFEE"],
"allWidgetCodes": ["LAST_COFFEE", "COFFEE_AND_FLUSH_TREND"],
"selectedWidgets": [
{
"code": "COFFEE_AND_FLUSH_TREND",
"index": 1,
"output": {
"days": 7,
"timezone": "Europe/Berlin",
"coffees": [
{ "timestamp": 1741993200000, "value": 2 },
{ "timestamp": 1742079600000, "value": 2 },
{ "timestamp": 1742166000000, "value": 2 },
{ "timestamp": 1742252400000, "value": 2 },
{ "timestamp": 1742338800000, "value": 4 },
{ "timestamp": 1742425200000, "value": 3 },
{ "timestamp": 1742511600000, "value": 1 }
],
"flushes": [
{ "timestamp": 1741993200000, "value": 1 },
{ "timestamp": 1742079600000, "value": 1 },
{ "timestamp": 1742166000000, "value": 0 },
{ "timestamp": 1742252400000, "value": 0 },
{ "timestamp": 1742338800000, "value": 4 },
{ "timestamp": 1742425200000, "value": 2 },
{ "timestamp": 1742511600000, "value": 1 }
]
}
},
{
"code": "LAST_COFFEE",
"index": 1,
"output": {
"lastCoffees": [
{
"time": 1742535679203,
"extractionSeconds": 30.44,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742489827722,
"extractionSeconds": 10.8,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742448826919,
"extractionSeconds": 12.457,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742448702812,
"extractionSeconds": 23.504,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742396255439,
"extractionSeconds": 16.031,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742396142154,
"extractionSeconds": 27.413,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742364379903,
"extractionSeconds": 14.182,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742364235304,
"extractionSeconds": 23.228,
"doseMode": "Continuous",
"doseIndex": "Continuous",
"doseValue": null,
"doseValueNumerator": null
},
{
"time": 1742277098548,
"extractionSeconds": 12.98,
"doseMode": "PulsesType",
"doseIndex": "DoseA",
"doseValue": 0,
"doseValueNumerator": null
},
{
"time": 1742277006774,
"extractionSeconds": 26.99,
"doseMode": "PulsesType",
"doseIndex": "DoseA",
"doseValue": 0,
"doseValueNumerator": null
},
{
"time": 1742190219197,
"extractionSeconds": 11.069,
"doseMode": "PulsesType",
"doseIndex": "DoseA",
"doseValue": 0,
"doseValueNumerator": null
},
{
"time": 1742190123385,
"extractionSeconds": 35.472,
"doseMode": "PulsesType",
"doseIndex": "DoseA",
"doseValue": 0,
"doseValueNumerator": null
},
{
"time": 1742106228119,
"extractionSeconds": 11.494,
"doseMode": "PulsesType",
"doseIndex": "DoseA",
"doseValue": 0,
"doseValueNumerator": null
},
{
"time": 1742106147433,
"extractionSeconds": 39.915,
"doseMode": "PulsesType",
"doseIndex": "DoseA",
"doseValue": 0,
"doseValueNumerator": null
},
{
"time": 1742017890205,
"extractionSeconds": 13.891,
"doseMode": "PulsesType",
"doseIndex": "DoseA",
"doseValue": 0,
"doseValueNumerator": null
}
]
}
},
{
"code": "COFFEE_AND_FLUSH_COUNTER",
"index": 1,
"output": {
"totalCoffee": 1620,
"totalFlush": 1366
}
}
]
}

View File

@ -143,3 +143,105 @@
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.gs012345_total_coffees_made-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_total_coffees_made',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total coffees made',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_coffees_made',
'unique_id': 'GS012345_drink_stats_coffee',
'unit_of_measurement': 'coffees',
})
# ---
# name: test_sensors[sensor.gs012345_total_coffees_made-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Total coffees made',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'coffees',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_total_coffees_made',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1620',
})
# ---
# name: test_sensors[sensor.gs012345_total_flushes_done-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_total_flushes_done',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total flushes done',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_flushes_done',
'unique_id': 'GS012345_drink_stats_flushing',
'unit_of_measurement': 'flushes',
})
# ---
# name: test_sensors[sensor.gs012345_total_flushes_done-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Total flushes done',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'flushes',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_total_flushes_done',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1366',
})
# ---