pylamarzocco 1.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Josef
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.1
2
+ Name: pylamarzocco
3
+ Version: 1.2.3
4
+ Summary: A Python implementation of the new La Marzocco API
5
+ Home-page: https://github.com/zweckj/pylamarzocco
6
+ Author: Josef Zweck
7
+ Author-email: 24647999+zweckj@users.noreply.github.com
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Natural Language :: English
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: httpx>=0.16.1
19
+ Requires-Dist: websockets>=11.0.2
20
+ Requires-Dist: bleak>=0.20.2
21
+
22
+ # La Marzocco Python Client
23
+
24
+ This is a library to interface with La Marzocco's Home machines.
25
+ It also has support to get information for the Pico grinder.
26
+
27
+ ![workflow](https://github.com/zweckj/pylamarzocco/actions/workflows/pypi.yaml/badge.svg)
28
+ [![codecov](https://codecov.io/gh/zweckj/pylamarzocco/graph/badge.svg?token=350GPTLZXS)](https://codecov.io/gh/zweckj/pylamarzocco)
29
+
30
+ # Libraries in this project
31
+
32
+ - `LaMarzoccoLocalClient` calls the new local API the Micra exposes, using the Bearer token from the customer cloud endpoint. However, this API currently only supports getting the config, and some status objects (like shottimer) over websockets, but does not support setting anything (to my knowledge). Local settings appear to only happen through [Bluetooth connections](#lmbluetooth).
33
+ - `LaMarzoccoCloudClient` interacts with `gw-lmz.lamarzocco.com` to send commands. pylamarzocco can be initialized to only issue remote commands, or to initialize an instance of `lmlocalapi` for getting the current machine settings. This helps to avoid flooding the cloud API and is faster overall.
34
+ - `LaMarzoccoBluetoothClient` provides a bluetooth client to send settings to the machine via bluetooth
35
+
36
+ # Setup
37
+
38
+ ## LaMarzoccoCloudClient
39
+
40
+ You need `username` and `password`, which are the credentials you're using to sign into the La Marzocco Home app.
41
+
42
+ It is initialized like this
43
+
44
+ ```python
45
+ cloud_client = await LaMarzoccoCloudClient(username, password)
46
+ ```
47
+
48
+ ## LaMarzoccoLocalClient
49
+
50
+ If you just want to run the local API you need the IP of your machine, the Port it is listening on (8081 by default), the Bearer token (`communicationKey`) used for local communication.
51
+ You can obtain that key by inspecting a call to `https://cms.lamarzocco.io/api/customer`, while connected to `mitmproxy` (process above), or making a new (authenticated) call to that endpoint.
52
+
53
+ Then you can init the class with
54
+
55
+ ```python
56
+ local_client = LaMarzoccoLocalClient(ip, local_token)
57
+ ```
58
+
59
+ ## LaMarzoccoBluetoothClient
60
+
61
+ Some commands, like turning the machine on and off are always sent through bluetooth whenever possible. The available bluetooth characteristics are described in [bluetooth_characteristics](docs/bluetooth_characteristics.md).
62
+ The class `LaMarzoccoBluetoothClient` discovers any bluetooth devices connects to it. Then we can send local bluetooth commands.
63
+
64
+ To use Bluetooth you can either init pylamarzocco with
65
+
66
+ ```python
67
+ if bluetooth_devices := LaMarzoccoBluetoothClient.discover_devices():
68
+ print("Found bluetooth device:", bluetooth_devices[0])
69
+
70
+ bluetooth_client = LaMarzoccoBluetoothClient(
71
+ username,
72
+ serial_number,
73
+ local_token
74
+ bluetooth_devices[0],
75
+ )
76
+ ```
77
+
78
+ The local_token is the same token you need to initialize the local API, which you need to get from LM's cloud once. The serial number is your machine's serial number and the username is the email of your LaMarzocco account.
79
+
80
+ ## Machine
81
+
82
+ Once you have any or all of the clients, you can initialize a machine object with
83
+
84
+ ```python
85
+ machine = Machine.create(model, serial_number, name, cloud_client, local_client, bluetooth_client)
86
+ ```
87
+
88
+ You can then use the machine object to send commands to the machine, or to get the current status of the machine. If you're running in cloud only mode, please be mindful with the requests to not flood the cloud API.
89
+
90
+ ## Grinder
91
+
92
+ The Pico grinder can be initialized with
93
+
94
+ ```python
95
+ grinder = LaMarzoccoGrinder.create(model, serial_number, name, cloud_client, local_client, bluetooth_client)
96
+ ```
97
+
98
+ where you can use the same cloud client as for the machine, but you need to initialize new local and bluetooth clients (the same way as for the machine) to use the grinder.
99
+
100
+ ### Websockets
101
+
102
+ The local API initiates a websocket connection to
103
+
104
+ ```
105
+ http://{IP}:8081/api/v1/streaming
106
+ ```
107
+
108
+ The packets which are received on that WebSocket are documented in [websockets](docs/websockets.md)
109
+
110
+ If WebSockets are enabled the shot timer becomes available to use, however as long as the library is running in WebSocket mode, the App will no longer be able to connect.
111
+
112
+ To use WebSockets start the integration with
113
+
114
+ ```python
115
+ await machine.websocket_connect(callback)
116
+ ```
117
+
118
+ with an optional callback function that will be called whenever there have been updates for the machine from the websocket.
@@ -0,0 +1,97 @@
1
+ # La Marzocco Python Client
2
+
3
+ This is a library to interface with La Marzocco's Home machines.
4
+ It also has support to get information for the Pico grinder.
5
+
6
+ ![workflow](https://github.com/zweckj/pylamarzocco/actions/workflows/pypi.yaml/badge.svg)
7
+ [![codecov](https://codecov.io/gh/zweckj/pylamarzocco/graph/badge.svg?token=350GPTLZXS)](https://codecov.io/gh/zweckj/pylamarzocco)
8
+
9
+ # Libraries in this project
10
+
11
+ - `LaMarzoccoLocalClient` calls the new local API the Micra exposes, using the Bearer token from the customer cloud endpoint. However, this API currently only supports getting the config, and some status objects (like shottimer) over websockets, but does not support setting anything (to my knowledge). Local settings appear to only happen through [Bluetooth connections](#lmbluetooth).
12
+ - `LaMarzoccoCloudClient` interacts with `gw-lmz.lamarzocco.com` to send commands. pylamarzocco can be initialized to only issue remote commands, or to initialize an instance of `lmlocalapi` for getting the current machine settings. This helps to avoid flooding the cloud API and is faster overall.
13
+ - `LaMarzoccoBluetoothClient` provides a bluetooth client to send settings to the machine via bluetooth
14
+
15
+ # Setup
16
+
17
+ ## LaMarzoccoCloudClient
18
+
19
+ You need `username` and `password`, which are the credentials you're using to sign into the La Marzocco Home app.
20
+
21
+ It is initialized like this
22
+
23
+ ```python
24
+ cloud_client = await LaMarzoccoCloudClient(username, password)
25
+ ```
26
+
27
+ ## LaMarzoccoLocalClient
28
+
29
+ If you just want to run the local API you need the IP of your machine, the Port it is listening on (8081 by default), the Bearer token (`communicationKey`) used for local communication.
30
+ You can obtain that key by inspecting a call to `https://cms.lamarzocco.io/api/customer`, while connected to `mitmproxy` (process above), or making a new (authenticated) call to that endpoint.
31
+
32
+ Then you can init the class with
33
+
34
+ ```python
35
+ local_client = LaMarzoccoLocalClient(ip, local_token)
36
+ ```
37
+
38
+ ## LaMarzoccoBluetoothClient
39
+
40
+ Some commands, like turning the machine on and off are always sent through bluetooth whenever possible. The available bluetooth characteristics are described in [bluetooth_characteristics](docs/bluetooth_characteristics.md).
41
+ The class `LaMarzoccoBluetoothClient` discovers any bluetooth devices connects to it. Then we can send local bluetooth commands.
42
+
43
+ To use Bluetooth you can either init pylamarzocco with
44
+
45
+ ```python
46
+ if bluetooth_devices := LaMarzoccoBluetoothClient.discover_devices():
47
+ print("Found bluetooth device:", bluetooth_devices[0])
48
+
49
+ bluetooth_client = LaMarzoccoBluetoothClient(
50
+ username,
51
+ serial_number,
52
+ local_token
53
+ bluetooth_devices[0],
54
+ )
55
+ ```
56
+
57
+ The local_token is the same token you need to initialize the local API, which you need to get from LM's cloud once. The serial number is your machine's serial number and the username is the email of your LaMarzocco account.
58
+
59
+ ## Machine
60
+
61
+ Once you have any or all of the clients, you can initialize a machine object with
62
+
63
+ ```python
64
+ machine = Machine.create(model, serial_number, name, cloud_client, local_client, bluetooth_client)
65
+ ```
66
+
67
+ You can then use the machine object to send commands to the machine, or to get the current status of the machine. If you're running in cloud only mode, please be mindful with the requests to not flood the cloud API.
68
+
69
+ ## Grinder
70
+
71
+ The Pico grinder can be initialized with
72
+
73
+ ```python
74
+ grinder = LaMarzoccoGrinder.create(model, serial_number, name, cloud_client, local_client, bluetooth_client)
75
+ ```
76
+
77
+ where you can use the same cloud client as for the machine, but you need to initialize new local and bluetooth clients (the same way as for the machine) to use the grinder.
78
+
79
+ ### Websockets
80
+
81
+ The local API initiates a websocket connection to
82
+
83
+ ```
84
+ http://{IP}:8081/api/v1/streaming
85
+ ```
86
+
87
+ The packets which are received on that WebSocket are documented in [websockets](docs/websockets.md)
88
+
89
+ If WebSockets are enabled the shot timer becomes available to use, however as long as the library is running in WebSocket mode, the App will no longer be able to connect.
90
+
91
+ To use WebSockets start the integration with
92
+
93
+ ```python
94
+ await machine.websocket_connect(callback)
95
+ ```
96
+
97
+ with an optional callback function that will be called whenever there have been updates for the machine from the websocket.
@@ -0,0 +1,7 @@
1
+ """Import for ease of use."""
2
+
3
+ from .client_local import LaMarzoccoLocalClient
4
+ from .client_cloud import LaMarzoccoCloudClient
5
+ from .client_bluetooth import LaMarzoccoBluetoothClient
6
+ from .lm_machine import LaMarzoccoMachine
7
+ from .lm_grinder import LaMarzoccoGrinder
@@ -0,0 +1,167 @@
1
+ """Bluetooth class for La Marzocco machines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import logging
8
+ from typing import Any
9
+
10
+ from bleak import (
11
+ BaseBleakScanner,
12
+ BleakClient,
13
+ BleakError,
14
+ BleakScanner,
15
+ BLEDevice,
16
+ )
17
+
18
+ from .const import (
19
+ AUTH_CHARACTERISTIC,
20
+ BT_MODEL_PREFIXES,
21
+ SETTINGS_CHARACTERISTIC,
22
+ BoilerType,
23
+ )
24
+ from .exceptions import (
25
+ BluetoothConnectionFailed,
26
+ )
27
+
28
+ _logger = logging.getLogger(__name__)
29
+
30
+
31
+ class LaMarzoccoBluetoothClient:
32
+ """Class to interact with machine via Bluetooth."""
33
+
34
+ def __init__(
35
+ self,
36
+ username: str,
37
+ serial_number: str,
38
+ token: str,
39
+ address_or_ble_device: BLEDevice | str,
40
+ ) -> None:
41
+ """Initializes a new LaMarzoccoBluetoothClient instance."""
42
+ self._username = username
43
+ self._serial_number = serial_number
44
+ self._token = token
45
+ self._address = (
46
+ address_or_ble_device.address
47
+ if isinstance(address_or_ble_device, BLEDevice)
48
+ else address_or_ble_device
49
+ )
50
+ self._address_or_ble_device = address_or_ble_device
51
+ self._client = BleakClient(address_or_ble_device)
52
+
53
+ @staticmethod
54
+ async def discover_devices(
55
+ scanner: BaseBleakScanner | BleakScanner | None = None,
56
+ ) -> list[BLEDevice]:
57
+ """Find machines based on model name."""
58
+ ble_devices: list[BLEDevice] = []
59
+
60
+ if scanner is None:
61
+ scanner = BleakScanner()
62
+ assert hasattr(scanner, "discover")
63
+ devices: list[BLEDevice] = await scanner.discover()
64
+ for device in devices:
65
+ if device.name and device.name.startswith(BT_MODEL_PREFIXES):
66
+ ble_devices.append(device)
67
+
68
+ return ble_devices
69
+
70
+ @property
71
+ def address(self) -> str:
72
+ """Return the BT MAC address of the machine."""
73
+
74
+ return self._address
75
+
76
+ @property
77
+ def connected(self) -> bool:
78
+ """Return the connection status."""
79
+
80
+ return self._client.is_connected
81
+
82
+ async def set_power(self, enabled: bool) -> None:
83
+ """Power on the machine."""
84
+
85
+ mode = "BrewingMode" if enabled else "StandBy"
86
+ data = {
87
+ "name": "MachineChangeMode",
88
+ "parameter": {
89
+ "mode": mode,
90
+ },
91
+ }
92
+ await self._write_bluetooth_json_message(data)
93
+
94
+ async def set_steam(self, enabled: bool) -> None:
95
+ """Power cycle steam."""
96
+
97
+ data = {
98
+ "name": "SettingBoilerEnable",
99
+ "parameter": {
100
+ "identifier": "SteamBoiler",
101
+ "state": enabled,
102
+ },
103
+ }
104
+ await self._write_bluetooth_json_message(data)
105
+
106
+ async def set_temp(self, boiler: BoilerType, temperature: float) -> None:
107
+ """Set boiler temperature (in Celsius)"""
108
+
109
+ data = {
110
+ "name": "SettingBoilerTarget",
111
+ "parameter": {
112
+ "identifier": boiler.value,
113
+ "value": temperature,
114
+ },
115
+ }
116
+ await self._write_bluetooth_json_message(data)
117
+
118
+ async def _write_bluetooth_message(
119
+ self, characteristic: str, message: bytes | str
120
+ ) -> None:
121
+ """Connect to machine and write a message."""
122
+
123
+ if not self._client.is_connected:
124
+ try:
125
+ self._client = BleakClient(self._address_or_ble_device)
126
+ await self._client.connect()
127
+ await self._authenticate()
128
+ except (BleakError, TimeoutError) as e:
129
+ raise BluetoothConnectionFailed(
130
+ f"Failed to connect to machine with Bluetooth: {e}"
131
+ ) from e
132
+
133
+ # check if message is already bytes string
134
+ if not isinstance(message, bytes):
135
+ message = bytes(message, "utf-8")
136
+
137
+ # append trailing zeros to settings message
138
+ if characteristic == SETTINGS_CHARACTERISTIC:
139
+ message += b"\x00"
140
+
141
+ _logger.debug("Sending bluetooth message: %s to %s", message, characteristic)
142
+
143
+ await self._client.write_gatt_char(characteristic, message)
144
+
145
+ async def _write_bluetooth_json_message(
146
+ self,
147
+ data: dict[str, Any],
148
+ characteristic: str = SETTINGS_CHARACTERISTIC,
149
+ ) -> None:
150
+ """Write a json message to the machine."""
151
+
152
+ await self._write_bluetooth_message(
153
+ characteristic=characteristic,
154
+ message=json.dumps(data, separators=(",", ":")),
155
+ )
156
+
157
+ async def _authenticate(self) -> None:
158
+ """Build authentication string and send it to the machine."""
159
+
160
+ user = self._username + ":" + self._serial_number
161
+ user_bytes = user.encode("utf-8")
162
+ token = self._token.encode("utf-8")
163
+ auth_string = base64.b64encode(user_bytes) + b"@" + base64.b64encode(token)
164
+ await self._write_bluetooth_message(
165
+ characteristic=AUTH_CHARACTERISTIC,
166
+ message=auth_string,
167
+ )