Skip to content

Commit 1c153f8

Browse files
committed
Merge upstream/main into add-qrevo-s5v-dock-type
2 parents 0cfa87f + c01e302 commit 1c153f8

14 files changed

Lines changed: 334 additions & 13 deletions

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22

33
<!-- version list -->
44

5+
## v5.11.0 (2026-05-12)
6+
7+
### Features
8+
9+
- Add null option to ZeoMode and ZeoProgram enums
10+
([#823](https://github.com/Python-roborock/python-roborock/pull/823),
11+
[`905f916`](https://github.com/Python-roborock/python-roborock/commit/905f91686cea233ff914a607dcd21e8f7489d108))
12+
13+
- Add Saros 20 dock type code (27) to RoborockDockTypeCode
14+
([#830](https://github.com/Python-roborock/python-roborock/pull/830),
15+
[`6c6c396`](https://github.com/Python-roborock/python-roborock/commit/6c6c39658300139b13af9d47c77f953fc433ab42))
16+
17+
- Add saros20 ([#830](https://github.com/Python-roborock/python-roborock/pull/830),
18+
[`6c6c396`](https://github.com/Python-roborock/python-roborock/commit/6c6c39658300139b13af9d47c77f953fc433ab42))
19+
20+
- Add some new Zeo code mappings
21+
([#823](https://github.com/Python-roborock/python-roborock/pull/823),
22+
[`905f916`](https://github.com/Python-roborock/python-roborock/commit/905f91686cea233ff914a607dcd21e8f7489d108))
23+
24+
25+
## v5.10.1 (2026-05-12)
26+
27+
### Bug Fixes
28+
29+
- Handle Web API unauthorized errors
30+
([#825](https://github.com/Python-roborock/python-roborock/pull/825),
31+
[`ad8d8f0`](https://github.com/Python-roborock/python-roborock/commit/ad8d8f095a260ee720c3e0193bcba5bd691c9f96))
32+
33+
- Mark non vacuum v1 devices as not supported
34+
([#828](https://github.com/Python-roborock/python-roborock/pull/828),
35+
[`2e9a848`](https://github.com/Python-roborock/python-roborock/commit/2e9a84807f5d0c2cffb5b0b6592b9baa6ee14c64))
36+
37+
538
## v5.10.0 (2026-05-03)
639

740
### Features

device_info.yaml

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,3 +1196,155 @@ roborock.vacuum.sc05:
11961196
mode: rw
11971197
type: RAW
11981198
property: 'null'
1199+
roborock.vacuum.a288:
1200+
protocol_version: '1.0'
1201+
product_nickname: PEARLPLUS
1202+
new_feature_info: 4499197267967999
1203+
new_feature_info_str: 0000000000099518CCFF7EFDA8E93EDDDBFF8F7F7EFEFFFF
1204+
feature_info:
1205+
- 111
1206+
- 112
1207+
- 113
1208+
- 114
1209+
- 115
1210+
- 116
1211+
- 117
1212+
- 118
1213+
- 119
1214+
- 120
1215+
- 121
1216+
- 122
1217+
- 123
1218+
- 124
1219+
- 125
1220+
product:
1221+
id: 3eizObJLFvPAxXvdW96ftd
1222+
name: Saros 20
1223+
model: roborock.vacuum.a288
1224+
category: robot.vacuum.cleaner
1225+
capability: 0
1226+
schema:
1227+
- id: 101
1228+
name: rpc_request
1229+
code: rpc_request
1230+
mode: rw
1231+
type: RAW
1232+
- id: 102
1233+
name: rpc_response
1234+
code: rpc_response
1235+
mode: rw
1236+
type: RAW
1237+
- id: 120
1238+
name: "\u9519\u8BEF\u4EE3\u7801"
1239+
code: error_code
1240+
mode: ro
1241+
type: ENUM
1242+
property: '{"range": [""]}'
1243+
- id: 121
1244+
name: "\u8BBE\u5907\u72B6\u6001"
1245+
code: state
1246+
mode: ro
1247+
type: ENUM
1248+
property: '{"range": [""]}'
1249+
- id: 122
1250+
name: "\u8BBE\u5907\u7535\u91CF"
1251+
code: battery
1252+
mode: ro
1253+
type: ENUM
1254+
property: '{"range": [""]}'
1255+
- id: 123
1256+
name: "\u6E05\u626B\u6A21\u5F0F"
1257+
code: fan_power
1258+
mode: rw
1259+
type: ENUM
1260+
property: '{"range": [""]}'
1261+
- id: 124
1262+
name: "\u62D6\u5730\u6A21\u5F0F"
1263+
code: water_box_mode
1264+
mode: rw
1265+
type: ENUM
1266+
property: '{"range": [""]}'
1267+
- id: 125
1268+
name: "\u4E3B\u5237\u5BFF\u547D"
1269+
code: main_brush_life
1270+
mode: rw
1271+
type: VALUE
1272+
property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}'
1273+
- id: 126
1274+
name: "\u8FB9\u5237\u5BFF\u547D"
1275+
code: side_brush_life
1276+
mode: rw
1277+
type: VALUE
1278+
property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}'
1279+
- id: 127
1280+
name: "\u6EE4\u7F51\u5BFF\u547D"
1281+
code: filter_life
1282+
mode: rw
1283+
type: VALUE
1284+
property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}'
1285+
- id: 128
1286+
name: "\u989D\u5916\u72B6\u6001"
1287+
code: additional_props
1288+
mode: ro
1289+
type: RAW
1290+
- id: 130
1291+
name: "\u5B8C\u6210\u4E8B\u4EF6"
1292+
code: task_complete
1293+
mode: ro
1294+
type: RAW
1295+
- id: 131
1296+
name: "\u7535\u91CF\u4E0D\u8DB3\u4EFB\u52A1\u53D6\u6D88"
1297+
code: task_cancel_low_power
1298+
mode: ro
1299+
type: RAW
1300+
- id: 132
1301+
name: "\u8FD0\u52A8\u4E2D\u4EFB\u52A1\u53D6\u6D88"
1302+
code: task_cancel_in_motion
1303+
mode: ro
1304+
type: RAW
1305+
- id: 133
1306+
name: "\u5145\u7535\u72B6\u6001"
1307+
code: charge_status
1308+
mode: ro
1309+
type: RAW
1310+
- id: 134
1311+
name: "\u70D8\u5E72\u72B6\u6001"
1312+
code: drying_status
1313+
mode: ro
1314+
type: RAW
1315+
- id: 135
1316+
name: "\u79BB\u7EBF\u539F\u56E0\u7EC6\u5206"
1317+
code: offline_status
1318+
mode: ro
1319+
type: RAW
1320+
- id: 138
1321+
name: "\u5DE5\u4F5C\u4EFB\u52A1\u7C7B\u578B"
1322+
code: clean_task_type
1323+
mode: ro
1324+
type: ENUM
1325+
property: "{\"range\": [\"0 \u7A7A\u95F2\", \"1 \u5168\u5C4B\u6E05\u6D01\",\
1326+
\ \"2 \u9009\u533A\u6E05\u6D01\", \"3 \u5212\u533A\u6E05\u6D01\", \"4 \u5EFA\
1327+
\u56FE\", \"5 \u5C40\u90E8\u6E05\u6D01\", \"6 \u9065\u63A7\u6A21\u5F0F\",\
1328+
\ \"7 \u5DE1\u822A\", \"8 \u5BFB\u5BA0\", \"9 \u6574\u7406\"]}"
1329+
- id: 139
1330+
name: "\u56DE\u57FA\u7AD9\u76EE\u7684"
1331+
code: back_type
1332+
mode: ro
1333+
type: RAW
1334+
- id: 141
1335+
name: "\u6E05\u6D01\u8FDB\u5EA6"
1336+
code: cleaning_progress
1337+
mode: ro
1338+
type: VALUE
1339+
property: "{\"max\": 100, \"min\": 0, \"step\": 1, \"unit\": \"\u767E\u5206\u6BD4\
1340+
\", \"scale\": 1}"
1341+
- id: 142
1342+
name: publish_dsp
1343+
code: publish_dsp
1344+
mode: ro
1345+
type: RAW
1346+
- id: 143
1347+
name: "\u5730\u56FE\u53D8\u5316\u6570\u636E"
1348+
code: map_diff
1349+
mode: ro
1350+
type: RAW

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-roborock"
3-
version = "5.10.0"
3+
version = "5.11.0"
44
description = "A package to control Roborock vacuums."
55
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
66
requires-python = ">=3.11, <4"

roborock/data/v1/v1_code_mappings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,7 @@ class RoborockDockTypeCode(RoborockEnum):
665665
qrevo_curv_dock = 17
666666
saros_10_dock = 18
667667
qrevo_s5v_dock = 22
668+
saros_20_dock = 27
668669

669670

670671
class RoborockDockDustCollectionModeCode(RoborockEnum):

roborock/data/zeo/zeo_code_mappings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
class ZeoMode(RoborockEnum):
5+
null = 0
56
wash = 1
67
wash_and_dry = 2
78
dry = 3
@@ -23,6 +24,7 @@ class ZeoState(RoborockEnum):
2324

2425

2526
class ZeoProgram(RoborockEnum):
27+
null = 0
2628
standard = 1
2729
quick = 2
2830
sanitize = 3
@@ -75,6 +77,7 @@ class ZeoTemperature(RoborockEnum):
7577
high = 4
7678
max = 5
7779
twenty_c = 6
80+
ninety_c = 7
7881

7982

8083
class ZeoRinse(RoborockEnum):
@@ -87,6 +90,7 @@ class ZeoRinse(RoborockEnum):
8790

8891

8992
class ZeoSpin(RoborockEnum):
93+
null = 0
9094
none = 1
9195
very_low = 2
9296
low = 3

roborock/device_features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,7 @@ def get_supported_features(self) -> list[str]:
654654
RoborockDockTypeCode.saros_r10_dock,
655655
RoborockDockTypeCode.qrevo_curv_dock,
656656
RoborockDockTypeCode.qrevo_s5v_dock,
657+
RoborockDockTypeCode.saros_20_dock,
657658
]
658659

659660

roborock/devices/device_manager.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
HomeData,
1414
HomeDataDevice,
1515
HomeDataProduct,
16+
RoborockCategory,
1617
UserData,
1718
)
1819
from roborock.devices.device import DeviceReadyCallback, RoborockDevice
@@ -173,14 +174,15 @@ def create_web_api_wrapper(
173174
*,
174175
cache: Cache | None = None,
175176
session: aiohttp.ClientSession | None = None,
177+
unauthorized_hook: SessionUnauthorizedHook | None = None,
176178
) -> UserWebApiClient:
177179
"""Create a home data API wrapper from an existing API client."""
178180

179181
# Note: This will auto discover the API base URL. This can be improved
180182
# by caching this next to `UserData` if needed to avoid unnecessary API calls.
181183
client = RoborockApiClient(username=user_params.username, base_url=user_params.base_url, session=session)
182184

183-
return UserWebApiClient(client, user_params.user_data)
185+
return UserWebApiClient(client, user_params.user_data, unauthorized_hook=unauthorized_hook)
184186

185187

186188
async def create_device_manager(
@@ -212,7 +214,9 @@ async def create_device_manager(
212214
if cache is None:
213215
cache = NoCache()
214216

215-
web_api = create_web_api_wrapper(user_params, session=session, cache=cache)
217+
web_api = create_web_api_wrapper(
218+
user_params, session=session, cache=cache, unauthorized_hook=mqtt_session_unauthorized_hook
219+
)
216220
user_data = user_params.user_data
217221

218222
diagnostics = Diagnostics()
@@ -228,6 +232,10 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
228232
device_cache: DeviceCache = DeviceCache(device.duid, cache)
229233
match device.pv:
230234
case DeviceVersion.V1:
235+
if product.category != RoborockCategory.VACUUM:
236+
raise UnsupportedDeviceError(
237+
f"Device {device.name} has unsupported V1 category {product.category}: {product.model}"
238+
)
231239
channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, device_cache)
232240
trait = v1.create(
233241
device.duid,
@@ -265,6 +273,12 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
265273
dev.add_ready_callback(ready_callback)
266274
return dev
267275

268-
manager = DeviceManager(web_api, device_creator, mqtt_session=mqtt_session, cache=cache, diagnostics=diagnostics)
276+
manager = DeviceManager(
277+
web_api,
278+
device_creator,
279+
mqtt_session=mqtt_session,
280+
cache=cache,
281+
diagnostics=diagnostics,
282+
)
269283
await manager.discover_devices(prefer_cache)
270284
return manager

roborock/web_api.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import secrets
77
import string
88
import time
9+
from collections.abc import Callable
910
from dataclasses import dataclass
1011

1112
import aiohttp
@@ -737,23 +738,46 @@ class UserWebApiClient:
737738
to avoid needing to pass UserData around and mock out the web API.
738739
"""
739740

740-
def __init__(self, web_api: RoborockApiClient, user_data: UserData) -> None:
741+
def __init__(
742+
self, web_api: RoborockApiClient, user_data: UserData, unauthorized_hook: Callable[[], None] | None = None
743+
) -> None:
741744
"""Initialize the wrapper with the API client and user data."""
742745
self._web_api = web_api
743746
self._user_data = user_data
747+
self._unauthorized_hook = unauthorized_hook
744748

745749
async def get_home_data(self) -> HomeData:
746750
"""Fetch home data using the API client."""
747-
return await self._web_api.get_home_data_v3(self._user_data)
751+
try:
752+
return await self._web_api.get_home_data_v3(self._user_data)
753+
except RoborockInvalidCredentials:
754+
if self._unauthorized_hook:
755+
self._unauthorized_hook()
756+
raise
748757

749758
async def get_routines(self, device_id: str) -> list[HomeDataScene]:
750759
"""Fetch routines (scenes) for a specific device."""
751-
return await self._web_api.get_scenes(self._user_data, device_id)
760+
try:
761+
return await self._web_api.get_scenes(self._user_data, device_id)
762+
except RoborockInvalidCredentials:
763+
if self._unauthorized_hook:
764+
self._unauthorized_hook()
765+
raise
752766

753767
async def get_rooms(self) -> list[HomeDataRoom]:
754768
"""Fetch rooms using the API client."""
755-
return await self._web_api.get_rooms(self._user_data)
769+
try:
770+
return await self._web_api.get_rooms(self._user_data)
771+
except RoborockInvalidCredentials:
772+
if self._unauthorized_hook:
773+
self._unauthorized_hook()
774+
raise
756775

757776
async def execute_routine(self, scene_id: int) -> None:
758777
"""Execute a specific routine (scene) by its ID."""
759-
await self._web_api.execute_scene(self._user_data, scene_id)
778+
try:
779+
await self._web_api.execute_scene(self._user_data, scene_id)
780+
except RoborockInvalidCredentials:
781+
if self._unauthorized_hook:
782+
self._unauthorized_hook()
783+
raise

0 commit comments

Comments
 (0)