1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add brew by weight controls to lamarzocco (#158169)

This commit is contained in:
Josef Zweck
2025-12-13 22:28:11 +01:00
committed by GitHub
parent 2c2934065f
commit afb9e18a7d
9 changed files with 395 additions and 4 deletions
@@ -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": {
+74 -2
View File
@@ -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)
),
),
)
+35 -1
View File
@@ -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)
),
),
)
@@ -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": {
@@ -163,7 +163,7 @@
"code": "CMBrewByWeightDoses",
"index": 1,
"output": {
"scaleConnected": false,
"scaleConnected": true,
"availableModes": ["Continuous"],
"mode": "Continuous",
"doses": {
@@ -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': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.lm012345_brew_by_weight_dose_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.WEIGHT: 'weight'>,
'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': <UnitOfMass.GRAMS: 'g'>,
})
# ---
# name: test_brew_by_weight_dose[Linea Mini][entry-dose-2]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 5,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.lm012345_brew_by_weight_dose_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.WEIGHT: 'weight'>,
'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': <UnitOfMass.GRAMS: 'g'>,
})
# ---
# 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': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
'unit_of_measurement': <UnitOfMass.GRAMS: 'g'>,
}),
'context': <ANY>,
'entity_id': 'number.lm012345_brew_by_weight_dose_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
'unit_of_measurement': <UnitOfMass.GRAMS: 'g'>,
}),
'context': <ANY>,
'entity_id': 'number.lm012345_brew_by_weight_dose_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '17.5',
})
# ---
# name: test_general_numbers[coffee_target_temperature-94-set_coffee_target_temperature-kwargs0]
StateSnapshot({
'attributes': ReadOnlyDict({
@@ -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': <ANY>,
'entity_id': 'select.lm012345_brew_by_weight_dose_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.lm012345_brew_by_weight_dose_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': '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({
@@ -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,
)
@@ -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
)