Add smart standby functionality to lamarzocco (#129333)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Josef Zweck 2024-10-29 13:22:37 +01:00 committed by GitHub
parent 7929895b11
commit 478bf643bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 316 additions and 25 deletions

View File

@ -43,6 +43,9 @@
"preinfusion_off": { "preinfusion_off": {
"default": "mdi:water" "default": "mdi:water"
}, },
"smart_standby_time": {
"default": "mdi:timer"
},
"steam_temp": { "steam_temp": {
"default": "mdi:thermometer-water" "default": "mdi:thermometer-water"
}, },
@ -51,6 +54,13 @@
} }
}, },
"select": { "select": {
"smart_standby_mode": {
"default": "mdi:power",
"state": {
"poweron": "mdi:power",
"lastbrewing": "mdi:coffee"
}
},
"steam_temp_select": { "steam_temp_select": {
"default": "mdi:thermometer", "default": "mdi:thermometer",
"state": { "state": {
@ -100,6 +110,12 @@
"off": "mdi:alarm-off" "off": "mdi:alarm-off"
} }
}, },
"smart_standby_enabled": {
"state": {
"on": "mdi:sleep",
"off": "mdi:sleep-off"
}
},
"steam_boiler": { "steam_boiler": {
"default": "mdi:water-boiler", "default": "mdi:water-boiler",
"state": { "state": {

View File

@ -109,6 +109,22 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
MachineModel.GS3_MP, 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,
),
) )

View File

@ -4,7 +4,7 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any 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.exceptions import RequestNotSuccessful
from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoMachineConfig from lmcloud.models import LaMarzoccoMachineConfig
@ -43,6 +43,13 @@ PREBREW_MODE_LM_TO_HA = {
PrebrewMode.PREINFUSION: "preinfusion", 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) @dataclass(frozen=True, kw_only=True)
class LaMarzoccoSelectEntityDescription( class LaMarzoccoSelectEntityDescription(
@ -83,6 +90,20 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
MachineModel.LINEA_MINI, 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
],
),
) )

View File

@ -116,6 +116,9 @@
"preinfusion_off_key": { "preinfusion_off_key": {
"name": "Preinfusion time Key {key}" "name": "Preinfusion time Key {key}"
}, },
"smart_standby_time": {
"name": "Smart standby time"
},
"steam_temp": { "steam_temp": {
"name": "Steam target temperature" "name": "Steam target temperature"
}, },
@ -132,6 +135,13 @@
"preinfusion": "Preinfusion" "preinfusion": "Preinfusion"
} }
}, },
"smart_standby_mode": {
"name": "Smart standby mode",
"state": {
"last_brewing": "Last brewing",
"power_on": "Power on"
}
},
"steam_temp_select": { "steam_temp_select": {
"name": "Steam level", "name": "Steam level",
"state": { "state": {
@ -162,6 +172,9 @@
"auto_on_off": { "auto_on_off": {
"name": "Auto on/off ({id})" "name": "Auto on/off ({id})"
}, },
"smart_standby_enabled": {
"name": "Smart standby enabled"
},
"steam_boiler": { "steam_boiler": {
"name": "Steam boiler" "name": "Steam boiler"
} }

View File

@ -46,6 +46,17 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
control_fn=lambda machine, state: machine.set_steam(state), control_fn=lambda machine, state: machine.set_steam(state),
is_on_fn=lambda config: config.boilers[BoilerType.STEAM].enabled, 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,
),
) )

View File

@ -1,5 +1,5 @@
# serializer version: 1 # serializer version: 1
# name: test_coffee_boiler # name: test_general_numbers[coffee_target_temperature-94-set_temp-kwargs0]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'temperature', 'device_class': 'temperature',
@ -18,7 +18,7 @@
'state': '95', 'state': '95',
}) })
# --- # ---
# name: test_coffee_boiler.1 # name: test_general_numbers[coffee_target_temperature-94-set_temp-kwargs0].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -56,6 +56,63 @@
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}) })
# --- # ---
# 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': <NumberMode.AUTO: 'auto'>,
'step': 10,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'number.gs01234_smart_standby_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <NumberMode.AUTO: 'auto'>,
'step': 10,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.gs01234_smart_standby_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'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': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 AV] # name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 AV]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({

View File

@ -170,6 +170,61 @@
'unit_of_measurement': None, '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': <ANY>,
'entity_id': 'select.gs01234_smart_standby_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.gs01234_smart_standby_mode',
'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': '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] # name: test_steam_boiler_level[Micra]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({

View File

@ -123,7 +123,7 @@
'via_device_id': None, 'via_device_id': None,
}) })
# --- # ---
# name: test_switches[-set_power] # name: test_switches[-set_power-kwargs0]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234', 'friendly_name': 'GS01234',
@ -136,7 +136,7 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_switches[-set_power].1 # name: test_switches[-set_power-kwargs0].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -169,7 +169,53 @@
'unit_of_measurement': None, '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': <ANY>,
'entity_id': 'switch.gs01234_smart_standby_enabled',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switches[_smart_standby_enabled-set_smart_standby-kwargs2].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.gs01234_smart_standby_enabled',
'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': '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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Steam boiler', 'friendly_name': 'GS01234 Steam boiler',
@ -182,7 +228,7 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_switches[_steam_boiler-set_steam].1 # name: test_switches[_steam_boiler-set_steam-kwargs1].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),

View File

@ -1,5 +1,6 @@
"""Tests for the La Marzocco number entities.""" """Tests for the La Marzocco number entities."""
from typing import Any
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.const import ( from lmcloud.const import (
@ -28,20 +29,41 @@ from . import async_init_integration
from tests.common import MockConfigEntry 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, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_name: str,
value: float,
func_name: str,
kwargs: dict[str, Any],
) -> None: ) -> None:
"""Test the La Marzocco coffee temperature Number.""" """Test the numbers available to all machines."""
await async_init_integration(hass, mock_config_entry) await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number 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
assert state == snapshot assert state == snapshot
@ -59,16 +81,14 @@ async def test_coffee_boiler(
NUMBER_DOMAIN, NUMBER_DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
{ {
ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature", ATTR_ENTITY_ID: f"number.{serial_number}_{entity_name}",
ATTR_VALUE: 94, ATTR_VALUE: value,
}, },
blocking=True, blocking=True,
) )
assert len(mock_lamarzocco.set_temp.mock_calls) == 1 mock_func = getattr(mock_lamarzocco, func_name)
mock_lamarzocco.set_temp.assert_called_once_with( mock_func.assert_called_once_with(**kwargs)
boiler=BoilerType.COFFEE, temperature=94
)
@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV, MachineModel.GS3_MP]) @pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV, MachineModel.GS3_MP])

View File

@ -2,7 +2,7 @@
from unittest.mock import MagicMock 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 from lmcloud.exceptions import RequestNotSuccessful
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -121,6 +121,40 @@ async def test_pre_brew_infusion_select_none(
assert state is 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( async def test_select_errors(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,

View File

@ -1,5 +1,6 @@
"""Tests for La Marzocco switches.""" """Tests for La Marzocco switches."""
from typing import Any
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.exceptions import RequestNotSuccessful from lmcloud.exceptions import RequestNotSuccessful
@ -25,15 +26,15 @@ from tests.common import MockConfigEntry
( (
"entity_name", "entity_name",
"method_name", "method_name",
"kwargs",
), ),
[ [
("", "set_power", {}),
("_steam_boiler", "set_steam", {}),
( (
"", "_smart_standby_enabled",
"set_power", "set_smart_standby",
), {"mode": "LastBrewing", "minutes": 10},
(
"_steam_boiler",
"set_steam",
), ),
], ],
) )
@ -45,6 +46,7 @@ async def test_switches(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_name: str, entity_name: str,
method_name: str, method_name: str,
kwargs: dict[str, Any],
) -> None: ) -> None:
"""Test the La Marzocco switches.""" """Test the La Marzocco switches."""
await async_init_integration(hass, mock_config_entry) await async_init_integration(hass, mock_config_entry)
@ -71,7 +73,7 @@ async def test_switches(
) )
assert len(control_fn.mock_calls) == 1 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( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
@ -83,7 +85,7 @@ async def test_switches(
) )
assert len(control_fn.mock_calls) == 2 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( async def test_device(