python-yarbo 0.1.0__py3-none-any.whl
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.
- python_yarbo-0.1.0.dist-info/METADATA +306 -0
- python_yarbo-0.1.0.dist-info/RECORD +18 -0
- python_yarbo-0.1.0.dist-info/WHEEL +4 -0
- python_yarbo-0.1.0.dist-info/licenses/LICENSE +21 -0
- yarbo/__init__.py +122 -0
- yarbo/_codec.py +63 -0
- yarbo/auth.py +244 -0
- yarbo/client.py +695 -0
- yarbo/cloud.py +288 -0
- yarbo/cloud_mqtt.py +109 -0
- yarbo/const.py +197 -0
- yarbo/discovery.py +217 -0
- yarbo/error_reporting.py +83 -0
- yarbo/exceptions.py +112 -0
- yarbo/keys/README.md +41 -0
- yarbo/local.py +1636 -0
- yarbo/models.py +787 -0
- yarbo/mqtt.py +487 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-yarbo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library for local and cloud control of Yarbo robot mowers via MQTT
|
|
5
|
+
Project-URL: Homepage, https://github.com/markus-lassfolk/python-yarbo
|
|
6
|
+
Project-URL: Repository, https://github.com/markus-lassfolk/python-yarbo
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/markus-lassfolk/python-yarbo/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/markus-lassfolk/python-yarbo#readme
|
|
9
|
+
Project-URL: Changelog, https://github.com/markus-lassfolk/python-yarbo/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: yarbo-reversing, https://github.com/markus-lassfolk/yarbo-reversing
|
|
11
|
+
Author: Markus Lassfolk
|
|
12
|
+
License: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: iot,mower,mqtt,robot,smart-home,snow-blower,yarbo
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Home Automation
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: aiohttp>=3.9
|
|
26
|
+
Requires-Dist: paho-mqtt>=2.0
|
|
27
|
+
Requires-Dist: sentry-sdk>=2.0
|
|
28
|
+
Provides-Extra: cloud
|
|
29
|
+
Requires-Dist: cryptography>=42.0; extra == 'cloud'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: bandit[toml]>=1.7; extra == 'dev'
|
|
32
|
+
Requires-Dist: cryptography>=42.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
34
|
+
Requires-Dist: pip-audit>=2.7; extra == 'dev'
|
|
35
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
40
|
+
Requires-Dist: types-aiofiles; extra == 'dev'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# python-yarbo
|
|
44
|
+
|
|
45
|
+
[](https://github.com/markus-lassfolk/python-yarbo/actions/workflows/ci.yml)
|
|
46
|
+
[](https://pypi.org/project/python-yarbo/)
|
|
47
|
+
[](https://pypi.org/project/python-yarbo/)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
|
|
50
|
+
Python library for **local and cloud control** of [Yarbo](https://yarbo.com/) robot
|
|
51
|
+
mowers and snow blowers — built from reverse-engineered protocol knowledge.
|
|
52
|
+
|
|
53
|
+
> **Status**: Alpha (0.1.0) — local MQTT control is functional and confirmed working
|
|
54
|
+
> on hardware. Cloud API is partially functional (JWT auth migration in progress).
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- 🔌 **Local MQTT control** — no cloud account required
|
|
59
|
+
- 💡 **Full LED control** — 7 independent channels (head, fill, body, tail)
|
|
60
|
+
- 🔊 **Buzzer control**
|
|
61
|
+
- 🌨️ **Snow chute direction** (snow blower models)
|
|
62
|
+
- 📡 **Live telemetry stream** — battery, state, position, heading
|
|
63
|
+
- 🔍 **Auto-discovery** — finds Yarbo brokers on your local network
|
|
64
|
+
- ☁️ **Cloud API** — robot management, scheduling, notifications
|
|
65
|
+
- ⚡ **Async-first** — built on asyncio with sync wrappers for scripts
|
|
66
|
+
- 🏠 **Home Assistant ready** — see [`home-assistant-yarbo`](https://github.com/markus-lassfolk/home-assistant-yarbo) (coming soon)
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- Python ≥ 3.11
|
|
71
|
+
- Same WiFi network as the robot (for local control)
|
|
72
|
+
- `paho-mqtt` ≥ 2.0 (included)
|
|
73
|
+
- `aiohttp` ≥ 3.9 (included)
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install python-yarbo
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For cloud API features (RSA password encryption):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install "python-yarbo[cloud]"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick Start
|
|
88
|
+
|
|
89
|
+
### Async (recommended)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import asyncio
|
|
93
|
+
from yarbo import YarboClient
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
async with YarboClient(broker="192.168.1.24", sn="24400102L8HO5227") as client:
|
|
97
|
+
# Get a telemetry snapshot
|
|
98
|
+
status = await client.get_status()
|
|
99
|
+
if status:
|
|
100
|
+
print(f"Battery: {status.battery}% State: {status.state}")
|
|
101
|
+
|
|
102
|
+
# Light control
|
|
103
|
+
await client.lights_on()
|
|
104
|
+
await asyncio.sleep(2)
|
|
105
|
+
await client.lights_off()
|
|
106
|
+
|
|
107
|
+
# Buzzer
|
|
108
|
+
await client.buzzer(state=1)
|
|
109
|
+
|
|
110
|
+
# Live telemetry stream
|
|
111
|
+
async for telemetry in client.watch_telemetry():
|
|
112
|
+
print(f"Battery: {telemetry.battery}% Heading: {telemetry.heading}°")
|
|
113
|
+
if telemetry.battery and telemetry.battery < 20:
|
|
114
|
+
print("Low battery!")
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Sync (scripts / REPL)
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from yarbo import YarboClient
|
|
124
|
+
|
|
125
|
+
client = YarboClient.connect_sync(broker="192.168.1.24", sn="24400102L8HO5227")
|
|
126
|
+
client.lights_on()
|
|
127
|
+
client.buzzer()
|
|
128
|
+
client.disconnect()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Auto-discovery
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import asyncio
|
|
135
|
+
from yarbo import discover_yarbo, YarboClient
|
|
136
|
+
|
|
137
|
+
async def main():
|
|
138
|
+
print("Scanning for Yarbo robots...")
|
|
139
|
+
robots = await discover_yarbo()
|
|
140
|
+
|
|
141
|
+
if not robots:
|
|
142
|
+
print("No robots found")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
print(f"Found: {robots[0]}")
|
|
146
|
+
async with YarboClient(broker=robots[0].broker_host, sn=robots[0].sn) as client:
|
|
147
|
+
await client.lights_on()
|
|
148
|
+
|
|
149
|
+
asyncio.run(main())
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Cloud login (account management)
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
import asyncio
|
|
156
|
+
from yarbo import YarboCloudClient
|
|
157
|
+
|
|
158
|
+
async def main():
|
|
159
|
+
async with YarboCloudClient(
|
|
160
|
+
username="your@email.com",
|
|
161
|
+
password="yourpassword",
|
|
162
|
+
rsa_key_path="/path/to/rsa_public_key.pem", # from APK
|
|
163
|
+
) as client:
|
|
164
|
+
robots = await client.list_robots()
|
|
165
|
+
for robot in robots:
|
|
166
|
+
print(f"{robot.sn}: {robot.name} (online: {robot.is_online})")
|
|
167
|
+
|
|
168
|
+
version = await client.get_latest_version()
|
|
169
|
+
print(f"App: {version['appVersion']} Firmware: {version['firmwareVersion']}")
|
|
170
|
+
|
|
171
|
+
asyncio.run(main())
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## API Reference
|
|
175
|
+
|
|
176
|
+
### `YarboClient` (hybrid)
|
|
177
|
+
|
|
178
|
+
| Method | Description |
|
|
179
|
+
|--------|-------------|
|
|
180
|
+
| `async with YarboClient(broker, sn)` | Connect via async context manager |
|
|
181
|
+
| `await client.get_status()` | Single telemetry snapshot → `YarboTelemetry` |
|
|
182
|
+
| `await client.watch_telemetry()` | Async generator of `YarboTelemetry` |
|
|
183
|
+
| `await client.lights_on()` | All LEDs → 255 |
|
|
184
|
+
| `await client.lights_off()` | All LEDs → 0 |
|
|
185
|
+
| `await client.set_lights(YarboLightState)` | Per-channel LED control |
|
|
186
|
+
| `await client.buzzer(state=1)` | Buzzer on (1) or off (0) |
|
|
187
|
+
| `await client.set_chute(vel)` | Snow chute direction |
|
|
188
|
+
| `await client.get_controller()` | Acquire controller role (auto-called) |
|
|
189
|
+
| `await client.publish_raw(cmd, payload)` | Arbitrary MQTT command |
|
|
190
|
+
| `await client.list_robots()` | Cloud: bound robots |
|
|
191
|
+
| `YarboClient.connect_sync(broker, sn)` | Sync wrapper factory |
|
|
192
|
+
|
|
193
|
+
### `YarboLocalClient` (MQTT-only)
|
|
194
|
+
|
|
195
|
+
Same interface as `YarboClient`, local only, no cloud features.
|
|
196
|
+
|
|
197
|
+
### `YarboLightState`
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from yarbo import YarboLightState
|
|
201
|
+
|
|
202
|
+
# All on
|
|
203
|
+
state = YarboLightState.all_on()
|
|
204
|
+
|
|
205
|
+
# Custom
|
|
206
|
+
state = YarboLightState(
|
|
207
|
+
led_head=255, # Front white
|
|
208
|
+
led_left_w=128, # Left fill white
|
|
209
|
+
led_right_w=128, # Right fill white
|
|
210
|
+
body_left_r=255, # Left body red
|
|
211
|
+
body_right_r=255, # Right body red
|
|
212
|
+
tail_left_r=0, # Left tail red
|
|
213
|
+
tail_right_r=0, # Right tail red
|
|
214
|
+
)
|
|
215
|
+
async with YarboClient(...) as client:
|
|
216
|
+
await client.set_lights(state)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `YarboTelemetry`
|
|
220
|
+
|
|
221
|
+
Parsed from `DeviceMSG` nested schema (`BatteryMSG`, `StateMSG`, `RTKMSG`, `CombinedOdom`).
|
|
222
|
+
|
|
223
|
+
| Field | Type | Source | Description |
|
|
224
|
+
|-------|------|--------|-------------|
|
|
225
|
+
| `battery` | `int \| None` | `BatteryMSG.capacity` | State of charge (0–100 %) |
|
|
226
|
+
| `state` | `str \| None` | derived | `"idle"` or `"active"` |
|
|
227
|
+
| `working_state` | `int \| None` | `StateMSG.working_state` | Raw state (0=idle, 1=active) |
|
|
228
|
+
| `charging_status` | `int \| None` | `StateMSG.charging_status` | 2 = charging/docked |
|
|
229
|
+
| `error_code` | `int \| str \| None` | `StateMSG.error_code` | Active fault code |
|
|
230
|
+
| `heading` | `float \| None` | `RTKMSG.heading` | Compass heading (degrees) |
|
|
231
|
+
| `position_x` | `float \| None` | `CombinedOdom.x` | Odometry X (metres) |
|
|
232
|
+
| `position_y` | `float \| None` | `CombinedOdom.y` | Odometry Y (metres) |
|
|
233
|
+
| `phi` | `float \| None` | `CombinedOdom.phi` | Odometry heading (radians) |
|
|
234
|
+
| `speed` | `float \| None` | flat | Current speed (m/s) |
|
|
235
|
+
| `raw` | `dict` | — | Complete raw DeviceMSG dict |
|
|
236
|
+
|
|
237
|
+
## Cloud vs Local
|
|
238
|
+
|
|
239
|
+
| Feature | Local MQTT | Cloud REST |
|
|
240
|
+
|---------|-----------|------------|
|
|
241
|
+
| Robot control (lights, buzzer, …) | ✅ Yes | ❌ No |
|
|
242
|
+
| Live telemetry | ✅ Yes | ❌ No |
|
|
243
|
+
| List bound robots | ❌ No | ✅ Yes |
|
|
244
|
+
| Account management | ❌ No | ✅ Yes |
|
|
245
|
+
| Robot rename / bind / unbind | ❌ No | ✅ Yes |
|
|
246
|
+
| Notifications | ❌ No | ✅ Yes |
|
|
247
|
+
| Works offline | ✅ Yes | ❌ No |
|
|
248
|
+
| Requires cloud account | ❌ No | ✅ Yes |
|
|
249
|
+
|
|
250
|
+
> **⚠️ Cloud MQTT not implemented.** The Yarbo backend also provides a Tencent
|
|
251
|
+
> TDMQ MQTT broker (`mqtt-b8rkj5da-usw-public.mqtt.tencenttdmq.com:8883`) for
|
|
252
|
+
> remote control without LAN access. This library does **not** implement cloud
|
|
253
|
+
> MQTT — there is no remote-control fallback. All robot commands go via the
|
|
254
|
+
> local broker only.
|
|
255
|
+
|
|
256
|
+
## Security Notes
|
|
257
|
+
|
|
258
|
+
> ⚠️ **The Yarbo local MQTT broker accepts anonymous connections without
|
|
259
|
+
> authentication.** Anyone on your WiFi network can connect and send commands
|
|
260
|
+
> to your robot.
|
|
261
|
+
|
|
262
|
+
Recommendations:
|
|
263
|
+
- Keep the robot on a dedicated IoT VLAN and firewall it from the internet.
|
|
264
|
+
- Do **not** port-forward port 1883 to the internet.
|
|
265
|
+
- Consider a firewall rule that allows only your home automation host to reach
|
|
266
|
+
port 1883 on the robot's IP.
|
|
267
|
+
|
|
268
|
+
## Protocol Notes
|
|
269
|
+
|
|
270
|
+
This library was built from reverse-engineering the Yarbo Flutter app and
|
|
271
|
+
live packet captures. Key protocol facts:
|
|
272
|
+
|
|
273
|
+
- **MQTT broker**: Local EMQX at `192.168.1.24:1883` or `192.168.1.55:1883`
|
|
274
|
+
(check which IP your robot uses — both have been observed in production)
|
|
275
|
+
- **Payload encoding**: `zlib.compress(json.dumps(payload).encode())`
|
|
276
|
+
(exception: `heart_beat` topic uses plain uncompressed JSON)
|
|
277
|
+
- **Controller handshake**: `get_controller` must be sent before action commands
|
|
278
|
+
- **Topics**: `snowbot/{SN}/app/{cmd}` (publish) and
|
|
279
|
+
`snowbot/{SN}/device/{feedback}` (subscribe)
|
|
280
|
+
- **Telemetry topic**: `DeviceMSG` (~1–2 Hz) with nested schema:
|
|
281
|
+
`BatteryMSG.capacity`, `StateMSG.working_state`, `RTKMSG.heading`,
|
|
282
|
+
`CombinedOdom.x/y/phi`
|
|
283
|
+
- **Not yet implemented**: Local REST API (port 8088) and TCP JSON (port 22220)
|
|
284
|
+
are documented in `yarbo-reversing` but not implemented here
|
|
285
|
+
|
|
286
|
+
See [`yarbo-reversing`](https://github.com/markus-lassfolk/yarbo-reversing) for:
|
|
287
|
+
- Full [command catalogue](https://github.com/markus-lassfolk/yarbo-reversing/blob/main/docs/COMMAND_CATALOGUE.md)
|
|
288
|
+
- [Light control protocol](https://github.com/markus-lassfolk/yarbo-reversing/blob/main/docs/LIGHT_CTRL_PROTOCOL.md)
|
|
289
|
+
- [API endpoints](https://github.com/markus-lassfolk/yarbo-reversing/blob/main/docs/API_ENDPOINTS.md)
|
|
290
|
+
- [MQTT protocol reference](https://github.com/markus-lassfolk/yarbo-reversing/blob/main/docs/MQTT_PROTOCOL.md)
|
|
291
|
+
|
|
292
|
+
## Related Projects
|
|
293
|
+
|
|
294
|
+
| Project | Description |
|
|
295
|
+
|---------|-------------|
|
|
296
|
+
| [`yarbo-reversing`](https://github.com/markus-lassfolk/yarbo-reversing) | Protocol RE: Frida scripts, MITM setup, APK tools |
|
|
297
|
+
| [`PSYarbo`](https://github.com/markus-lassfolk/PSYarbo) | PowerShell module (same protocol, same architecture) |
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
MIT — see [LICENSE](LICENSE).
|
|
302
|
+
|
|
303
|
+
## Disclaimer
|
|
304
|
+
|
|
305
|
+
This library was built by reverse engineering. It is not affiliated with or endorsed by
|
|
306
|
+
Yarbo. Use at your own risk. Do not expose your robot's MQTT broker to the internet.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
yarbo/__init__.py,sha256=Hzz1uzSNFF_M7kGI7RVwgu3xASJr2dJ_LCzgy8elk7Y,3047
|
|
2
|
+
yarbo/_codec.py,sha256=iyfn34p-LeHWJrNaKHVX6rAMyRrYG43W8ip03Madnu0,1944
|
|
3
|
+
yarbo/auth.py,sha256=t6nP_DrKCWGNENimPR2VbK3X0Ifr4wpO8qdJJRJUE70,9413
|
|
4
|
+
yarbo/client.py,sha256=o2lV9P5NAVxXXw7Xd5zx5g3SUG2yS5mkQvnBlSYLhOc,27539
|
|
5
|
+
yarbo/cloud.py,sha256=DBwC7gm2nA0G0o1iPv2xWJV3vm68rHvkz5qUVG04r_8,10032
|
|
6
|
+
yarbo/cloud_mqtt.py,sha256=_8fiB9mA51fdaPltlHSmi8G52abvzV90WFB7R76mpN0,4260
|
|
7
|
+
yarbo/const.py,sha256=qf9SRZ8kWmZVoyvc4eargSqcu1lVYyJiyyNByYbuxd8,6896
|
|
8
|
+
yarbo/discovery.py,sha256=pMy1hIkC_GyyUTd-KFXbHEVtkRRu-awCog5aDDbS6VE,7368
|
|
9
|
+
yarbo/error_reporting.py,sha256=9GpLnPD0eDG5q9Ifitg2c0eqWLBisf1utSOJi9E0_VE,3195
|
|
10
|
+
yarbo/exceptions.py,sha256=UQ5dg48YTx4DlVGnDepP-FA9Jxt4BkV3v4Wh_PmBbas,3443
|
|
11
|
+
yarbo/local.py,sha256=Wp-M95hXVsh5GFgDkgR-Cw1bC2kzbGBAPxImEJcJP4A,59737
|
|
12
|
+
yarbo/models.py,sha256=PTuNj19OX7mZ-xzxEHzVrYGMc5n3dskUDIpYzhRKzL4,26033
|
|
13
|
+
yarbo/mqtt.py,sha256=mMlqpRV-lXN3_p28UtR0b4m46u4BX961QS5PvW5hwpg,20063
|
|
14
|
+
yarbo/keys/README.md,sha256=2vDNU2O66QNO5FwLIw6vfeWJrRg5DeyIA95gmmpdNmU,1150
|
|
15
|
+
python_yarbo-0.1.0.dist-info/METADATA,sha256=mQz2W1zkrB1RHblYTMUoQVrthsC3swnIWcn9GR7vnh8,11560
|
|
16
|
+
python_yarbo-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
17
|
+
python_yarbo-0.1.0.dist-info/licenses/LICENSE,sha256=hnpk3bWclcT3ci3znvjdHFo112rtbsgEUwccm75AqT8,1072
|
|
18
|
+
python_yarbo-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Markus Lassfolk
|
|
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.
|
yarbo/__init__.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
yarbo — Python library for local and cloud control of Yarbo robot mowers.
|
|
3
|
+
|
|
4
|
+
Yarbo makes autonomous snow blowers and lawn mowers controlled via local MQTT.
|
|
5
|
+
This library was built from reverse-engineering the Yarbo Flutter app and
|
|
6
|
+
probing the protocol with live hardware captures.
|
|
7
|
+
|
|
8
|
+
Quick start (async)::
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from yarbo import YarboClient
|
|
12
|
+
|
|
13
|
+
async def main():
|
|
14
|
+
async with YarboClient(broker="192.168.1.24", sn="24400102L8HO5227") as client:
|
|
15
|
+
status = await client.get_status()
|
|
16
|
+
print(f"Battery: {status.battery}%")
|
|
17
|
+
await client.lights_on()
|
|
18
|
+
await client.buzzer()
|
|
19
|
+
|
|
20
|
+
asyncio.run(main())
|
|
21
|
+
|
|
22
|
+
Quick start (sync)::
|
|
23
|
+
|
|
24
|
+
from yarbo import YarboClient
|
|
25
|
+
|
|
26
|
+
client = YarboClient.connect(broker="192.168.1.24", sn="24400102L8HO5227")
|
|
27
|
+
client.lights_on()
|
|
28
|
+
client.disconnect()
|
|
29
|
+
|
|
30
|
+
Auto-discovery::
|
|
31
|
+
|
|
32
|
+
import asyncio
|
|
33
|
+
from yarbo import discover_yarbo, YarboClient
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
robots = await discover_yarbo()
|
|
37
|
+
if robots:
|
|
38
|
+
async with YarboClient(broker=robots[0].broker_host, sn=robots[0].sn) as client:
|
|
39
|
+
await client.lights_on()
|
|
40
|
+
|
|
41
|
+
asyncio.run(main())
|
|
42
|
+
|
|
43
|
+
See README.md for full documentation.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
__version__ = "0.1.0"
|
|
49
|
+
__author__ = "Markus Lassfolk"
|
|
50
|
+
__license__ = "MIT"
|
|
51
|
+
|
|
52
|
+
from ._codec import decode, encode
|
|
53
|
+
from .client import YarboClient
|
|
54
|
+
from .cloud import YarboCloudClient
|
|
55
|
+
from .cloud_mqtt import YarboCloudMqttClient
|
|
56
|
+
from .const import Topic
|
|
57
|
+
from .discovery import DiscoveredRobot, discover_yarbo
|
|
58
|
+
from .error_reporting import init_error_reporting
|
|
59
|
+
from .exceptions import (
|
|
60
|
+
YarboAuthError,
|
|
61
|
+
YarboCommandError,
|
|
62
|
+
YarboConnectionError,
|
|
63
|
+
YarboError,
|
|
64
|
+
YarboNotControllerError,
|
|
65
|
+
YarboProtocolError,
|
|
66
|
+
YarboTimeoutError,
|
|
67
|
+
YarboTokenExpiredError,
|
|
68
|
+
)
|
|
69
|
+
from .local import YarboLocalClient
|
|
70
|
+
from .models import (
|
|
71
|
+
HeadType,
|
|
72
|
+
TelemetryEnvelope,
|
|
73
|
+
YarboCommandResult,
|
|
74
|
+
YarboLightState,
|
|
75
|
+
YarboPlan,
|
|
76
|
+
YarboPlanParams,
|
|
77
|
+
YarboRobot,
|
|
78
|
+
YarboSchedule,
|
|
79
|
+
YarboTelemetry,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = [ # noqa: RUF022 — grouped by category, alphabetical within each
|
|
83
|
+
# Version
|
|
84
|
+
"__version__",
|
|
85
|
+
# Codec helpers
|
|
86
|
+
"decode",
|
|
87
|
+
"encode",
|
|
88
|
+
# Error reporting
|
|
89
|
+
"init_error_reporting",
|
|
90
|
+
# Discovery
|
|
91
|
+
"DiscoveredRobot",
|
|
92
|
+
"discover_yarbo",
|
|
93
|
+
# Topic helper
|
|
94
|
+
"Topic",
|
|
95
|
+
# Models (alphabetical)
|
|
96
|
+
"HeadType",
|
|
97
|
+
"TelemetryEnvelope",
|
|
98
|
+
"YarboCommandResult",
|
|
99
|
+
"YarboLightState",
|
|
100
|
+
"YarboPlan",
|
|
101
|
+
"YarboPlanParams",
|
|
102
|
+
"YarboRobot",
|
|
103
|
+
"YarboSchedule",
|
|
104
|
+
"YarboTelemetry",
|
|
105
|
+
# Clients (alphabetical)
|
|
106
|
+
"YarboClient",
|
|
107
|
+
"YarboCloudClient",
|
|
108
|
+
"YarboCloudMqttClient",
|
|
109
|
+
"YarboLocalClient",
|
|
110
|
+
# Exceptions (alphabetical)
|
|
111
|
+
"YarboAuthError",
|
|
112
|
+
"YarboCommandError",
|
|
113
|
+
"YarboConnectionError",
|
|
114
|
+
"YarboError",
|
|
115
|
+
"YarboNotControllerError",
|
|
116
|
+
"YarboProtocolError",
|
|
117
|
+
"YarboTimeoutError",
|
|
118
|
+
"YarboTokenExpiredError",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
# Opt-out error reporting: enabled by default, disable via YARBO_SENTRY_DSN=""
|
|
122
|
+
init_error_reporting()
|
yarbo/_codec.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
yarbo._codec — zlib encode/decode helpers for MQTT payloads.
|
|
3
|
+
|
|
4
|
+
All MQTT payloads in the Yarbo protocol are zlib-compressed JSON.
|
|
5
|
+
The robot firmware checks the firmware version (>= 3.9.0, MIN_ZIP_MQTT_VERSION)
|
|
6
|
+
before decompressing. All current firmware versions use zlib compression.
|
|
7
|
+
|
|
8
|
+
Reference: Blutter ASM analysis of the Flutter app's MqttPublish class.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any, cast
|
|
15
|
+
import zlib
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def encode(payload: dict[str, Any]) -> bytes:
|
|
19
|
+
"""
|
|
20
|
+
Encode a Python dict to a zlib-compressed JSON byte string.
|
|
21
|
+
|
|
22
|
+
This is the wire format for ALL MQTT publishes to the Yarbo broker.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
payload: Dict to encode (must be JSON-serialisable).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Compressed bytes ready to publish.
|
|
29
|
+
|
|
30
|
+
Example::
|
|
31
|
+
|
|
32
|
+
from yarbo._codec import encode, decode
|
|
33
|
+
raw = encode({"led_head": 255, "led_left_w": 255})
|
|
34
|
+
assert decode(raw) == {"led_head": 255, "led_left_w": 255}
|
|
35
|
+
"""
|
|
36
|
+
return zlib.compress(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def decode(data: bytes) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Decode a zlib-compressed JSON byte string to a Python dict.
|
|
42
|
+
|
|
43
|
+
Falls back to plain JSON if decompression fails.
|
|
44
|
+
|
|
45
|
+
.. note:: ``heart_beat`` exception
|
|
46
|
+
The ``heart_beat`` topic delivers plain (uncompressed) JSON, e.g.
|
|
47
|
+
``{"working_state": 0}``. The zlib fallback path handles this
|
|
48
|
+
transparently — no special case is needed in callers.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
data: Raw bytes received from the MQTT broker.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Decoded dict. Returns ``{"_raw": data.hex()}`` on total failure.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
return cast("dict[str, Any]", json.loads(zlib.decompress(data)))
|
|
58
|
+
except (zlib.error, json.JSONDecodeError):
|
|
59
|
+
pass
|
|
60
|
+
try:
|
|
61
|
+
return cast("dict[str, Any]", json.loads(data))
|
|
62
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
63
|
+
return {"_raw": data[:512].hex()}
|