Skip to content

Commit 9101225

Browse files
committed
Update .gitignore to include examples/secrets.json
Update `requirements.txt` to remove `authlib` dependency Update `setup.py` to bump version to `1.2.0` Add `authlib_cloud_client.py` file Update `run.py` to use `LaMarzoccoAuthlibCloudClient` instead of `LaMarzoccoCloudClient` Commit message: Update `.gitignore`, `requirements.txt`, `setup.py`, add `authlib_cloud_client.py`, and update `run.py`
1 parent 9e5362a commit 9101225

8 files changed

+121
-81
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ dmypy.json
130130

131131
#local config
132132
secrets.json
133+
examples/secrets.json
133134
bleak_device_info.py
134135
bleak_discover.py
135136
.gitignore

authlib_cloud_client.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Example implementation of a La Marzocco Cloud Client using Authlib."""
2+
3+
from authlib.common.errors import AuthlibHTTPError # type: ignore[import]
4+
from authlib.integrations.base_client.errors import OAuthError # type: ignore[import]
5+
from authlib.integrations.httpx_client import AsyncOAuth2Client # type: ignore[import]
6+
7+
from lmcloud.client_cloud import LaMarzoccoCloudClient
8+
from lmcloud.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET, TOKEN_URL
9+
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
10+
11+
12+
class LaMarzoccoAuthlibCloudClient(LaMarzoccoCloudClient):
13+
"""La Marzocco Cloud Client using Authlib."""
14+
15+
_client: AsyncOAuth2Client
16+
17+
def __init__(self, username: str, password: str) -> None:
18+
self.username = username
19+
self.password = password
20+
client = AsyncOAuth2Client(
21+
client_id=DEFAULT_CLIENT_ID,
22+
client_secret=DEFAULT_CLIENT_SECRET,
23+
token_endpoint=TOKEN_URL,
24+
)
25+
super().__init__(client)
26+
27+
async def async_get_access_token(self) -> str:
28+
try:
29+
await self._client.fetch_token(
30+
url=TOKEN_URL,
31+
username=self.username,
32+
password=self.password,
33+
)
34+
except OAuthError as exc:
35+
raise AuthFail(f"Authorization failure: {exc}") from exc
36+
except AuthlibHTTPError as exc:
37+
raise RequestNotSuccessful(
38+
f"Exception during token request: {exc}"
39+
) from exc
40+
41+
# make sure oauth token is still valid
42+
if self._client.token.is_expired():
43+
await self._client.refresh_token(TOKEN_URL)
44+
return ""

lmcloud/client_cloud.py

+27-43
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,25 @@
33
from __future__ import annotations
44

55
import asyncio
6+
from abc import abstractmethod
67
from datetime import datetime
78
import logging
89
from http import HTTPMethod
910
from typing import Any
1011

11-
from authlib.common.errors import AuthlibHTTPError # type: ignore[import]
12-
from authlib.integrations.base_client.errors import OAuthError # type: ignore[import]
13-
from authlib.integrations.httpx_client import AsyncOAuth2Client # type: ignore[import]
14-
from httpx import RequestError
12+
from httpx import AsyncClient, RequestError
1513

1614
from .const import (
1715
CUSTOMER_URL,
18-
DEFAULT_CLIENT_ID,
19-
DEFAULT_CLIENT_SECRET,
2016
GW_AWS_PROXY_BASE_URL,
2117
GW_MACHINE_BASE_URL,
22-
TOKEN_URL,
2318
BoilerType,
2419
FirmwareType,
2520
PhysicalKey,
2621
PrebrewMode,
2722
SmartStandbyMode,
2823
)
29-
from .exceptions import AuthFail, RequestNotSuccessful
24+
from .exceptions import RequestNotSuccessful
3025
from .models import LaMarzoccoFirmware, LaMarzoccoDeviceInfo, LaMarzoccoWakeUpSleepEntry
3126

3227
_LOGGER = logging.getLogger(__name__)
@@ -35,49 +30,31 @@
3530
class LaMarzoccoCloudClient:
3631
"""La Marzocco Cloud Client."""
3732

38-
def __init__(self, username: str, password: str):
39-
self._oauth_client: AsyncOAuth2Client | None = None
40-
self.username = username
41-
self.password = password
33+
_client: AsyncClient
4234

43-
async def _connect(self) -> AsyncOAuth2Client:
44-
"""Establish connection by building the OAuth client and requesting the token"""
35+
def __init__(self, client: AsyncClient) -> None:
36+
self._client = client
4537

46-
client = AsyncOAuth2Client(
47-
client_id=DEFAULT_CLIENT_ID,
48-
client_secret=DEFAULT_CLIENT_SECRET,
49-
token_endpoint=TOKEN_URL,
50-
)
51-
52-
try:
53-
await client.fetch_token(
54-
url=TOKEN_URL,
55-
username=self.username,
56-
password=self.password,
57-
)
58-
except OAuthError as exc:
59-
raise AuthFail(f"Authorization failure: {exc}") from exc
60-
except AuthlibHTTPError as exc:
61-
raise RequestNotSuccessful(
62-
f"Exception during token request: {exc}"
63-
) from exc
64-
65-
return client
38+
@abstractmethod
39+
async def async_get_access_token(self) -> str:
40+
"""Return a valid access token."""
6641

6742
async def _rest_api_call(
68-
self, url: str, method: HTTPMethod, data: dict[str, Any] | None = None, timeout: int = 5,
43+
self,
44+
url: str,
45+
method: HTTPMethod,
46+
data: dict[str, Any] | None = None,
47+
timeout: int = 5,
6948
) -> Any:
7049
"""Wrapper for the API call."""
7150

72-
if self._oauth_client is None:
73-
self._oauth_client = await self._connect()
74-
75-
# make sure oauth token is still valid
76-
if self._oauth_client.token.is_expired():
77-
await self._oauth_client.refresh_token(TOKEN_URL)
51+
access_token = await self.async_get_access_token()
52+
headers = {"Authorization": f"Bearer {access_token}"}
7853

7954
try:
80-
response = await self._oauth_client.request(method, url, json=data, timeout=timeout)
55+
response = await self._client.request(
56+
method=method, url=url, json=data, timeout=timeout, headers=headers
57+
)
8158
except RequestError as ecx:
8259
raise RequestNotSuccessful(
8360
f"Error during HTTP request. Request to endpoint {url} failed with error: {ecx}"
@@ -420,7 +397,14 @@ async def get_statistics(self, serial_number: str) -> list[dict[str, Any]]:
420397

421398
return await self._rest_api_call(url=url, method=HTTPMethod.GET)
422399

423-
async def get_daily_statistics(self, serial_number: str, start_date: datetime, end_date: datetime, timezone_offset: int, timezone: str) -> list[dict[str, Any]]:
400+
async def get_daily_statistics(
401+
self,
402+
serial_number: str,
403+
start_date: datetime,
404+
end_date: datetime,
405+
timezone_offset: int,
406+
timezone: str,
407+
) -> list[dict[str, Any]]:
424408
"""Get daily statistics from cloud."""
425409

426410
_LOGGER.debug("Getting daily statistics from cloud")

requirements.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
httpx >=0.16.1
2-
authlib >=0.15.5
32
websockets >= 11.0.2
43
bleak >= 0.20.2

run.py

+15-12
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,35 @@
22

33
import json
44
import asyncio
5-
65
from pathlib import Path
76

8-
from lmcloud.client_cloud import LaMarzoccoCloudClient
7+
98
from lmcloud.lm_device import LaMarzoccoDevice
109
from lmcloud.lm_machine import LaMarzoccoMachine
1110
from lmcloud.const import MachineModel
1211
from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient
1312
from lmcloud.client_local import LaMarzoccoLocalClient
1413

14+
from authlib_cloud_client import LaMarzoccoAuthlibCloudClient
15+
1516

1617
async def main():
1718
"""Main function."""
1819
with open(f"{Path(__file__).parent}/secrets.json", encoding="utf-8") as f:
1920
data = json.load(f)
2021

21-
cloud_client = LaMarzoccoCloudClient(
22+
cloud_client = LaMarzoccoAuthlibCloudClient(
2223
username=data["username"],
2324
password=data["password"],
2425
)
2526
fleet = await cloud_client.get_customer_fleet()
2627

2728
# serial = list(fleet.keys())[0]
2829

29-
local_client = LaMarzoccoLocalClient(
30-
host=data["host"],
31-
local_bearer=data["token"],
32-
)
30+
# local_client = LaMarzoccoLocalClient(
31+
# host=data["host"],
32+
# local_bearer=data["token"],
33+
# )
3334

3435
# bluetooth_devices = await LaMarzoccoBluetoothClient.discover_devices()
3536
# bluetooth_client = None
@@ -55,11 +56,13 @@ async def main():
5556
serial_number=data["serial"],
5657
name=data["serial"],
5758
cloud_client=cloud_client,
58-
local_client=local_client,
59+
# local_client=local_client,
5960
# bluetooth_client=bluetooth_client,
6061
)
6162

62-
await machine.websocket_connect()
63+
await machine.get_config()
64+
65+
# await machine.websocket_connect()
6366
# await asyncio.sleep(300)
6467

6568
# lmcloud = await LMCloud.create_with_local_api(creds, data["host"], data["port"])
@@ -89,9 +92,9 @@ async def main():
8992
# print("Brewing")
9093
# await asyncio.sleep(1)
9194

92-
await machine.set_power(True)
93-
await asyncio.sleep(5)
94-
await machine.set_power(False)
95+
# await machine.set_power(True)
96+
# await asyncio.sleep(5)
97+
# await machine.set_power(False)
9598

9699
# while True:
97100
# print("waiting...")

setup.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setuptools.setup(
99
name="lmcloud",
10-
version="1.1.13",
10+
version="1.2.0",
1111
description="A Python implementation of the new La Marzocco API",
1212
long_description=readme,
1313
long_description_content_type="text/markdown",
@@ -27,7 +27,6 @@
2727
packages=setuptools.find_packages(),
2828
install_requires=[
2929
"httpx>=0.16.1",
30-
"authlib>=0.15.5",
3130
"websockets>=11.0.2",
3231
"bleak>=0.20.2",
3332
],

tests/conftest.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
)
2323

2424

25+
class ImplementedLaMarzoccoCloudClient(LaMarzoccoCloudClient):
26+
"""Implemented La Marzocco Cloud Client."""
27+
28+
async def async_get_access_token(self) -> str:
29+
return "token"
30+
31+
2532
def load_fixture(device_type: str, file_name: str) -> dict:
2633
"""Load a fixture."""
2734
with open(
@@ -32,8 +39,8 @@ def load_fixture(device_type: str, file_name: str) -> dict:
3239

3340
def get_mock_response(*args, **kwargs) -> Response:
3441
"""Get a mock response from HTTP request."""
35-
method: HTTPMethod = args[0]
36-
url: str = str(args[1])
42+
method: HTTPMethod = kwargs["method"]
43+
url: str = str(kwargs["url"])
3744

3845
if MACHINE_SERIAL in url:
3946
device_type = "machine"
@@ -76,13 +83,11 @@ def get_local_machine_mock_response(*args, **kwargs) -> Response:
7683
def cloud_client() -> Generator[LaMarzoccoCloudClient, None, None]:
7784
"""Fixture for a cloud client."""
7885

79-
client = LaMarzoccoCloudClient("username", "password")
86+
client = AsyncMock()
87+
client.request.side_effect = get_mock_response
88+
_cloud_client = ImplementedLaMarzoccoCloudClient(client=client)
8089

81-
oauth_client = AsyncMock()
82-
oauth_client.request.side_effect = get_mock_response
83-
84-
client._oauth_client = oauth_client
85-
yield client
90+
yield _cloud_client
8691

8792

8893
@pytest.fixture

tests/test_machine.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,12 @@ async def test_set_power(
107107
"050b7847-e12b-09a8-b04b-8e0922a9abab",
108108
b'{"name":"MachineChangeMode","parameter":{"mode":"BrewingMode"}}\x00',
109109
)
110-
cloud_client._oauth_client.request.assert_any_call( # type: ignore[union-attr]
111-
HTTPMethod.POST,
112-
"https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/status",
110+
cloud_client._client.request.assert_any_call( # type: ignore[attr-defined]
111+
method=HTTPMethod.POST,
112+
url="https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/status",
113113
json={"status": "BrewingMode"},
114114
timeout=5,
115+
headers={"Authorization": "Bearer token"},
115116
)
116117

117118

@@ -127,11 +128,12 @@ async def test_set_steam(
127128
"050b7847-e12b-09a8-b04b-8e0922a9abab",
128129
b'{"name":"SettingBoilerEnable","parameter":{"identifier":"SteamBoiler","state":true}}\x00',
129130
)
130-
cloud_client._oauth_client.request.assert_any_call( # type: ignore[union-attr]
131-
HTTPMethod.POST,
132-
"https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/enable-boiler",
131+
cloud_client._client.request.assert_any_call( # type: ignore[attr-defined]
132+
method=HTTPMethod.POST,
133+
url="https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/enable-boiler",
133134
json={"identifier": "SteamBoiler", "state": True},
134135
timeout=5,
136+
headers={"Authorization": "Bearer token"},
135137
)
136138

137139

@@ -147,11 +149,12 @@ async def test_set_temperature(
147149
"050b7847-e12b-09a8-b04b-8e0922a9abab",
148150
b'{"name":"SettingBoilerTarget","parameter":{"identifier":"SteamBoiler","value":131}}\x00',
149151
)
150-
cloud_client._oauth_client.request.assert_any_call( # type: ignore[union-attr]
151-
HTTPMethod.POST,
152-
"https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/target-boiler",
152+
cloud_client._client.request.assert_any_call( # type: ignore[attr-defined]
153+
method=HTTPMethod.POST,
154+
url="https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/target-boiler",
153155
json={"identifier": "SteamBoiler", "value": 131},
154156
timeout=5,
157+
headers={"Authorization": "Bearer token"},
155158
)
156159

157160

@@ -163,27 +166,29 @@ async def test_set_prebrew_time(cloud_client: LaMarzoccoCloudClient):
163166

164167
assert await machine.set_prebrew_time(1.0, 3.5)
165168

166-
cloud_client._oauth_client.request.assert_any_call( # type: ignore[union-attr]
167-
HTTPMethod.POST,
168-
"https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/setting-preinfusion",
169+
cloud_client._client.request.assert_any_call( # type: ignore[attr-defined]
170+
method=HTTPMethod.POST,
171+
url="https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/setting-preinfusion",
169172
json={
170173
"button": "DoseA",
171174
"group": "Group1",
172175
"holdTimeMs": 3500,
173176
"wetTimeMs": 1000,
174177
},
175178
timeout=5,
179+
headers={"Authorization": "Bearer token"},
176180
)
177181

178182
assert machine.config.prebrew_configuration[PhysicalKey.A].on_time == 1.0
179183
assert machine.config.prebrew_configuration[PhysicalKey.A].off_time == 3.5
180184

181185
assert await machine.set_preinfusion_time(4.5)
182-
cloud_client._oauth_client.request.assert_any_call( # type: ignore[union-attr]
183-
HTTPMethod.POST,
184-
"https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/setting-preinfusion",
186+
cloud_client._client.request.assert_any_call( # type: ignore[attr-defined]
187+
method=HTTPMethod.POST,
188+
url="https://gw-lmz.lamarzocco.io/v1/home/machines/GS01234/setting-preinfusion",
185189
json={"button": "DoseA", "group": "Group1", "holdTimeMs": 4500, "wetTimeMs": 0},
186190
timeout=5,
191+
headers={"Authorization": "Bearer token"},
187192
)
188193

189194
assert machine.config.prebrew_configuration[PhysicalKey.A].off_time == 4.5

0 commit comments

Comments
 (0)