diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 51a939391a8..1d77dbc2f1a 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -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 diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index a83f7e6ab76..cfe570efb53 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -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()) diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index 8ea764b4d18..a319384d7fd 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -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": { diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index 9c1214835fa..5dc0eb3dbef 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -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 + ) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 9b153b5707e..6383e931c22 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -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" } diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 8f7c089a75b..c7530d464db 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -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, diff --git a/tests/components/lamarzocco/fixtures/statistics.json b/tests/components/lamarzocco/fixtures/statistics.json new file mode 100644 index 00000000000..0c333457d69 --- /dev/null +++ b/tests/components/lamarzocco/fixtures/statistics.json @@ -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 + } + } + ] +} diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index f23771b77b4..46abb93dd2e 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -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': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_total_coffees_made', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'unit_of_measurement': 'coffees', + }), + 'context': , + 'entity_id': 'sensor.gs012345_total_coffees_made', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1620', + }) +# --- +# name: test_sensors[sensor.gs012345_total_flushes_done-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_total_flushes_done', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'unit_of_measurement': 'flushes', + }), + 'context': , + 'entity_id': 'sensor.gs012345_total_flushes_done', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1366', + }) +# ---