From 74d53a4231eb52fb2571f643615b6ae39a496a3a Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:21:33 +0100 Subject: [PATCH] Add select platform to La Marzocco integration (#108222) * add select * change check, icons * fix docstrings, use [] --- .../components/lamarzocco/__init__.py | 1 + homeassistant/components/lamarzocco/select.py | 92 ++++++++ .../components/lamarzocco/strings.json | 18 ++ .../lamarzocco/fixtures/current_status.json | 1 + .../lamarzocco/snapshots/test_select.ambr | 221 ++++++++++++++++++ tests/components/lamarzocco/test_select.py | 124 ++++++++++ 6 files changed, 457 insertions(+) create mode 100644 homeassistant/components/lamarzocco/select.py create mode 100644 tests/components/lamarzocco/snapshots/test_select.ambr create mode 100644 tests/components/lamarzocco/test_select.py diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index a5ebf727071..6d2802fb218 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -9,6 +9,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py new file mode 100644 index 00000000000..f29dabae529 --- /dev/null +++ b/homeassistant/components/lamarzocco/select.py @@ -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() diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index db4f443b18b..57f14030a6d 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -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" diff --git a/tests/components/lamarzocco/fixtures/current_status.json b/tests/components/lamarzocco/fixtures/current_status.json index 4367bd1e38d..4f208607c17 100644 --- a/tests/components/lamarzocco/fixtures/current_status.json +++ b/tests/components/lamarzocco/fixtures/current_status.json @@ -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, diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr new file mode 100644 index 00000000000..e35b721436c --- /dev/null +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -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': , + 'entity_id': 'select.gs01234_prebrew_infusion_mode', + 'last_changed': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + 'entity_id': 'select.lm01234_prebrew_infusion_mode', + 'last_changed': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + 'entity_id': 'select.mr01234_prebrew_infusion_mode', + 'last_changed': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + 'entity_id': 'select.mr01234_steam_level', + 'last_changed': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mr01234_steam_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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, + }) +# --- diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py new file mode 100644 index 00000000000..a2e4248f0af --- /dev/null +++ b/tests/components/lamarzocco/test_select.py @@ -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