WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 8ca4bd7

Browse files
authored
Merge pull request #136 from zweckj/copilot/add-cloud-commands-linea-mini-r
Add brew by weight cloud commands for Linea Mini R
2 parents 56418b7 + ec29065 commit 8ca4bd7

File tree

5 files changed

+217
-9
lines changed

5 files changed

+217
-9
lines changed

pylamarzocco/clients/_cloud.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
BASE_URL,
2727
CUSTOMER_APP_URL,
2828
CommandStatus,
29+
DoseMode,
2930
PreExtractionMode,
3031
SmartStandByType,
3132
SteamTargetLevel,
@@ -571,7 +572,7 @@ async def set_coffee_target_temperature(
571572
return await self.__execute_command(
572573
serial_number, "CoffeeMachineSettingCoffeeBoilerTargetTemperature", data
573574
)
574-
575+
575576
async def set_steam_target_temperature(
576577
self, serial_number: str, target_temperature: float, boiler_index: int = 1
577578
) -> bool:
@@ -649,6 +650,34 @@ async def set_wakeup_schedule(
649650
serial_number, "CoffeeMachineSetWakeUpSchedule", schedule.to_dict()
650651
)
651652

653+
async def change_brew_by_weight_dose_mode(
654+
self,
655+
serial_number: str,
656+
mode: DoseMode,
657+
) -> bool:
658+
"""Change the brew by weight dose mode (Linea Mini R only)."""
659+
data = {"mode": mode.value}
660+
return await self.__execute_command(
661+
serial_number, "CoffeeMachineBrewByWeightChangeMode", data
662+
)
663+
664+
async def set_brew_by_weight_dose(
665+
self,
666+
serial_number: str,
667+
dose_1: float,
668+
dose_2: float,
669+
) -> bool:
670+
"""Set the brew by weight doses (Linea Mini R only)."""
671+
data = {
672+
"doses": {
673+
"Dose1": round(dose_1, 1),
674+
"Dose2": round(dose_2, 1),
675+
}
676+
}
677+
return await self.__execute_command(
678+
serial_number, "CoffeeMachineBrewByWeightSettingDoses", data
679+
)
680+
652681
async def update_firmware(
653682
self,
654683
serial_number: str,

pylamarzocco/devices/_machine.py

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pylamarzocco.const import (
1212
BoilerStatus,
1313
BoilerType,
14+
DoseMode,
1415
MachineMode,
1516
MachineState,
1617
ModelCode,
@@ -21,6 +22,7 @@
2122
)
2223
from pylamarzocco.exceptions import BluetoothConnectionFailed
2324
from pylamarzocco.models import (
25+
BrewByWeightDoses,
2426
CoffeeAndFlushCounter,
2527
CoffeeAndFlushTrend,
2628
CoffeeBoiler,
@@ -68,13 +70,13 @@ async def get_schedule(self) -> None:
6870

6971
async def get_model_info_from_bluetooth(self) -> None:
7072
"""Fetch and update model information from Bluetooth.
71-
73+
7274
Retrieves machine capabilities via Bluetooth and updates the dashboard
7375
with model_name and model_code information.
7476
"""
7577
if self._bluetooth_client is None:
7678
raise BluetoothConnectionFailed("Bluetooth client not initialized")
77-
79+
7880
try:
7981
capabilities = await self._bluetooth_client.get_machine_capabilities()
8082
except (BleakError, BluetoothConnectionFailed) as exc:
@@ -168,9 +170,13 @@ async def get_dashboard_from_bluetooth(self) -> None:
168170
),
169171
)
170172
steam_level.enabled = boiler.is_enabled
171-
self.dashboard.config[WidgetType.CM_STEAM_BOILER_LEVEL] = steam_level
173+
self.dashboard.config[WidgetType.CM_STEAM_BOILER_LEVEL] = (
174+
steam_level
175+
)
172176
# Remove temperature widget if it exists (not applicable for this model)
173-
self.dashboard.config.pop(WidgetType.CM_STEAM_BOILER_TEMPERATURE, None)
177+
self.dashboard.config.pop(
178+
WidgetType.CM_STEAM_BOILER_TEMPERATURE, None
179+
)
174180
else:
175181
# Other models (GS3, original Mini) use steam temperature widget
176182
steam_temp = cast(
@@ -309,7 +315,7 @@ async def set_coffee_target_temperature(self, temperature: float) -> bool:
309315
coffee_boiler.target_temperature = float(temperature)
310316

311317
return result
312-
318+
313319
@models_supported((ModelCode.GS3, ModelCode.GS3_AV, ModelCode.GS3_MP))
314320
async def set_steam_target_temperature(self, temperature: float) -> bool:
315321
"""Set the steam target temperature."""
@@ -401,6 +407,78 @@ async def set_wakeup_schedule(
401407
self.serial_number, schedule
402408
)
403409

410+
@cloud_only
411+
@models_supported((ModelCode.LINEA_MINI_R,))
412+
async def set_brew_by_weight_dose_mode(self, mode: DoseMode) -> bool:
413+
"""Set the brew by weight dose mode (Linea Mini R only).
414+
415+
Args:
416+
mode: The dose mode (DoseMode.DOSE_1, DoseMode.DOSE_2, or DoseMode.CONTINUOUS)
417+
"""
418+
assert self._cloud_client
419+
result = await self._cloud_client.change_brew_by_weight_dose_mode(
420+
self.serial_number, mode
421+
)
422+
423+
# Update dashboard if command succeeded
424+
if result and WidgetType.CM_BREW_BY_WEIGHT_DOSES in self.dashboard.config:
425+
brew_by_weight = cast(
426+
BrewByWeightDoses,
427+
self.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
428+
)
429+
brew_by_weight.mode = mode
430+
431+
return result
432+
433+
@cloud_only
434+
@models_supported((ModelCode.LINEA_MINI_R,))
435+
async def set_brew_by_weight_dose(self, dose: DoseMode, value: float) -> bool:
436+
"""Set a brew by weight dose value (Linea Mini R only).
437+
438+
Args:
439+
dose: Which dose to set (must be DoseMode.DOSE_1 or DoseMode.DOSE_2,
440+
CONTINUOUS is not valid for setting dose values)
441+
value: The dose value in grams
442+
443+
Returns:
444+
True if the command was successful, False otherwise.
445+
Returns False if dose is not DOSE_1 or DOSE_2 or if the brew by
446+
weight widget is not available in the dashboard.
447+
"""
448+
assert self._cloud_client
449+
450+
# Get current doses from dashboard
451+
if WidgetType.CM_BREW_BY_WEIGHT_DOSES not in self.dashboard.config:
452+
return False
453+
454+
brew_by_weight = cast(
455+
BrewByWeightDoses,
456+
self.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
457+
)
458+
459+
# Set the dose values, keeping the other one unchanged
460+
if dose == DoseMode.DOSE_1:
461+
dose_1 = value
462+
dose_2 = brew_by_weight.doses.dose_2.dose
463+
elif dose == DoseMode.DOSE_2:
464+
dose_1 = brew_by_weight.doses.dose_1.dose
465+
dose_2 = value
466+
else:
467+
return False
468+
469+
result = await self._cloud_client.set_brew_by_weight_dose(
470+
self.serial_number, dose_1, dose_2
471+
)
472+
473+
# Update dashboard if command succeeded
474+
if result:
475+
if dose == DoseMode.DOSE_1:
476+
brew_by_weight.doses.dose_1.dose = value
477+
else:
478+
brew_by_weight.doses.dose_2.dose = value
479+
480+
return result
481+
404482
def to_dict(self) -> dict[Any, Any]:
405483
"""Return self in dict represenation."""
406484
return {

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pylamarzocco"
3-
version = "2.2.2"
3+
version = "2.2.3"
44
license = { text = "MIT" }
55
description = "A Python implementation of the La Marzocco API"
66
readme = "README.md"

tests/test_cloud_client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pylamarzocco.const import (
1717
CUSTOMER_APP_URL,
1818
CommandStatus,
19+
DoseMode,
1920
PreExtractionMode,
2021
SmartStandByType,
2122
SteamTargetLevel,
@@ -640,3 +641,48 @@ async def test_start_update(
640641
client = LaMarzoccoCloudClient("test", "test", MOCK_SECRET_DATA)
641642
result = await client.update_firmware(serial)
642643
assert result.to_dict() == snapshot
644+
645+
646+
async def test_change_brew_by_weight_dose_mode(
647+
mock_aioresponse: aioresponses,
648+
serial: str,
649+
) -> None:
650+
"""Test changing the brew by weight dose mode."""
651+
652+
url = f"{CUSTOMER_APP_URL}/things/{serial}/command/CoffeeMachineBrewByWeightChangeMode"
653+
mock_aioresponse.post(
654+
url=url,
655+
status=200,
656+
payload=MOCK_COMMAND_RESPONSE,
657+
)
658+
659+
client = LaMarzoccoCloudClient("test", "test", MOCK_SECRET_DATA)
660+
result = await client.change_brew_by_weight_dose_mode(serial, DoseMode.DOSE_1)
661+
call = mock_aioresponse.requests[(HTTPMethod.POST, URL(url))][0]
662+
assert call.kwargs["json"] == {"mode": "Dose1"}
663+
assert result is True
664+
665+
666+
async def test_set_brew_by_weight_dose(
667+
mock_aioresponse: aioresponses,
668+
serial: str,
669+
) -> None:
670+
"""Test setting the brew by weight doses."""
671+
672+
url = f"{CUSTOMER_APP_URL}/things/{serial}/command/CoffeeMachineBrewByWeightSettingDoses"
673+
mock_aioresponse.post(
674+
url=url,
675+
status=200,
676+
payload=MOCK_COMMAND_RESPONSE,
677+
)
678+
679+
client = LaMarzoccoCloudClient("test", "test", MOCK_SECRET_DATA)
680+
result = await client.set_brew_by_weight_dose(serial, 32.56, 45.67)
681+
call = mock_aioresponse.requests[(HTTPMethod.POST, URL(url))][0]
682+
assert call.kwargs["json"] == {
683+
"doses": {
684+
"Dose1": 32.6,
685+
"Dose2": 45.7,
686+
}
687+
}
688+
assert result is True

tests/test_machine.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,21 @@
99
LaMarzoccoCloudClient,
1010
LaMarzoccoMachine,
1111
)
12-
from pylamarzocco.const import BoilerType, SteamTargetLevel, SmartStandByType, ModelCode
12+
from pylamarzocco.const import (
13+
BoilerType,
14+
DoseMode,
15+
ModelCode,
16+
SmartStandByType,
17+
SteamTargetLevel,
18+
WidgetType,
19+
)
1320
from pylamarzocco.exceptions import BluetoothConnectionFailed
14-
from pylamarzocco.models import BluetoothCommandStatus
21+
from pylamarzocco.models import (
22+
BaseDoseSettings,
23+
BluetoothCommandStatus,
24+
BrewByWeightDoses,
25+
BrewByWeightDoseSettings,
26+
)
1527

1628

1729
@pytest.fixture(name="mock_bluetooth_client")
@@ -210,3 +222,46 @@ async def test_failing_command(
210222
)
211223
mock_cloud_client.set_power.return_value = False
212224
assert not await mock_machine.set_power(True)
225+
226+
227+
async def test_set_brew_by_weight_dose_mode(
228+
mock_machine: LaMarzoccoMachine,
229+
mock_cloud_client: MagicMock,
230+
) -> None:
231+
"""Test the set_brew_by_weight_dose_mode method."""
232+
mock_machine.dashboard.model_code = ModelCode.LINEA_MINI_R
233+
mock_cloud_client.change_brew_by_weight_dose_mode.return_value = True
234+
235+
assert await mock_machine.set_brew_by_weight_dose_mode(DoseMode.DOSE_1)
236+
mock_cloud_client.change_brew_by_weight_dose_mode.assert_called_once_with(
237+
"MR123456", DoseMode.DOSE_1
238+
)
239+
240+
241+
async def test_set_brew_by_weight_dose(
242+
mock_machine: LaMarzoccoMachine,
243+
mock_cloud_client: MagicMock,
244+
) -> None:
245+
"""Test the set_brew_by_weight_dose method."""
246+
mock_machine.dashboard.model_code = ModelCode.LINEA_MINI_R
247+
mock_cloud_client.set_brew_by_weight_dose.return_value = True
248+
249+
# Set up the dashboard with brew by weight widget
250+
mock_machine.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES] = (
251+
BrewByWeightDoses(
252+
mode=DoseMode.DOSE_1,
253+
doses=BrewByWeightDoseSettings(
254+
dose_1=BaseDoseSettings(
255+
dose=32.0, dose_min=5, dose_max=100, dose_step=1
256+
),
257+
dose_2=BaseDoseSettings(
258+
dose=34.0, dose_min=5, dose_max=100, dose_step=1
259+
),
260+
),
261+
)
262+
)
263+
264+
assert await mock_machine.set_brew_by_weight_dose(DoseMode.DOSE_1, 36.5)
265+
mock_cloud_client.set_brew_by_weight_dose.assert_called_once_with(
266+
"MR123456", 36.5, 34.0
267+
)

0 commit comments

Comments
 (0)