From 478bf643bfb79707934d8538bedf30d85f4546cb Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:22:37 +0100 Subject: [PATCH] Add smart standby functionality to lamarzocco (#129333) Co-authored-by: Joost Lekkerkerker --- .../components/lamarzocco/icons.json | 16 +++++ homeassistant/components/lamarzocco/number.py | 16 +++++ homeassistant/components/lamarzocco/select.py | 23 ++++++- .../components/lamarzocco/strings.json | 13 ++++ homeassistant/components/lamarzocco/switch.py | 11 ++++ .../lamarzocco/snapshots/test_number.ambr | 61 ++++++++++++++++++- .../lamarzocco/snapshots/test_select.ambr | 55 +++++++++++++++++ .../lamarzocco/snapshots/test_switch.ambr | 54 ++++++++++++++-- tests/components/lamarzocco/test_number.py | 38 +++++++++--- tests/components/lamarzocco/test_select.py | 36 ++++++++++- tests/components/lamarzocco/test_switch.py | 18 +++--- 11 files changed, 316 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index bc7d621d91d..860da12ddd9 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -43,6 +43,9 @@ "preinfusion_off": { "default": "mdi:water" }, + "smart_standby_time": { + "default": "mdi:timer" + }, "steam_temp": { "default": "mdi:thermometer-water" }, @@ -51,6 +54,13 @@ } }, "select": { + "smart_standby_mode": { + "default": "mdi:power", + "state": { + "poweron": "mdi:power", + "lastbrewing": "mdi:coffee" + } + }, "steam_temp_select": { "default": "mdi:thermometer", "state": { @@ -100,6 +110,12 @@ "off": "mdi:alarm-off" } }, + "smart_standby_enabled": { + "state": { + "on": "mdi:sleep", + "off": "mdi:sleep-off" + } + }, "steam_boiler": { "default": "mdi:water-boiler", "state": { diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index e607d856193..97e4c0b252a 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -109,6 +109,22 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( MachineModel.GS3_MP, ), ), + LaMarzoccoNumberEntityDescription( + key="smart_standby_time", + translation_key="smart_standby_time", + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + native_step=10, + native_min_value=10, + native_max_value=240, + entity_category=EntityCategory.CONFIG, + set_value_fn=lambda machine, value: machine.set_smart_standby( + enabled=machine.config.smart_standby.enabled, + mode=machine.config.smart_standby.mode, + minutes=int(value), + ), + native_value_fn=lambda config: config.smart_standby.minutes, + ), ) diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index 7a410796285..62ad17c0df4 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -4,7 +4,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from lmcloud.const import MachineModel, PrebrewMode, SteamLevel +from lmcloud.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel from lmcloud.exceptions import RequestNotSuccessful from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig @@ -43,6 +43,13 @@ PREBREW_MODE_LM_TO_HA = { PrebrewMode.PREINFUSION: "preinfusion", } +STANDBY_MODE_HA_TO_LM = { + "power_on": SmartStandbyMode.POWER_ON, + "last_brewing": SmartStandbyMode.LAST_BREWING, +} + +STANDBY_MODE_LM_TO_HA = {value: key for key, value in STANDBY_MODE_HA_TO_LM.items()} + @dataclass(frozen=True, kw_only=True) class LaMarzoccoSelectEntityDescription( @@ -83,6 +90,20 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( MachineModel.LINEA_MINI, ), ), + LaMarzoccoSelectEntityDescription( + key="smart_standby_mode", + translation_key="smart_standby_mode", + entity_category=EntityCategory.CONFIG, + options=["power_on", "last_brewing"], + select_option_fn=lambda machine, option: machine.set_smart_standby( + enabled=machine.config.smart_standby.enabled, + mode=STANDBY_MODE_HA_TO_LM[option], + minutes=machine.config.smart_standby.minutes, + ), + current_option_fn=lambda config: STANDBY_MODE_LM_TO_HA[ + config.smart_standby.mode + ], + ), ) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 6188b9d3d67..ec3b00a7474 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -116,6 +116,9 @@ "preinfusion_off_key": { "name": "Preinfusion time Key {key}" }, + "smart_standby_time": { + "name": "Smart standby time" + }, "steam_temp": { "name": "Steam target temperature" }, @@ -132,6 +135,13 @@ "preinfusion": "Preinfusion" } }, + "smart_standby_mode": { + "name": "Smart standby mode", + "state": { + "last_brewing": "Last brewing", + "power_on": "Power on" + } + }, "steam_temp_select": { "name": "Steam level", "state": { @@ -162,6 +172,9 @@ "auto_on_off": { "name": "Auto on/off ({id})" }, + "smart_standby_enabled": { + "name": "Smart standby enabled" + }, "steam_boiler": { "name": "Steam boiler" } diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index dda0f0f1d58..ccb050d2081 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -46,6 +46,17 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( control_fn=lambda machine, state: machine.set_steam(state), is_on_fn=lambda config: config.boilers[BoilerType.STEAM].enabled, ), + LaMarzoccoSwitchEntityDescription( + key="smart_standby_enabled", + translation_key="smart_standby_enabled", + entity_category=EntityCategory.CONFIG, + control_fn=lambda machine, state: machine.set_smart_standby( + enabled=state, + mode=machine.config.smart_standby.mode, + minutes=machine.config.smart_standby.minutes, + ), + is_on_fn=lambda config: config.smart_standby.enabled, + ), ) diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index 8265e7d7646..bd54ce2c0b4 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_coffee_boiler +# name: test_general_numbers[coffee_target_temperature-94-set_temp-kwargs0] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -18,7 +18,7 @@ 'state': '95', }) # --- -# name: test_coffee_boiler.1 +# name: test_general_numbers[coffee_target_temperature-94-set_temp-kwargs0].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -56,6 +56,63 @@ 'unit_of_measurement': , }) # --- +# name: test_general_numbers[smart_standby_time-23-set_smart_standby-kwargs1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'GS01234 Smart standby time', + 'max': 240, + 'min': 10, + 'mode': , + 'step': 10, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.gs01234_smart_standby_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_general_numbers[smart_standby_time-23-set_smart_standby-kwargs1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 240, + 'min': 10, + 'mode': , + 'step': 10, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.gs01234_smart_standby_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': 'Smart standby time', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_standby_time', + 'unique_id': 'GS01234_smart_standby_time', + 'unit_of_measurement': , + }) +# --- # name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 AV] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr index be56af2b092..4f08b0898b1 100644 --- a/tests/components/lamarzocco/snapshots/test_select.ambr +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -170,6 +170,61 @@ 'unit_of_measurement': None, }) # --- +# name: test_smart_standby_mode + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS01234 Smart standby mode', + 'options': list([ + 'power_on', + 'last_brewing', + ]), + }), + 'context': , + 'entity_id': 'select.gs01234_smart_standby_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'last_brewing', + }) +# --- +# name: test_smart_standby_mode.1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_on', + 'last_brewing', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.gs01234_smart_standby_mode', + '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': 'Smart standby mode', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_standby_mode', + 'unique_id': 'GS01234_smart_standby_mode', + 'unit_of_measurement': None, + }) +# --- # name: test_steam_boiler_level[Micra] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 5d020cbee5f..2a368a56467 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -123,7 +123,7 @@ 'via_device_id': None, }) # --- -# name: test_switches[-set_power] +# name: test_switches[-set_power-kwargs0] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234', @@ -136,7 +136,7 @@ 'state': 'on', }) # --- -# name: test_switches[-set_power].1 +# name: test_switches[-set_power-kwargs0].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -169,7 +169,53 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[_steam_boiler-set_steam] +# name: test_switches[_smart_standby_enabled-set_smart_standby-kwargs2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS01234 Smart standby enabled', + }), + 'context': , + 'entity_id': 'switch.gs01234_smart_standby_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[_smart_standby_enabled-set_smart_standby-kwargs2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.gs01234_smart_standby_enabled', + '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': 'Smart standby enabled', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_standby_enabled', + 'unique_id': 'GS01234_smart_standby_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[_steam_boiler-set_steam-kwargs1] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Steam boiler', @@ -182,7 +228,7 @@ 'state': 'on', }) # --- -# name: test_switches[_steam_boiler-set_steam].1 +# name: test_switches[_steam_boiler-set_steam-kwargs1].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 70d8efa5de7..352271f26cf 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -1,5 +1,6 @@ """Tests for the La Marzocco number entities.""" +from typing import Any from unittest.mock import MagicMock from lmcloud.const import ( @@ -28,20 +29,41 @@ from . import async_init_integration from tests.common import MockConfigEntry -async def test_coffee_boiler( +@pytest.mark.parametrize( + ("entity_name", "value", "func_name", "kwargs"), + [ + ( + "coffee_target_temperature", + 94, + "set_temp", + {"boiler": BoilerType.COFFEE, "temperature": 94}, + ), + ( + "smart_standby_time", + 23, + "set_smart_standby", + {"enabled": True, "mode": "LastBrewing", "minutes": 23}, + ), + ], +) +async def test_general_numbers( hass: HomeAssistant, mock_lamarzocco: MagicMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, + entity_name: str, + value: float, + func_name: str, + kwargs: dict[str, Any], ) -> None: - """Test the La Marzocco coffee temperature Number.""" + """Test the numbers available to all machines.""" await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number - state = hass.states.get(f"number.{serial_number}_coffee_target_temperature") + state = hass.states.get(f"number.{serial_number}_{entity_name}") assert state assert state == snapshot @@ -59,16 +81,14 @@ async def test_coffee_boiler( NUMBER_DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature", - ATTR_VALUE: 94, + ATTR_ENTITY_ID: f"number.{serial_number}_{entity_name}", + ATTR_VALUE: value, }, blocking=True, ) - assert len(mock_lamarzocco.set_temp.mock_calls) == 1 - mock_lamarzocco.set_temp.assert_called_once_with( - boiler=BoilerType.COFFEE, temperature=94 - ) + mock_func = getattr(mock_lamarzocco, func_name) + mock_func.assert_called_once_with(**kwargs) @pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV, MachineModel.GS3_MP]) diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index 862898428f5..415954d30be 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from lmcloud.const import MachineModel, PrebrewMode, SteamLevel +from lmcloud.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -121,6 +121,40 @@ async def test_pre_brew_infusion_select_none( assert state is None +async def test_smart_standby_mode( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_lamarzocco: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test the La Marzocco Smart Standby mode select.""" + + serial_number = mock_lamarzocco.serial_number + + state = hass.states.get(f"select.{serial_number}_smart_standby_mode") + + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.{serial_number}_smart_standby_mode", + ATTR_OPTION: "power_on", + }, + blocking=True, + ) + + mock_lamarzocco.set_smart_standby.assert_called_once_with( + enabled=True, mode=SmartStandbyMode.POWER_ON, minutes=10 + ) + + async def test_select_errors( hass: HomeAssistant, mock_lamarzocco: MagicMock, diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index a09d254ffe9..802ab59148e 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -1,5 +1,6 @@ """Tests for La Marzocco switches.""" +from typing import Any from unittest.mock import MagicMock from lmcloud.exceptions import RequestNotSuccessful @@ -25,15 +26,15 @@ from tests.common import MockConfigEntry ( "entity_name", "method_name", + "kwargs", ), [ + ("", "set_power", {}), + ("_steam_boiler", "set_steam", {}), ( - "", - "set_power", - ), - ( - "_steam_boiler", - "set_steam", + "_smart_standby_enabled", + "set_smart_standby", + {"mode": "LastBrewing", "minutes": 10}, ), ], ) @@ -45,6 +46,7 @@ async def test_switches( snapshot: SnapshotAssertion, entity_name: str, method_name: str, + kwargs: dict[str, Any], ) -> None: """Test the La Marzocco switches.""" await async_init_integration(hass, mock_config_entry) @@ -71,7 +73,7 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 1 - control_fn.assert_called_once_with(False) + control_fn.assert_called_once_with(enabled=False, **kwargs) await hass.services.async_call( SWITCH_DOMAIN, @@ -83,7 +85,7 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 2 - control_fn.assert_called_with(True) + control_fn.assert_called_with(enabled=True, **kwargs) async def test_device(