From 7c7f18b5011255e10009dc69b7217ce60b4bbf94 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 19 Apr 2025 12:29:08 +0200 Subject: [PATCH] Add preinfusion settings to lamarzocco (#143159) --- .../components/lamarzocco/icons.json | 9 + homeassistant/components/lamarzocco/number.py | 121 +++++++++++- .../components/lamarzocco/strings.json | 9 + .../lamarzocco/snapshots/test_number.ambr | 174 ++++++++++++++++++ tests/components/lamarzocco/test_number.py | 141 +++++++++++++- 5 files changed, 451 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index 7a42bcd6028..7f22be34d3c 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -36,6 +36,15 @@ }, "smart_standby_time": { "default": "mdi:timer" + }, + "preinfusion_time": { + "default": "mdi:water" + }, + "prebrew_time_on": { + "default": "mdi:water" + }, + "prebrew_time_off": { + "default": "mdi:water-off" } }, "select": { diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 6b849f1783d..81a03b4d6ee 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -5,9 +5,9 @@ from dataclasses import dataclass from typing import Any, cast from pylamarzocco import LaMarzoccoMachine -from pylamarzocco.const import WidgetType +from pylamarzocco.const import ModelName, PreExtractionMode, WidgetType from pylamarzocco.exceptions import RequestNotSuccessful -from pylamarzocco.models import CoffeeBoiler +from pylamarzocco.models import CoffeeBoiler, PreBrewing from homeassistant.components.number import ( NumberDeviceClass, @@ -77,6 +77,123 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( ), native_value_fn=lambda machine: machine.schedule.smart_wake_up_sleep.smart_stand_by_minutes, ), + LaMarzoccoNumberEntityDescription( + key="preinfusion_off", + translation_key="preinfusion_time", + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + native_step=PRECISION_TENTHS, + native_min_value=0, + native_max_value=10, + entity_category=EntityCategory.CONFIG, + set_value_fn=( + lambda machine, value: machine.set_pre_extraction_times( + seconds_on=0, + seconds_off=float(value), + ) + ), + native_value_fn=( + lambda machine: cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ) + .times.pre_infusion[0] + .seconds.seconds_out + ), + available_fn=( + lambda machine: cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ).mode + is PreExtractionMode.PREINFUSION + ), + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in ( + ModelName.LINEA_MICRA, + ModelName.LINEA_MINI, + ModelName.LINEA_MINI_R, + ) + ), + ), + LaMarzoccoNumberEntityDescription( + key="prebrew_on", + translation_key="prebrew_time_on", + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + native_step=PRECISION_TENTHS, + native_min_value=0, + native_max_value=10, + entity_category=EntityCategory.CONFIG, + set_value_fn=( + lambda machine, value: machine.set_pre_extraction_times( + seconds_on=float(value), + seconds_off=cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ) + .times.pre_brewing[0] + .seconds.seconds_out, + ) + ), + native_value_fn=( + lambda machine: cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ) + .times.pre_brewing[0] + .seconds.seconds_in + ), + available_fn=lambda machine: cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ).mode + is PreExtractionMode.PREBREWING, + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in ( + ModelName.LINEA_MICRA, + ModelName.LINEA_MINI, + ModelName.LINEA_MINI_R, + ) + ), + ), + LaMarzoccoNumberEntityDescription( + key="prebrew_off", + translation_key="prebrew_time_off", + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + native_step=PRECISION_TENTHS, + native_min_value=0, + native_max_value=10, + entity_category=EntityCategory.CONFIG, + set_value_fn=( + lambda machine, value: machine.set_pre_extraction_times( + seconds_on=cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ) + .times.pre_brewing[0] + .seconds.seconds_in, + seconds_off=float(value), + ) + ), + native_value_fn=( + lambda machine: cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ) + .times.pre_brewing[0] + .seconds.seconds_out + ), + available_fn=( + lambda machine: cast( + PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + ).mode + is PreExtractionMode.PREBREWING + ), + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in ( + ModelName.LINEA_MICRA, + ModelName.LINEA_MINI, + ModelName.LINEA_MINI_R, + ) + ), + ), ) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index ad58c4e0ee3..43f3a14db6f 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -104,6 +104,15 @@ }, "smart_standby_time": { "name": "Smart standby time" + }, + "preinfusion_time": { + "name": "Preinfusion time" + }, + "prebrew_time_on": { + "name": "Prebrew on time" + }, + "prebrew_time_off": { + "name": "Prebrew off time" } }, "select": { diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index d9a644567d5..8f59ce4a6fa 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -115,3 +115,177 @@ 'unit_of_measurement': , }) # --- +# name: test_prebrew_off[Linea Micra] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'MR012345 Prebrew off time', + 'max': 10, + 'min': 0, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mr012345_prebrew_off_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_prebrew_off[Linea Micra].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': 0, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mr012345_prebrew_off_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': 'Prebrew off time', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'prebrew_time_off', + 'unique_id': 'MR012345_prebrew_off', + 'unit_of_measurement': , + }) +# --- +# name: test_prebrew_on[Linea Micra] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'MR012345 Prebrew on time', + 'max': 10, + 'min': 0, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mr012345_prebrew_on_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_prebrew_on[Linea Micra].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': 0, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mr012345_prebrew_on_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': 'Prebrew on time', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'prebrew_time_on', + 'unique_id': 'MR012345_prebrew_on', + 'unit_of_measurement': , + }) +# --- +# name: test_preinfusion[Linea Micra] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'MR012345 Preinfusion time', + 'max': 10, + 'min': 0, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mr012345_preinfusion_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.0', + }) +# --- +# name: test_preinfusion[Linea Micra].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': 0, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mr012345_preinfusion_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': 'Preinfusion time', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'preinfusion_time', + 'unique_id': 'MR012345_preinfusion_off', + 'unit_of_measurement': , + }) +# --- diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index d70b99c7f57..e4be04f4ce4 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -3,7 +3,12 @@ from typing import Any from unittest.mock import MagicMock -from pylamarzocco.const import SmartStandByType +from pylamarzocco.const import ( + ModelName, + PreExtractionMode, + SmartStandByType, + WidgetType, +) from pylamarzocco.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -85,6 +90,140 @@ async def test_general_numbers( mock_func.assert_called_once_with(**kwargs) +@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MICRA]) +async def test_preinfusion( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test preinfusion number.""" + + await async_init_integration(hass, mock_config_entry) + serial_number = mock_lamarzocco.serial_number + entity_id = f"number.{serial_number}_preinfusion_time" + + state = hass.states.get(entity_id) + + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot + + # service call + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 5.3, + }, + blocking=True, + ) + + mock_lamarzocco.set_pre_extraction_times.assert_called_once_with( + seconds_off=5.3, + seconds_on=0, + ) + + +@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MICRA]) +async def test_prebrew_on( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test prebrew on number.""" + + mock_lamarzocco.dashboard.config[ + WidgetType.CM_PRE_BREWING + ].mode = PreExtractionMode.PREBREWING + + await async_init_integration(hass, mock_config_entry) + serial_number = mock_lamarzocco.serial_number + entity_id = f"number.{serial_number}_prebrew_on_time" + + state = hass.states.get(entity_id) + + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot + + # service call + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 5.3, + }, + blocking=True, + ) + + mock_lamarzocco.set_pre_extraction_times.assert_called_once_with( + seconds_on=5.3, + seconds_off=mock_lamarzocco.dashboard.config[WidgetType.CM_PRE_BREWING] + .times.pre_brewing[0] + .seconds.seconds_out, + ) + + +@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MICRA]) +async def test_prebrew_off( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test prebrew off number.""" + mock_lamarzocco.dashboard.config[ + WidgetType.CM_PRE_BREWING + ].mode = PreExtractionMode.PREBREWING + + await async_init_integration(hass, mock_config_entry) + serial_number = mock_lamarzocco.serial_number + entity_id = f"number.{serial_number}_prebrew_off_time" + + state = hass.states.get(entity_id) + + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot + + # service call + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 7, + }, + blocking=True, + ) + + mock_lamarzocco.set_pre_extraction_times.assert_called_once_with( + seconds_off=7, + seconds_on=mock_lamarzocco.dashboard.config[WidgetType.CM_PRE_BREWING] + .times.pre_brewing[0] + .seconds.seconds_in, + ) + + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_number_error( hass: HomeAssistant,