Add select platform to La Marzocco integration (#108222)

* add select

* change check, icons

* fix docstrings, use []
pull/108231/head
Josef Zweck 2024-01-17 13:21:33 +01:00 committed by GitHub
parent 1b2a4d2bf3
commit 74d53a4231
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 457 additions and 0 deletions

View File

@ -9,6 +9,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@ -0,0 +1,92 @@
"""Select platform for La Marzocco espresso machines."""
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from lmcloud import LMCloud as LaMarzoccoClient
from lmcloud.const import LaMarzoccoModel
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
@dataclass(frozen=True, kw_only=True)
class LaMarzoccoSelectEntityDescription(
LaMarzoccoEntityDescription,
SelectEntityDescription,
):
"""Description of a La Marzocco select entity."""
current_option_fn: Callable[[LaMarzoccoClient], str]
select_option_fn: Callable[
[LaMarzoccoUpdateCoordinator, str], Coroutine[Any, Any, bool]
]
ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
LaMarzoccoSelectEntityDescription(
key="steam_temp_select",
translation_key="steam_temp_select",
icon="mdi:water-thermometer",
options=["1", "2", "3"],
select_option_fn=lambda coordinator, option: coordinator.lm.set_steam_level(
int(option)
),
current_option_fn=lambda lm: lm.current_status["steam_level_set"],
supported_fn=lambda coordinator: coordinator.lm.model_name
== LaMarzoccoModel.LINEA_MICRA,
),
LaMarzoccoSelectEntityDescription(
key="prebrew_infusion_select",
translation_key="prebrew_infusion_select",
icon="mdi:water-plus",
options=["disabled", "prebrew", "preinfusion"],
select_option_fn=lambda coordinator,
option: coordinator.lm.select_pre_brew_infusion_mode(option.capitalize()),
current_option_fn=lambda lm: lm.pre_brew_infusion_mode.lower(),
supported_fn=lambda coordinator: coordinator.lm.model_name
in (
LaMarzoccoModel.GS3_AV,
LaMarzoccoModel.LINEA_MICRA,
LaMarzoccoModel.LINEA_MINI,
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up select entities."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
LaMarzoccoSelectEntity(coordinator, description)
for description in ENTITIES
if description.supported_fn(coordinator)
)
class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity):
"""La Marzocco select entity."""
entity_description: LaMarzoccoSelectEntityDescription
@property
def current_option(self) -> str:
"""Return the current selected option."""
return str(self.entity_description.current_option_fn(self.coordinator.lm))
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.entity_description.select_option_fn(self.coordinator, option)
self.async_write_ha_state()

View File

@ -51,6 +51,24 @@
"name": "Water tank empty"
}
},
"select": {
"prebrew_infusion_select": {
"name": "Prebrew/-infusion mode",
"state": {
"disabled": "Disabled",
"prebrew": "Prebrew",
"preinfusion": "Preinfusion"
}
},
"steam_temp_select": {
"name": "Steam level",
"state": {
"1": "1",
"2": "2",
"3": "3"
}
}
},
"sensor": {
"current_temp_coffee": {
"name": "Current coffee temperature"

View File

@ -8,6 +8,7 @@
"steam_boiler_enable": true,
"steam_temp": 113,
"steam_set_temp": 128,
"steam_level_set": 3,
"coffee_temp": 93,
"coffee_set_temp": 95,
"water_reservoir_contact": true,

View File

@ -0,0 +1,221 @@
# serializer version: 1
# name: test_pre_brew_infusion_select[GS3 AV]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Prebrew/-infusion mode',
'icon': 'mdi:water-plus',
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'context': <ANY>,
'entity_id': 'select.gs01234_prebrew_infusion_mode',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_pre_brew_infusion_select[GS3 AV].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.gs01234_prebrew_infusion_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:water-plus',
'original_name': 'Prebrew/-infusion mode',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'prebrew_infusion_select',
'unique_id': 'GS01234_prebrew_infusion_select',
'unit_of_measurement': None,
})
# ---
# name: test_pre_brew_infusion_select[Linea Mini]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'LM01234 Prebrew/-infusion mode',
'icon': 'mdi:water-plus',
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'context': <ANY>,
'entity_id': 'select.lm01234_prebrew_infusion_mode',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_pre_brew_infusion_select[Linea Mini].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.lm01234_prebrew_infusion_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:water-plus',
'original_name': 'Prebrew/-infusion mode',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'prebrew_infusion_select',
'unique_id': 'LM01234_prebrew_infusion_select',
'unit_of_measurement': None,
})
# ---
# name: test_pre_brew_infusion_select[Micra]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MR01234 Prebrew/-infusion mode',
'icon': 'mdi:water-plus',
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'context': <ANY>,
'entity_id': 'select.mr01234_prebrew_infusion_mode',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_pre_brew_infusion_select[Micra].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.mr01234_prebrew_infusion_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:water-plus',
'original_name': 'Prebrew/-infusion mode',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'prebrew_infusion_select',
'unique_id': 'MR01234_prebrew_infusion_select',
'unit_of_measurement': None,
})
# ---
# name: test_steam_boiler_level[Micra]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MR01234 Steam level',
'icon': 'mdi:water-thermometer',
'options': list([
'1',
'2',
'3',
]),
}),
'context': <ANY>,
'entity_id': 'select.mr01234_steam_level',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
# name: test_steam_boiler_level[Micra].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'1',
'2',
'3',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.mr01234_steam_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:water-thermometer',
'original_name': 'Steam level',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'steam_temp_select',
'unique_id': 'MR01234_steam_temp_select',
'unit_of_measurement': None,
})
# ---

View File

@ -0,0 +1,124 @@
"""Tests for the La Marzocco select entities."""
from unittest.mock import MagicMock
from lmcloud.const import LaMarzoccoModel
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
pytestmark = pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.LINEA_MICRA])
async def test_steam_boiler_level(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_lamarzocco: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the La Marzocco Steam Level Select (only for Micra Models)."""
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"select.{serial_number}_steam_level")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
# on/off service calls
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: f"select.{serial_number}_steam_level",
ATTR_OPTION: "1",
},
blocking=True,
)
assert len(mock_lamarzocco.set_steam_level.mock_calls) == 1
mock_lamarzocco.set_steam_level.assert_called_once_with(level=1)
@pytest.mark.parametrize(
"device_fixture",
[LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP, LaMarzoccoModel.LINEA_MINI],
)
async def test_steam_boiler_level_none(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
) -> None:
"""Ensure the La Marzocco Steam Level Select is not created for non-Micra models."""
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"select.{serial_number}_steam_level")
assert state is None
@pytest.mark.parametrize(
"device_fixture",
[LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.GS3_AV, LaMarzoccoModel.LINEA_MINI],
)
async def test_pre_brew_infusion_select(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_lamarzocco: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Prebrew/-infusion select."""
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"select.{serial_number}_prebrew_infusion_mode")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
# on/off service calls
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: f"select.{serial_number}_prebrew_infusion_mode",
ATTR_OPTION: "preinfusion",
},
blocking=True,
)
assert len(mock_lamarzocco.select_pre_brew_infusion_mode.mock_calls) == 1
mock_lamarzocco.select_pre_brew_infusion_mode.assert_called_once_with(
mode="Preinfusion"
)
@pytest.mark.parametrize(
"device_fixture",
[LaMarzoccoModel.GS3_MP],
)
async def test_pre_brew_infusion_select_none(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
) -> None:
"""Ensure the La Marzocco Steam Level Select is not created for non-Micra models."""
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"select.{serial_number}_prebrew_infusion_mode")
assert state is None