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.
- pylamarzocco-1.2.3/LICENSE +21 -0
- pylamarzocco-1.2.3/PKG-INFO +118 -0
- pylamarzocco-1.2.3/README.md +97 -0
- pylamarzocco-1.2.3/pylamarzocco/__init__.py +7 -0
- pylamarzocco-1.2.3/pylamarzocco/client_bluetooth.py +167 -0
- pylamarzocco-1.2.3/pylamarzocco/client_cloud.py +481 -0
- pylamarzocco-1.2.3/pylamarzocco/client_local.py +130 -0
- pylamarzocco-1.2.3/pylamarzocco/const.py +105 -0
- pylamarzocco-1.2.3/pylamarzocco/exceptions.py +21 -0
- pylamarzocco-1.2.3/pylamarzocco/helpers.py +211 -0
- pylamarzocco-1.2.3/pylamarzocco/lm_device.py +199 -0
- pylamarzocco-1.2.3/pylamarzocco/lm_grinder.py +64 -0
- pylamarzocco-1.2.3/pylamarzocco/lm_machine.py +530 -0
- pylamarzocco-1.2.3/pylamarzocco/models.py +194 -0
- pylamarzocco-1.2.3/pylamarzocco/py.typed +0 -0
- pylamarzocco-1.2.3/pylamarzocco.egg-info/PKG-INFO +118 -0
- pylamarzocco-1.2.3/pylamarzocco.egg-info/SOURCES.txt +24 -0
- pylamarzocco-1.2.3/pylamarzocco.egg-info/dependency_links.txt +1 -0
- pylamarzocco-1.2.3/pylamarzocco.egg-info/requires.txt +3 -0
- pylamarzocco-1.2.3/pylamarzocco.egg-info/top_level.txt +2 -0
- pylamarzocco-1.2.3/setup.cfg +4 -0
- pylamarzocco-1.2.3/setup.py +36 -0
- pylamarzocco-1.2.3/tests/__init__.py +41 -0
- pylamarzocco-1.2.3/tests/conftest.py +115 -0
- pylamarzocco-1.2.3/tests/test_grinder.py +27 -0
- pylamarzocco-1.2.3/tests/test_machine.py +194 -0
|
@@ -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
|
+

|
|
28
|
+
[](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
|
+

|
|
7
|
+
[](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
|
+
)
|