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.
Files changed (39) hide show
  1. {botslab360-0.7.0 → botslab360-0.7.2}/ATTRIBUTION.md +1 -1
  2. botslab360-0.7.2/PKG-INFO +295 -0
  3. botslab360-0.7.2/README.md +275 -0
  4. {botslab360-0.7.0 → botslab360-0.7.2}/pyproject.toml +1 -1
  5. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/auth.py +8 -0
  6. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/client.py +102 -33
  7. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/quc.py +8 -0
  8. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/rooms.py +29 -6
  9. botslab360-0.7.2/src/botslab360.egg-info/PKG-INFO +295 -0
  10. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/SOURCES.txt +1 -0
  11. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_auth.py +58 -0
  12. botslab360-0.7.2/tests/test_push_profiles.py +315 -0
  13. botslab360-0.7.0/PKG-INFO +0 -455
  14. botslab360-0.7.0/README.md +0 -435
  15. botslab360-0.7.0/src/botslab360.egg-info/PKG-INFO +0 -455
  16. {botslab360-0.7.0 → botslab360-0.7.2}/LICENSE +0 -0
  17. {botslab360-0.7.0 → botslab360-0.7.2}/setup.cfg +0 -0
  18. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/__init__.py +0 -0
  19. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/commands.py +0 -0
  20. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/exceptions.py +0 -0
  21. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/models.py +0 -0
  22. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360/protocol.py +0 -0
  23. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/dependency_links.txt +0 -0
  24. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/requires.txt +0 -0
  25. {botslab360-0.7.0 → botslab360-0.7.2}/src/botslab360.egg-info/top_level.txt +0 -0
  26. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_android_360_diagnostic.py +0 -0
  27. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_commands.py +0 -0
  28. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_compare_android_quc.py +0 -0
  29. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_credentials_auth_example.py +0 -0
  30. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_devices.py +0 -0
  31. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_network_info.py +0 -0
  32. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_network_info_diagnostic.py +0 -0
  33. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_protocol.py +0 -0
  34. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_public_robot360_auth_diagnostic.py +0 -0
  35. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_public_room_api_diagnostic.py +0 -0
  36. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_quc.py +0 -0
  37. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_room_diagnostic.py +0 -0
  38. {botslab360-0.7.0 → botslab360-0.7.2}/tests/test_rooms.py +0 -0
  39. {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 <tombox2020@gmail.com>
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
+ [![Validate](https://github.com/placix/python-botslab360/actions/workflows/validate.yml/badge.svg)](https://github.com/placix/python-botslab360/actions/workflows/validate.yml)
24
+ [![PyPI](https://img.shields.io/pypi/v/botslab360.svg)](https://pypi.org/project/botslab360/)
25
+ [![Python](https://img.shields.io/pypi/pyversions/botslab360.svg)](https://pypi.org/project/botslab360/)
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![Validate](https://github.com/placix/python-botslab360/actions/workflows/validate.yml/badge.svg)](https://github.com/placix/python-botslab360/actions/workflows/validate.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/botslab360.svg)](https://pypi.org/project/botslab360/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/botslab360.svg)](https://pypi.org/project/botslab360/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "botslab360"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "Async Python client for Botslab/360 robot vacuums"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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: