diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index f8270909bae..ac59d954c37 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -28,6 +28,9 @@ } }, "number": { + "bbw_dose": { + "default": "mdi:weight-gram" + }, "coffee_temp": { "default": "mdi:thermometer-water" }, @@ -51,6 +54,14 @@ } }, "select": { + "bbw_dose_mode": { + "default": "mdi:all-inclusive-box", + "state": { + "continuous": "mdi:all-inclusive-box", + "dose1": "mdi:numeric-1-box", + "dose2": "mdi:numeric-2-box" + } + }, "prebrew_infusion_select": { "default": "mdi:water-pump-off", "state": { diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 675595bac4d..92431d33f90 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -5,9 +5,14 @@ from dataclasses import dataclass from typing import Any, cast from pylamarzocco import LaMarzoccoMachine -from pylamarzocco.const import ModelName, PreExtractionMode, WidgetType +from pylamarzocco.const import DoseMode, ModelName, PreExtractionMode, WidgetType from pylamarzocco.exceptions import RequestNotSuccessful -from pylamarzocco.models import CoffeeBoiler, PreBrewing, SteamBoilerTemperature +from pylamarzocco.models import ( + BrewByWeightDoses, + CoffeeBoiler, + PreBrewing, + SteamBoilerTemperature, +) from homeassistant.components.number import ( NumberDeviceClass, @@ -18,6 +23,7 @@ from homeassistant.const import ( PRECISION_TENTHS, PRECISION_WHOLE, EntityCategory, + UnitOfMass, UnitOfTemperature, UnitOfTime, ) @@ -219,6 +225,72 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( ) ), ), + LaMarzoccoNumberEntityDescription( + key="bbw_dose_1", + translation_key="bbw_dose", + translation_placeholders={"dose": "Dose 1"}, + device_class=NumberDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.GRAMS, + native_step=PRECISION_TENTHS, + native_min_value=5, + native_max_value=100, + entity_category=EntityCategory.CONFIG, + set_value_fn=( + lambda machine, value: machine.set_brew_by_weight_dose( + dose=DoseMode.DOSE_1, + value=value, + ) + ), + native_value_fn=( + lambda machine: cast( + BrewByWeightDoses, + machine.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES], + ).doses.dose_1.dose + ), + available_fn=lambda coordinator: ( + cast( + BrewByWeightDoses, + coordinator.device.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES], + ).scale_connected + ), + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R) + ), + ), + LaMarzoccoNumberEntityDescription( + key="bbw_dose_2", + translation_key="bbw_dose", + translation_placeholders={"dose": "Dose 2"}, + device_class=NumberDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.GRAMS, + native_step=PRECISION_TENTHS, + native_min_value=5, + native_max_value=100, + entity_category=EntityCategory.CONFIG, + set_value_fn=( + lambda machine, value: machine.set_brew_by_weight_dose( + dose=DoseMode.DOSE_2, + value=value, + ) + ), + native_value_fn=( + lambda machine: cast( + BrewByWeightDoses, + machine.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES], + ).doses.dose_2.dose + ), + available_fn=lambda coordinator: ( + cast( + BrewByWeightDoses, + coordinator.device.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES], + ).scale_connected + ), + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R) + ), + ), ) diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index 053c3b75bef..d7662b6f50d 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import Any, cast from pylamarzocco.const import ( + DoseMode, ModelName, PreExtractionMode, SmartStandByType, @@ -13,7 +14,7 @@ from pylamarzocco.const import ( ) from pylamarzocco.devices import LaMarzoccoMachine from pylamarzocco.exceptions import RequestNotSuccessful -from pylamarzocco.models import PreBrewing, SteamBoilerLevel +from pylamarzocco.models import BrewByWeightDoses, PreBrewing, SteamBoilerLevel from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory @@ -50,6 +51,14 @@ STANDBY_MODE_HA_TO_LM = { STANDBY_MODE_LM_TO_HA = {value: key for key, value in STANDBY_MODE_HA_TO_LM.items()} +DOSE_MODE_HA_TO_LM = { + "continuous": DoseMode.CONTINUOUS, + "dose1": DoseMode.DOSE_1, + "dose2": DoseMode.DOSE_2, +} + +DOSE_MODE_LM_TO_HA = {value: key for key, value in DOSE_MODE_HA_TO_LM.items()} + @dataclass(frozen=True, kw_only=True) class LaMarzoccoSelectEntityDescription( @@ -117,6 +126,31 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( machine.schedule.smart_wake_up_sleep.smart_stand_by_after ], ), + LaMarzoccoSelectEntityDescription( + key="bbw_dose_mode", + translation_key="bbw_dose_mode", + entity_category=EntityCategory.CONFIG, + options=["continuous", "dose1", "dose2"], + select_option_fn=lambda machine, option: machine.set_brew_by_weight_dose_mode( + mode=DOSE_MODE_HA_TO_LM[option] + ), + current_option_fn=lambda machine: DOSE_MODE_LM_TO_HA[ + cast( + BrewByWeightDoses, + machine.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES], + ).mode + ], + available_fn=lambda coordinator: ( + cast( + BrewByWeightDoses, + coordinator.device.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES], + ).scale_connected + ), + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R) + ), + ), ) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 9b7194fe5f4..01eec9fba7f 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -87,6 +87,9 @@ } }, "number": { + "bbw_dose": { + "name": "Brew by weight {dose}" + }, "coffee_temp": { "name": "Coffee target temperature" }, @@ -107,6 +110,14 @@ } }, "select": { + "bbw_dose_mode": { + "name": "Brew by weight dose mode", + "state": { + "continuous": "Continuous", + "dose1": "Dose 1", + "dose2": "Dose 2" + } + }, "prebrew_infusion_select": { "name": "Prebrew/-infusion mode", "state": { diff --git a/tests/components/lamarzocco/fixtures/config_mini.json b/tests/components/lamarzocco/fixtures/config_mini.json index a5a285800e9..cf7b1ebb201 100644 --- a/tests/components/lamarzocco/fixtures/config_mini.json +++ b/tests/components/lamarzocco/fixtures/config_mini.json @@ -163,7 +163,7 @@ "code": "CMBrewByWeightDoses", "index": 1, "output": { - "scaleConnected": false, + "scaleConnected": true, "availableModes": ["Continuous"], "mode": "Continuous", "doses": { diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index 72de5e84285..32a9cc613a8 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -1,4 +1,122 @@ # serializer version: 1 +# name: test_brew_by_weight_dose[Linea Mini][entry-dose-1] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 5, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.lm012345_brew_by_weight_dose_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Brew by weight Dose 1', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bbw_dose', + 'unique_id': 'LM012345_bbw_dose_1', + 'unit_of_measurement': , + }) +# --- +# name: test_brew_by_weight_dose[Linea Mini][entry-dose-2] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 5, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.lm012345_brew_by_weight_dose_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Brew by weight Dose 2', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bbw_dose', + 'unique_id': 'LM012345_bbw_dose_2', + 'unit_of_measurement': , + }) +# --- +# name: test_brew_by_weight_dose[Linea Mini][state-dose-1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'LM012345 Brew by weight Dose 1', + 'max': 100, + 'min': 5, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.lm012345_brew_by_weight_dose_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.5', + }) +# --- +# name: test_brew_by_weight_dose[Linea Mini][state-dose-2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'LM012345 Brew by weight Dose 2', + 'max': 100, + 'min': 5, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.lm012345_brew_by_weight_dose_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.5', + }) +# --- # name: test_general_numbers[coffee_target_temperature-94-set_coffee_target_temperature-kwargs0] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr index 701ce6b1cd2..f8516b4b89a 100644 --- a/tests/components/lamarzocco/snapshots/test_select.ambr +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -1,4 +1,63 @@ # serializer version: 1 +# name: test_bbw_dose_mode[Linea Mini] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LM012345 Brew by weight dose mode', + 'options': list([ + 'continuous', + 'dose1', + 'dose2', + ]), + }), + 'context': , + 'entity_id': 'select.lm012345_brew_by_weight_dose_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'continuous', + }) +# --- +# name: test_bbw_dose_mode[Linea Mini].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'continuous', + 'dose1', + 'dose2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.lm012345_brew_by_weight_dose_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': 'Brew by weight dose mode', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bbw_dose_mode', + 'unique_id': 'LM012345_bbw_dose_mode', + 'unit_of_measurement': None, + }) +# --- # name: test_pre_brew_infusion_select[GS3 AV] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 111a306abd1..f73ca211f2a 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import MagicMock from pylamarzocco.const import ( + DoseMode, ModelName, PreExtractionMode, SmartStandByType, @@ -27,6 +28,11 @@ from . import async_init_integration from tests.common import MockConfigEntry +DOSE_MODE_HA_TO_LM = { + "dose1": DoseMode.DOSE_1, + "dose2": DoseMode.DOSE_2, +} + @pytest.mark.parametrize( ("entity_name", "value", "func_name", "kwargs"), @@ -291,3 +297,45 @@ async def test_steam_temperature( mock_lamarzocco.set_steam_target_temperature.assert_called_once_with( temperature=128.3, ) + + +@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MINI]) +async def test_brew_by_weight_dose( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test brew by weight dose.""" + + await async_init_integration(hass, mock_config_entry) + serial_number = mock_lamarzocco.serial_number + for dose in (1, 2): + entity_id = f"number.{serial_number}_brew_by_weight_dose_{dose}" + + state = hass.states.get(entity_id) + + assert state + assert state == snapshot(name=f"state-dose-{dose}") + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot(name=f"entry-dose-{dose}") + + # service call + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 42, + }, + blocking=True, + ) + + mock_lamarzocco.set_brew_by_weight_dose.assert_called_with( + dose=DOSE_MODE_HA_TO_LM[f"dose{dose}"], + value=42, + ) diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index 845eda69d5b..97df0219294 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from pylamarzocco.const import ( + DoseMode, ModelName, PreExtractionMode, SmartStandByType, @@ -193,3 +194,40 @@ async def test_select_errors( blocking=True, ) assert exc_info.value.translation_key == "select_option_error" + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MINI]) +async def test_bbw_dose_mode( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_lamarzocco: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test the La Marzocco Brew By Weight Mode Select (only for Mini R Models).""" + + serial_number = mock_lamarzocco.serial_number + + state = hass.states.get(f"select.{serial_number}_brew_by_weight_dose_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}_brew_by_weight_dose_mode", + ATTR_OPTION: "dose2", + }, + blocking=True, + ) + + mock_lamarzocco.set_brew_by_weight_dose_mode.assert_called_once_with( + mode=DoseMode.DOSE_2 + )