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": {
"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": {

View File

@ -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,
),
)

View File

@ -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
],
),
)

View File

@ -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"
}

View File

@ -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,
),
)

View File

@ -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': <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]
StateSnapshot({
'attributes': ReadOnlyDict({

View File

@ -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': <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]
StateSnapshot({
'attributes': ReadOnlyDict({

View File

@ -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': <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({
'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({
}),

View File

@ -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])

View File

@ -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,

View File

@ -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(