botslab360 0.7.0__tar.gz → 0.7.2__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.
- {botslab360-0.7.0 → botslab360-0.7.2}/ATTRIBUTION.md +1 -1
- botslab360-0.7.2/PKG-INFO +295 -0
- botslab360-0.7.2/README.md +275 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/pyproject.toml +1 -1
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/auth.py +8 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/client.py +102 -33
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/quc.py +8 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/rooms.py +29 -6
- botslab360-0.7.2/src/botslab360.egg-info/PKG-INFO +295 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/SOURCES.txt +1 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_auth.py +58 -0
- botslab360-0.7.2/tests/test_push_profiles.py +315 -0
- botslab360-0.7.0/PKG-INFO +0 -455
- botslab360-0.7.0/README.md +0 -435
- botslab360-0.7.0/src/botslab360.egg-info/PKG-INFO +0 -455
- {botslab360-0.7.0 → botslab360-0.7.2}/LICENSE +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/setup.cfg +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/__init__.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/commands.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/exceptions.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/models.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/protocol.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/dependency_links.txt +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/requires.txt +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/top_level.txt +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_android_360_diagnostic.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_commands.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_compare_android_quc.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_credentials_auth_example.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_devices.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_network_info.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_network_info_diagnostic.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_protocol.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_public_robot360_auth_diagnostic.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_public_room_api_diagnostic.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_quc.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_room_diagnostic.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_rooms.py +0 -0
- {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_status.py +0 -0
|
@@ -24,7 +24,7 @@ https://github.com/TA2k/ioBroker.botslab360
|
|
|
24
24
|
|
|
25
25
|
MIT License
|
|
26
26
|
|
|
27
|
-
Copyright (c) 2022 TA2k
|
|
27
|
+
Copyright (c) 2022 TA2k
|
|
28
28
|
|
|
29
29
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
30
30
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: botslab360
|
|
3
|
+
Version: 0.7.2
|
|
4
|
+
Summary: Async Python client for Botslab/360 robot vacuums
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/placix/python-botslab360
|
|
7
|
+
Project-URL: Issues, https://github.com/placix/python-botslab360/issues
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
License-File: ATTRIBUTION.md
|
|
15
|
+
Requires-Dist: cryptography>=42
|
|
16
|
+
Requires-Dist: httpx<1,>=0.27
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# botslab360
|
|
22
|
+
|
|
23
|
+
[](https://github.com/placix/python-botslab360/actions/workflows/validate.yml)
|
|
24
|
+
[](https://pypi.org/project/botslab360/)
|
|
25
|
+
[](https://pypi.org/project/botslab360/)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
`botslab360` is an asynchronous Python client for selected Botslab / 360 robot
|
|
29
|
+
vacuums. It provides a reusable protocol layer for applications such as the
|
|
30
|
+
[`home-assistant-botslab360`](https://github.com/placix/home-assistant-botslab360)
|
|
31
|
+
custom integration.
|
|
32
|
+
|
|
33
|
+
> [!WARNING]
|
|
34
|
+
> This project is unofficial, experimental, and not affiliated with Botslab,
|
|
35
|
+
> Qihoo 360, or 360 Smart Home.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Qihoo Q/T session authentication and optional headless email/password login;
|
|
40
|
+
- explicit Botslab / CloudSmart and 360Robot account backends;
|
|
41
|
+
- captcha continuation controlled by the calling application;
|
|
42
|
+
- Smart Home login, session handling, and one automatic SID refresh;
|
|
43
|
+
- device discovery and robot status retrieval;
|
|
44
|
+
- station network-information retrieval;
|
|
45
|
+
- TCP/push communication and AES decryption of push messages;
|
|
46
|
+
- room discovery from the current robot map;
|
|
47
|
+
- single-room and multi-room cleaning;
|
|
48
|
+
- start, pause, resume, return-to-dock, locate, and mop-only controls.
|
|
49
|
+
|
|
50
|
+
The current device-control and room features have been verified with a 360 S9-P
|
|
51
|
+
using the 360Robot backend. Other models may work but have not been verified.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Install the published package from PyPI:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install botslab360
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Python 3.10 or newer is required.
|
|
62
|
+
|
|
63
|
+
## Authentication
|
|
64
|
+
|
|
65
|
+
Existing integrations can authenticate with Qihoo 360 `Q` and `T` session
|
|
66
|
+
tokens. New applications can instead let the library obtain those tokens from
|
|
67
|
+
an email/password login. Botslab / CloudSmart and original 360Robot accounts
|
|
68
|
+
use separate backends and are never tried as automatic fallbacks.
|
|
69
|
+
|
|
70
|
+
For a Botslab / CloudSmart account, the regional backend is the default.
|
|
71
|
+
Omitting `region` selects `eu1`:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from botslab360 import AuthBackend, Botslab360Client, DeviceIdentity
|
|
75
|
+
|
|
76
|
+
identity = DeviceIdentity.generate()
|
|
77
|
+
client = Botslab360Client.from_credentials(
|
|
78
|
+
"user@example.com",
|
|
79
|
+
"YOUR_PASSWORD",
|
|
80
|
+
backend=AuthBackend.BOTSLAB,
|
|
81
|
+
region="eu1",
|
|
82
|
+
device_identity=identity,
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For an account created in the original 360Robot application, select the
|
|
87
|
+
non-regional backend. Supplying `region` for this backend is rejected:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
client = Botslab360Client.from_credentials(
|
|
91
|
+
"user@example.com",
|
|
92
|
+
"YOUR_PASSWORD",
|
|
93
|
+
backend=AuthBackend.ROBOT360,
|
|
94
|
+
device_identity=identity,
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Persist the generated identity's `mid`, `android_id`, and `m2` values in secure
|
|
99
|
+
application configuration and reconstruct the same `DeviceIdentity` for later
|
|
100
|
+
logins. These identifiers are not account secrets, but should not be rotated on
|
|
101
|
+
every login.
|
|
102
|
+
|
|
103
|
+
The library does not solve captchas automatically. `authenticate()` raises
|
|
104
|
+
`CaptchaRequired` with image bytes and a challenge. Present the image to the
|
|
105
|
+
user, collect the code without logging it, and continue explicitly:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from botslab360 import CaptchaRequired
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
session = await client.authenticate()
|
|
112
|
+
except CaptchaRequired as exc:
|
|
113
|
+
show_captcha_to_user(exc.challenge.image)
|
|
114
|
+
captcha_code = await read_captcha_code_without_logging()
|
|
115
|
+
session = await client.continue_authentication(exc.challenge, captcha_code)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Both successful authentication methods return a ready `SmartSession`; callers
|
|
119
|
+
do not need to handle Q, T, or qid themselves.
|
|
120
|
+
|
|
121
|
+
## Basic usage
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import asyncio
|
|
125
|
+
|
|
126
|
+
from botslab360 import Botslab360Client
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def main() -> None:
|
|
130
|
+
q = "YOUR_Q_TOKEN"
|
|
131
|
+
t = "YOUR_T_TOKEN"
|
|
132
|
+
|
|
133
|
+
async with Botslab360Client(q, t) as client:
|
|
134
|
+
await client.authenticate()
|
|
135
|
+
|
|
136
|
+
devices = await client.get_devices()
|
|
137
|
+
robot = devices[0]
|
|
138
|
+
status = await client.get_status(robot.id)
|
|
139
|
+
|
|
140
|
+
print(robot.name, robot.model)
|
|
141
|
+
print(f"Battery: {status.battery}%")
|
|
142
|
+
print(f"State: {status.state}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
asyncio.run(main())
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Real applications should load credentials from secure configuration or
|
|
149
|
+
environment-specific secret storage instead of hard-coding them.
|
|
150
|
+
|
|
151
|
+
## Robot control
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
async with Botslab360Client(q, t) as client:
|
|
155
|
+
await client.authenticate()
|
|
156
|
+
robot = (await client.get_devices())[0]
|
|
157
|
+
|
|
158
|
+
await client.start_cleaning(robot)
|
|
159
|
+
await client.pause(robot)
|
|
160
|
+
await client.resume(robot)
|
|
161
|
+
await client.return_to_dock(robot)
|
|
162
|
+
await client.locate(robot)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Mop-only mode is also available. Disabling it selects sweep, or sweep and mop
|
|
166
|
+
when wiping hardware is installed; enabling it selects mop only:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
await client.set_mop_only(robot, False)
|
|
170
|
+
await client.set_mop_only(robot, True)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The known status payload does not report the current mop-only switch. The
|
|
174
|
+
library therefore does not invent a synchronized state or derived cleaning
|
|
175
|
+
mode. Water level remains an independent room setting.
|
|
176
|
+
|
|
177
|
+
## Room cleaning
|
|
178
|
+
|
|
179
|
+
Room cleaning fetches the current map before validating and sending a
|
|
180
|
+
selection. Room IDs belong to a particular device and map; always read the
|
|
181
|
+
current room list rather than hard-coding them.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
rooms = await client.get_rooms(robot)
|
|
185
|
+
|
|
186
|
+
for room in rooms:
|
|
187
|
+
print(room.id, room.name, room.vertices)
|
|
188
|
+
|
|
189
|
+
await client.clean_rooms(robot, [1, 6])
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Verified per-run settings can override pass count, suction, and water level for
|
|
193
|
+
selected rooms:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from botslab360 import RoomCleaningSettings, RoomFanMode, RoomWaterLevel
|
|
197
|
+
|
|
198
|
+
await client.clean_rooms(
|
|
199
|
+
robot,
|
|
200
|
+
[1],
|
|
201
|
+
room_settings={
|
|
202
|
+
1: RoomCleaningSettings(
|
|
203
|
+
clean_times=2,
|
|
204
|
+
fan_mode=RoomFanMode.STRONG,
|
|
205
|
+
water_pump=RoomWaterLevel.MEDIUM,
|
|
206
|
+
)
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Omitted settings retain their freshly fetched vendor values. The supported
|
|
212
|
+
suction values are `quiet`, `auto`, `strong`, and `max`; water levels are `1`,
|
|
213
|
+
`2`, and `3`. An existing `waterPump=0` is preserved when not overridden, but
|
|
214
|
+
is not exposed as an off choice because that meaning is unverified.
|
|
215
|
+
|
|
216
|
+
`Room.mode` preserves the optional raw `SweepArea.mode` string. It is a
|
|
217
|
+
carpet-related value, not a verified room sweep/mop selector. New code must not
|
|
218
|
+
use the deprecated `RoomCleaningMode` compatibility enum or
|
|
219
|
+
`RoomCleaningSettings.mode`; attempts to set the latter are rejected.
|
|
220
|
+
|
|
221
|
+
## Status and network information
|
|
222
|
+
|
|
223
|
+
`RobotStatus` exposes common values such as battery level, robot and charging
|
|
224
|
+
state, fan mode, cleaned area, cleaning time, error code, online state, and raw
|
|
225
|
+
wiping-assembly status. It also carries optional raw diagnostic counters,
|
|
226
|
+
states, map position, heading, timer status, and auto-boost state. Values with
|
|
227
|
+
unknown units or meanings remain raw, and missing fields remain `None`.
|
|
228
|
+
|
|
229
|
+
Station network identity is available separately:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
network_info = await client.get_network_info(robot)
|
|
233
|
+
|
|
234
|
+
print(network_info.station_ip)
|
|
235
|
+
print(network_info.station_mac)
|
|
236
|
+
print(network_info.station_signal)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
MAC addresses are normalized to lowercase colon-separated form. Treat SSIDs as
|
|
240
|
+
potentially sensitive when displaying or logging `network_info.station_ssid`.
|
|
241
|
+
|
|
242
|
+
## Session handling and security
|
|
243
|
+
|
|
244
|
+
The authentication chain is:
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
Q + T → qid → Smart Home login → sid + pushKey → device communication
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If the Smart Home SID expires, the library retries authentication once with the
|
|
251
|
+
current Q/T tokens. If the underlying Qihoo session is invalid, the caller must
|
|
252
|
+
provide fresh credentials. The email/password path obtains Q/T first and then
|
|
253
|
+
uses this same Smart Home login path; the newer signed `/v1` API is not used.
|
|
254
|
+
|
|
255
|
+
Treat passwords, captcha codes, Q, T, qid, sid, and push keys as secrets. Never
|
|
256
|
+
include real credentials in bug reports, screenshots, logs, test fixtures, or
|
|
257
|
+
Git commits.
|
|
258
|
+
|
|
259
|
+
## Known limitations
|
|
260
|
+
|
|
261
|
+
- Only the 360Robot S9-P has been verified with the current control and room
|
|
262
|
+
functionality.
|
|
263
|
+
- Captcha challenges require interaction by the calling application.
|
|
264
|
+
- Several diagnostic values intentionally remain raw because their physical
|
|
265
|
+
units or model-independent meanings are not established.
|
|
266
|
+
- Room polygons use vendor map coordinates, and malformed or missing polygons
|
|
267
|
+
are exposed as `None`.
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
git clone https://github.com/placix/python-botslab360.git
|
|
273
|
+
cd python-botslab360
|
|
274
|
+
python -m venv .venv
|
|
275
|
+
python -m pip install -e ".[test]"
|
|
276
|
+
python -m pip install build ruff twine
|
|
277
|
+
|
|
278
|
+
ruff check .
|
|
279
|
+
ruff format --check .
|
|
280
|
+
python -m pytest -p no:cacheprovider
|
|
281
|
+
python -m compileall -q src tests diagnostics
|
|
282
|
+
python -m build
|
|
283
|
+
python -m twine check dist/*
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Activate the virtual environment before installing dependencies when desired.
|
|
287
|
+
Files under `diagnostics/` are development and protocol-verification tools;
|
|
288
|
+
applications should use the public `Botslab360Client` API instead.
|
|
289
|
+
|
|
290
|
+
This independent project builds in part on protocol research by
|
|
291
|
+
[TA2k](https://github.com/TA2k) in
|
|
292
|
+
[ioBroker.botslab360](https://github.com/TA2k/ioBroker.botslab360). See
|
|
293
|
+
[ATTRIBUTION.md](ATTRIBUTION.md) for details.
|
|
294
|
+
|
|
295
|
+
Version `0.7.1` is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# botslab360
|
|
2
|
+
|
|
3
|
+
[](https://github.com/placix/python-botslab360/actions/workflows/validate.yml)
|
|
4
|
+
[](https://pypi.org/project/botslab360/)
|
|
5
|
+
[](https://pypi.org/project/botslab360/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
`botslab360` is an asynchronous Python client for selected Botslab / 360 robot
|
|
9
|
+
vacuums. It provides a reusable protocol layer for applications such as the
|
|
10
|
+
[`home-assistant-botslab360`](https://github.com/placix/home-assistant-botslab360)
|
|
11
|
+
custom integration.
|
|
12
|
+
|
|
13
|
+
> [!WARNING]
|
|
14
|
+
> This project is unofficial, experimental, and not affiliated with Botslab,
|
|
15
|
+
> Qihoo 360, or 360 Smart Home.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Qihoo Q/T session authentication and optional headless email/password login;
|
|
20
|
+
- explicit Botslab / CloudSmart and 360Robot account backends;
|
|
21
|
+
- captcha continuation controlled by the calling application;
|
|
22
|
+
- Smart Home login, session handling, and one automatic SID refresh;
|
|
23
|
+
- device discovery and robot status retrieval;
|
|
24
|
+
- station network-information retrieval;
|
|
25
|
+
- TCP/push communication and AES decryption of push messages;
|
|
26
|
+
- room discovery from the current robot map;
|
|
27
|
+
- single-room and multi-room cleaning;
|
|
28
|
+
- start, pause, resume, return-to-dock, locate, and mop-only controls.
|
|
29
|
+
|
|
30
|
+
The current device-control and room features have been verified with a 360 S9-P
|
|
31
|
+
using the 360Robot backend. Other models may work but have not been verified.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install the published package from PyPI:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install botslab360
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python 3.10 or newer is required.
|
|
42
|
+
|
|
43
|
+
## Authentication
|
|
44
|
+
|
|
45
|
+
Existing integrations can authenticate with Qihoo 360 `Q` and `T` session
|
|
46
|
+
tokens. New applications can instead let the library obtain those tokens from
|
|
47
|
+
an email/password login. Botslab / CloudSmart and original 360Robot accounts
|
|
48
|
+
use separate backends and are never tried as automatic fallbacks.
|
|
49
|
+
|
|
50
|
+
For a Botslab / CloudSmart account, the regional backend is the default.
|
|
51
|
+
Omitting `region` selects `eu1`:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from botslab360 import AuthBackend, Botslab360Client, DeviceIdentity
|
|
55
|
+
|
|
56
|
+
identity = DeviceIdentity.generate()
|
|
57
|
+
client = Botslab360Client.from_credentials(
|
|
58
|
+
"user@example.com",
|
|
59
|
+
"YOUR_PASSWORD",
|
|
60
|
+
backend=AuthBackend.BOTSLAB,
|
|
61
|
+
region="eu1",
|
|
62
|
+
device_identity=identity,
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For an account created in the original 360Robot application, select the
|
|
67
|
+
non-regional backend. Supplying `region` for this backend is rejected:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
client = Botslab360Client.from_credentials(
|
|
71
|
+
"user@example.com",
|
|
72
|
+
"YOUR_PASSWORD",
|
|
73
|
+
backend=AuthBackend.ROBOT360,
|
|
74
|
+
device_identity=identity,
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Persist the generated identity's `mid`, `android_id`, and `m2` values in secure
|
|
79
|
+
application configuration and reconstruct the same `DeviceIdentity` for later
|
|
80
|
+
logins. These identifiers are not account secrets, but should not be rotated on
|
|
81
|
+
every login.
|
|
82
|
+
|
|
83
|
+
The library does not solve captchas automatically. `authenticate()` raises
|
|
84
|
+
`CaptchaRequired` with image bytes and a challenge. Present the image to the
|
|
85
|
+
user, collect the code without logging it, and continue explicitly:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from botslab360 import CaptchaRequired
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
session = await client.authenticate()
|
|
92
|
+
except CaptchaRequired as exc:
|
|
93
|
+
show_captcha_to_user(exc.challenge.image)
|
|
94
|
+
captcha_code = await read_captcha_code_without_logging()
|
|
95
|
+
session = await client.continue_authentication(exc.challenge, captcha_code)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Both successful authentication methods return a ready `SmartSession`; callers
|
|
99
|
+
do not need to handle Q, T, or qid themselves.
|
|
100
|
+
|
|
101
|
+
## Basic usage
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import asyncio
|
|
105
|
+
|
|
106
|
+
from botslab360 import Botslab360Client
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def main() -> None:
|
|
110
|
+
q = "YOUR_Q_TOKEN"
|
|
111
|
+
t = "YOUR_T_TOKEN"
|
|
112
|
+
|
|
113
|
+
async with Botslab360Client(q, t) as client:
|
|
114
|
+
await client.authenticate()
|
|
115
|
+
|
|
116
|
+
devices = await client.get_devices()
|
|
117
|
+
robot = devices[0]
|
|
118
|
+
status = await client.get_status(robot.id)
|
|
119
|
+
|
|
120
|
+
print(robot.name, robot.model)
|
|
121
|
+
print(f"Battery: {status.battery}%")
|
|
122
|
+
print(f"State: {status.state}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
asyncio.run(main())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Real applications should load credentials from secure configuration or
|
|
129
|
+
environment-specific secret storage instead of hard-coding them.
|
|
130
|
+
|
|
131
|
+
## Robot control
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
async with Botslab360Client(q, t) as client:
|
|
135
|
+
await client.authenticate()
|
|
136
|
+
robot = (await client.get_devices())[0]
|
|
137
|
+
|
|
138
|
+
await client.start_cleaning(robot)
|
|
139
|
+
await client.pause(robot)
|
|
140
|
+
await client.resume(robot)
|
|
141
|
+
await client.return_to_dock(robot)
|
|
142
|
+
await client.locate(robot)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Mop-only mode is also available. Disabling it selects sweep, or sweep and mop
|
|
146
|
+
when wiping hardware is installed; enabling it selects mop only:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
await client.set_mop_only(robot, False)
|
|
150
|
+
await client.set_mop_only(robot, True)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The known status payload does not report the current mop-only switch. The
|
|
154
|
+
library therefore does not invent a synchronized state or derived cleaning
|
|
155
|
+
mode. Water level remains an independent room setting.
|
|
156
|
+
|
|
157
|
+
## Room cleaning
|
|
158
|
+
|
|
159
|
+
Room cleaning fetches the current map before validating and sending a
|
|
160
|
+
selection. Room IDs belong to a particular device and map; always read the
|
|
161
|
+
current room list rather than hard-coding them.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
rooms = await client.get_rooms(robot)
|
|
165
|
+
|
|
166
|
+
for room in rooms:
|
|
167
|
+
print(room.id, room.name, room.vertices)
|
|
168
|
+
|
|
169
|
+
await client.clean_rooms(robot, [1, 6])
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Verified per-run settings can override pass count, suction, and water level for
|
|
173
|
+
selected rooms:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from botslab360 import RoomCleaningSettings, RoomFanMode, RoomWaterLevel
|
|
177
|
+
|
|
178
|
+
await client.clean_rooms(
|
|
179
|
+
robot,
|
|
180
|
+
[1],
|
|
181
|
+
room_settings={
|
|
182
|
+
1: RoomCleaningSettings(
|
|
183
|
+
clean_times=2,
|
|
184
|
+
fan_mode=RoomFanMode.STRONG,
|
|
185
|
+
water_pump=RoomWaterLevel.MEDIUM,
|
|
186
|
+
)
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Omitted settings retain their freshly fetched vendor values. The supported
|
|
192
|
+
suction values are `quiet`, `auto`, `strong`, and `max`; water levels are `1`,
|
|
193
|
+
`2`, and `3`. An existing `waterPump=0` is preserved when not overridden, but
|
|
194
|
+
is not exposed as an off choice because that meaning is unverified.
|
|
195
|
+
|
|
196
|
+
`Room.mode` preserves the optional raw `SweepArea.mode` string. It is a
|
|
197
|
+
carpet-related value, not a verified room sweep/mop selector. New code must not
|
|
198
|
+
use the deprecated `RoomCleaningMode` compatibility enum or
|
|
199
|
+
`RoomCleaningSettings.mode`; attempts to set the latter are rejected.
|
|
200
|
+
|
|
201
|
+
## Status and network information
|
|
202
|
+
|
|
203
|
+
`RobotStatus` exposes common values such as battery level, robot and charging
|
|
204
|
+
state, fan mode, cleaned area, cleaning time, error code, online state, and raw
|
|
205
|
+
wiping-assembly status. It also carries optional raw diagnostic counters,
|
|
206
|
+
states, map position, heading, timer status, and auto-boost state. Values with
|
|
207
|
+
unknown units or meanings remain raw, and missing fields remain `None`.
|
|
208
|
+
|
|
209
|
+
Station network identity is available separately:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
network_info = await client.get_network_info(robot)
|
|
213
|
+
|
|
214
|
+
print(network_info.station_ip)
|
|
215
|
+
print(network_info.station_mac)
|
|
216
|
+
print(network_info.station_signal)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
MAC addresses are normalized to lowercase colon-separated form. Treat SSIDs as
|
|
220
|
+
potentially sensitive when displaying or logging `network_info.station_ssid`.
|
|
221
|
+
|
|
222
|
+
## Session handling and security
|
|
223
|
+
|
|
224
|
+
The authentication chain is:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
Q + T → qid → Smart Home login → sid + pushKey → device communication
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
If the Smart Home SID expires, the library retries authentication once with the
|
|
231
|
+
current Q/T tokens. If the underlying Qihoo session is invalid, the caller must
|
|
232
|
+
provide fresh credentials. The email/password path obtains Q/T first and then
|
|
233
|
+
uses this same Smart Home login path; the newer signed `/v1` API is not used.
|
|
234
|
+
|
|
235
|
+
Treat passwords, captcha codes, Q, T, qid, sid, and push keys as secrets. Never
|
|
236
|
+
include real credentials in bug reports, screenshots, logs, test fixtures, or
|
|
237
|
+
Git commits.
|
|
238
|
+
|
|
239
|
+
## Known limitations
|
|
240
|
+
|
|
241
|
+
- Only the 360Robot S9-P has been verified with the current control and room
|
|
242
|
+
functionality.
|
|
243
|
+
- Captcha challenges require interaction by the calling application.
|
|
244
|
+
- Several diagnostic values intentionally remain raw because their physical
|
|
245
|
+
units or model-independent meanings are not established.
|
|
246
|
+
- Room polygons use vendor map coordinates, and malformed or missing polygons
|
|
247
|
+
are exposed as `None`.
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
git clone https://github.com/placix/python-botslab360.git
|
|
253
|
+
cd python-botslab360
|
|
254
|
+
python -m venv .venv
|
|
255
|
+
python -m pip install -e ".[test]"
|
|
256
|
+
python -m pip install build ruff twine
|
|
257
|
+
|
|
258
|
+
ruff check .
|
|
259
|
+
ruff format --check .
|
|
260
|
+
python -m pytest -p no:cacheprovider
|
|
261
|
+
python -m compileall -q src tests diagnostics
|
|
262
|
+
python -m build
|
|
263
|
+
python -m twine check dist/*
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Activate the virtual environment before installing dependencies when desired.
|
|
267
|
+
Files under `diagnostics/` are development and protocol-verification tools;
|
|
268
|
+
applications should use the public `Botslab360Client` API instead.
|
|
269
|
+
|
|
270
|
+
This independent project builds in part on protocol research by
|
|
271
|
+
[TA2k](https://github.com/TA2k) in
|
|
272
|
+
[ioBroker.botslab360](https://github.com/TA2k/ioBroker.botslab360). See
|
|
273
|
+
[ATTRIBUTION.md](ATTRIBUTION.md) for details.
|
|
274
|
+
|
|
275
|
+
Version `0.7.1` is licensed under the [MIT License](LICENSE).
|
|
@@ -162,10 +162,12 @@ class BotslabAuth:
|
|
|
162
162
|
*,
|
|
163
163
|
base_url: str = SMART_HOME_BASE_URL,
|
|
164
164
|
language: str = "de_DE",
|
|
165
|
+
timeout: float | None = None,
|
|
165
166
|
) -> None:
|
|
166
167
|
self._http_client = http_client
|
|
167
168
|
self._base_url = base_url.rstrip("/")
|
|
168
169
|
self._language = language
|
|
170
|
+
self._request_timeout = timeout
|
|
169
171
|
|
|
170
172
|
async def login(
|
|
171
173
|
self,
|
|
@@ -240,10 +242,16 @@ class BotslabAuth:
|
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
try:
|
|
245
|
+
request_options = (
|
|
246
|
+
{}
|
|
247
|
+
if self._request_timeout is None
|
|
248
|
+
else {"timeout": self._request_timeout}
|
|
249
|
+
)
|
|
243
250
|
response = await self._http_client.post(
|
|
244
251
|
f"{self._base_url}{SMART_LOGIN_PATH}",
|
|
245
252
|
data=form,
|
|
246
253
|
headers=headers,
|
|
254
|
+
**request_options,
|
|
247
255
|
)
|
|
248
256
|
response.raise_for_status()
|
|
249
257
|
except httpx.HTTPStatusError as exc:
|