python-roborock 5.24.0__py3-none-any.whl → 5.26.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_roborock-5.24.0.dist-info → python_roborock-5.26.0.dist-info}/METADATA +1 -1
- {python_roborock-5.24.0.dist-info → python_roborock-5.26.0.dist-info}/RECORD +22 -20
- roborock/data/__init__.py +1 -0
- roborock/data/b01_q10/b01_q10_code_mappings.py +64 -0
- roborock/data/b01_q10/b01_q10_containers.py +7 -1
- roborock/data/mower/__init__.py +3 -0
- roborock/data/mower/mower_containers.py +43 -0
- roborock/data/v1/v1_clean_modes.py +171 -2
- roborock/devices/device_manager.py +13 -2
- roborock/devices/rpc/b01_q10_channel.py +51 -1
- roborock/devices/rpc/b01_q7_channel.py +76 -18
- roborock/devices/traits/b01/q10/__init__.py +4 -5
- roborock/devices/traits/b01/q10/command.py +5 -6
- roborock/devices/traits/b01/q7/__init__.py +24 -17
- roborock/devices/traits/b01/q7/clean_summary.py +6 -8
- roborock/devices/traits/b01/q7/map.py +5 -7
- roborock/devices/traits/b01/q7/map_content.py +6 -9
- roborock/devices/traits/v1/status.py +36 -4
- roborock/roborock_message.py +39 -0
- {python_roborock-5.24.0.dist-info → python_roborock-5.26.0.dist-info}/WHEEL +0 -0
- {python_roborock-5.24.0.dist-info → python_roborock-5.26.0.dist-info}/entry_points.txt +0 -0
- {python_roborock-5.24.0.dist-info → python_roborock-5.26.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.26.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/python-roborock/python-roborock
|
|
6
6
|
Project-URL: Documentation, https://python-roborock.readthedocs.io/
|
|
@@ -8,24 +8,26 @@ roborock/diagnostics.py,sha256=YIc7BhC6Cy6W2a6hykvfSRPG8NpLrPZCc_7jTX2Zdho,5091
|
|
|
8
8
|
roborock/exceptions.py,sha256=t2YvfSRMymZPtdPV238itjxVJTaTq5-uBL_EM5FTlfg,3159
|
|
9
9
|
roborock/protocol.py,sha256=eNUL6UGmtSdfsGM1mlYjmorukGGudV45iKXUA7a-Qa8,20193
|
|
10
10
|
roborock/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
roborock/roborock_message.py,sha256=
|
|
11
|
+
roborock/roborock_message.py,sha256=e7gDnBhv2RkG3G2eXmoNVnt5aUWWlLEA3i5LTIBFBy8,7349
|
|
12
12
|
roborock/roborock_typing.py,sha256=Rwx0c0oZCMY-4O8VMLnFSO_OUNb_D59cyG_ktZTZoZI,17134
|
|
13
13
|
roborock/util.py,sha256=mjw329ujLjoWPPAr9mG0VZstaVL06dmVvNnYACB-Pxk,1650
|
|
14
14
|
roborock/web_api.py,sha256=M_A4PiRK5cwkoT3oS8fvglOU97s1jpWKuuCxeeCiF_E,37507
|
|
15
|
-
roborock/data/__init__.py,sha256=
|
|
15
|
+
roborock/data/__init__.py,sha256=jSZHOrd-LfVN-TdBkjNW334ef99rGBz4-hPT_7KvoLs,278
|
|
16
16
|
roborock/data/code_mappings.py,sha256=Cj1nEQkv3Vko7Mh8FODNtfIS5gn5KlTq2QXFtcincHo,8948
|
|
17
17
|
roborock/data/containers.py,sha256=-b6H2snGQTzsJSsctgil6sx8rLtCDihTTzdkxcFRdlE,16489
|
|
18
18
|
roborock/data/b01_q10/__init__.py,sha256=E4lwJcz4AI1z92FcYOJ-cY3fcfHSoXqvYVgfA-TgaRk,71
|
|
19
|
-
roborock/data/b01_q10/b01_q10_code_mappings.py,sha256=
|
|
20
|
-
roborock/data/b01_q10/b01_q10_containers.py,sha256=
|
|
19
|
+
roborock/data/b01_q10/b01_q10_code_mappings.py,sha256=UBFkxCWLYCGQatENmNwR0owkXrcB8AosnD-hICfS2AY,15203
|
|
20
|
+
roborock/data/b01_q10/b01_q10_containers.py,sha256=jieHxHeWIRm2NHVdv15J6kYSeHjAZunGrvZsob-RIH8,11130
|
|
21
21
|
roborock/data/b01_q7/__init__.py,sha256=D-Cd8eB0njf1PQPM6Exl_9A0AhpzIjrL0hapGOCEkkA,69
|
|
22
22
|
roborock/data/b01_q7/b01_q7_code_mappings.py,sha256=EHSCZSv_Os049NcenKo4tkPjDOB4vpmQ2p2Psr9FRCc,10698
|
|
23
23
|
roborock/data/b01_q7/b01_q7_containers.py,sha256=jNEHoNEdq276XJZGGUmtSyJ81cSIzJwem-O_NOgmfdQ,10270
|
|
24
24
|
roborock/data/dyad/__init__.py,sha256=Ksx7Ri1alsAYAyILOpj9iNbCC_6sF7xvETFRnTO4rdM,65
|
|
25
25
|
roborock/data/dyad/dyad_code_mappings.py,sha256=MVhAuQaI2TcDBMQKQcbRk4MbFObx_5XEXTc3n4uAeWc,2836
|
|
26
26
|
roborock/data/dyad/dyad_containers.py,sha256=klJD2wV-2McjAoSN8Bthuyeid59L7M8nzSvc3MtmqOI,434
|
|
27
|
+
roborock/data/mower/__init__.py,sha256=v-bMapgWFzlb0jJVJBmsnmDfgnER7oTjP0OH3ikwGdE,79
|
|
28
|
+
roborock/data/mower/mower_containers.py,sha256=mCsRFTE2AZ2wC9wzrzsr-3z7T2M63CH5ax9kPqPygIY,3194
|
|
27
29
|
roborock/data/v1/__init__.py,sha256=Lx_D6baZrCMxp57D0NcdiA8rssJ5NnInZwUwrT79XOI,91
|
|
28
|
-
roborock/data/v1/v1_clean_modes.py,sha256=
|
|
30
|
+
roborock/data/v1/v1_clean_modes.py,sha256=71_olE6rsdx_UwxkBEPlcHTz0vnyKSnURGxIwuqhOwM,14056
|
|
29
31
|
roborock/data/v1/v1_code_mappings.py,sha256=EgkR3mexyNnPoHZSZHgxA0-ClLDCOFtPIlyEUrWfDIE,17448
|
|
30
32
|
roborock/data/v1/v1_containers.py,sha256=SW7NhY5zbu9j7gG9qEUwpfzMpRn_4RQHOtgnbeMRX5Y,27936
|
|
31
33
|
roborock/data/zeo/__init__.py,sha256=VxG5qG4M24DGOq2YAaxownWp6bEfj9DEOgOMHmTpcqY,63
|
|
@@ -35,23 +37,23 @@ roborock/devices/README.md,sha256=zjssyRIlFWTgQz8hLdsJC9_stlHdUQybz7r2af_8q1k,21
|
|
|
35
37
|
roborock/devices/__init__.py,sha256=yOWuncnp5wwKPLzcnblsBMGD0v8hcDDDC_x7YdSrN70,129
|
|
36
38
|
roborock/devices/cache.py,sha256=V3KC16LzTEAp_cdAirNtWPjZAgz6ugn3zfsYPyrbWsE,4633
|
|
37
39
|
roborock/devices/device.py,sha256=8lHdjLlHhrENPjrZxllpYU0CEh7Os9rp9spnwhdUOoc,9678
|
|
38
|
-
roborock/devices/device_manager.py,sha256=
|
|
40
|
+
roborock/devices/device_manager.py,sha256=NRQK9przB_FYFsljf7fz9_fL1NJCQSGzcvXchP-kplc,11731
|
|
39
41
|
roborock/devices/file_cache.py,sha256=-G2Bm4ra8K_DvSBjntv5hGaRDB-VzSoKWcI0Y_kK6jw,2724
|
|
40
42
|
roborock/devices/rpc/__init__.py,sha256=MDtZovvHi3F4Wbgyzu6gyjAjkZ6xYGU1LRQmtiIapuU,561
|
|
41
43
|
roborock/devices/rpc/a01_channel.py,sha256=9Cf3AlTTAHbts7sodX4gAXAGOIf5CA6WtqykZcYAfnk,3350
|
|
42
|
-
roborock/devices/rpc/b01_q10_channel.py,sha256=
|
|
43
|
-
roborock/devices/rpc/b01_q7_channel.py,sha256=
|
|
44
|
+
roborock/devices/rpc/b01_q10_channel.py,sha256=nT7fsTtti37alm68CHzId8Wy3GMd6npiSNB8Gjegy3I,3494
|
|
45
|
+
roborock/devices/rpc/b01_q7_channel.py,sha256=cxJR_DjJobP6SsAch_O3tNcJdiQGMIwpxH8Wx8bb9xA,7689
|
|
44
46
|
roborock/devices/rpc/v1_channel.py,sha256=aYlBZ0zC7jbwJhhHvKzBfHUZLAf7KOmIPOJQiiLKwvE,22275
|
|
45
47
|
roborock/devices/traits/__init__.py,sha256=YaL9qoxG6IBG0XXDP0HMTvoc0Jkmq7AF6iH2oYWNtcg,765
|
|
46
48
|
roborock/devices/traits/common.py,sha256=KUlXvx2UcFVeNXEhPiCAdy2APu7zrJmM_jME_bM8Odg,4474
|
|
47
49
|
roborock/devices/traits/traits_mixin.py,sha256=7t15l_Ty1cN-3-fKNtRH1XR6-mfadPnTBCQRZP4rAtc,2092
|
|
48
50
|
roborock/devices/traits/a01/__init__.py,sha256=i-us9zLPWPfSQ5XWq8t0MiYLYe5NkcLvk3aEQTYT7E0,8104
|
|
49
51
|
roborock/devices/traits/b01/__init__.py,sha256=pGN6wEocE6Fr2BTasG8Z_75crD6OdTwpju59Vw7eTDk,202
|
|
50
|
-
roborock/devices/traits/b01/q10/__init__.py,sha256=
|
|
52
|
+
roborock/devices/traits/b01/q10/__init__.py,sha256=wvXErH_V6L6bW-EYQFw1PfC8LouKpwItVSsTx2TYMJ4,6010
|
|
51
53
|
roborock/devices/traits/b01/q10/button_light.py,sha256=_CCWSjFe_iZnnSXgBHOju31VUmwG1KKd_jGkmSpLew4,994
|
|
52
54
|
roborock/devices/traits/b01/q10/child_lock.py,sha256=bTsbAMxn7AF1zpWteyZCkQm4jj3NiWD8ddZTM3tlG_0,1131
|
|
53
55
|
roborock/devices/traits/b01/q10/clean_history.py,sha256=OsuzliXtWl2RDMAdKElCaebp9Pr2czVOex180k4jzVs,6672
|
|
54
|
-
roborock/devices/traits/b01/q10/command.py,sha256=
|
|
56
|
+
roborock/devices/traits/b01/q10/command.py,sha256=M0sZu1eIvKYfsOpmJc-H4LKsSrF-kTp_vc_jCRBAvaY,1264
|
|
55
57
|
roborock/devices/traits/b01/q10/common.py,sha256=Qxl3zBAUulqdrjFZJ0DPEFEIJLvlPqUKuTQGPloy7Ds,3089
|
|
56
58
|
roborock/devices/traits/b01/q10/consumable.py,sha256=MbtMgYKqzNdVn6M1iyOAqZPCYTbN_Q_Pr4h8EYGEiuY,669
|
|
57
59
|
roborock/devices/traits/b01/q10/do_not_disturb.py,sha256=klyGQZ_nHyO6bhPlwBl8vElTnMwPf4A8VXwxkU8rMlM,1157
|
|
@@ -62,10 +64,10 @@ roborock/devices/traits/b01/q10/remote.py,sha256=8-fg4SA5M-tdMufHPeD9hIcax7y9Qh7
|
|
|
62
64
|
roborock/devices/traits/b01/q10/status.py,sha256=JAPixbsQ8jfN3OvI7o_NxISXFk8l7rGHFYcL4pH97Jk,859
|
|
63
65
|
roborock/devices/traits/b01/q10/vacuum.py,sha256=w2FftNH5n-Tb-9drR3-5OiMy85FJfTDNmtt34LklC8w,4374
|
|
64
66
|
roborock/devices/traits/b01/q10/volume.py,sha256=UnT0yYDQntxYwnpv4_NVychm0FnGAYRAWA_FJR4yJag,1001
|
|
65
|
-
roborock/devices/traits/b01/q7/__init__.py,sha256=
|
|
66
|
-
roborock/devices/traits/b01/q7/clean_summary.py,sha256=
|
|
67
|
-
roborock/devices/traits/b01/q7/map.py,sha256=
|
|
68
|
-
roborock/devices/traits/b01/q7/map_content.py,sha256=
|
|
67
|
+
roborock/devices/traits/b01/q7/__init__.py,sha256=y3NV-DYHyQoPcpWnmGppX5Y1GjF9MylVxyoIVldKFCA,7935
|
|
68
|
+
roborock/devices/traits/b01/q7/clean_summary.py,sha256=M43iJVeYjrkBV6ZxXdHtn4Rd5Mx-KAK7WLU5fABiN8I,2742
|
|
69
|
+
roborock/devices/traits/b01/q7/map.py,sha256=NfPQAEwNCUsb0SYGMxmGeq4q4rEHqDJK0zTD5cE-ESs,1299
|
|
70
|
+
roborock/devices/traits/b01/q7/map_content.py,sha256=5l7WJSDEfUzXkwXYcvTWhuNz3mlXvA_u2o5Pnh6f5kE,3372
|
|
69
71
|
roborock/devices/traits/v1/__init__.py,sha256=yUa3oN5CZtuVh-fst8FlstpCy1XnS3q42oPlQG9fFYw,15616
|
|
70
72
|
roborock/devices/traits/v1/child_lock.py,sha256=NeaPqcEoBRXvEzKZGonkyINkwCBOK3xa4zSWvJW66kA,1161
|
|
71
73
|
roborock/devices/traits/v1/clean_summary.py,sha256=2uDl3Tg6H9INf7HTEPPat_PBgodoWeX_ko5MJgWggn8,4625
|
|
@@ -84,7 +86,7 @@ roborock/devices/traits/v1/network_info.py,sha256=CX9-ye2Zb_8lAWPGvegXWSOguxMY2K
|
|
|
84
86
|
roborock/devices/traits/v1/rooms.py,sha256=PKNEvzL_Pk5pxGpiMC-XgGpgWVRjivMw_yv7vpGlz3s,6221
|
|
85
87
|
roborock/devices/traits/v1/routines.py,sha256=BfktlpnyDjD2DB3zBDS5BnmjZPE38aHa-wjMontPnik,931
|
|
86
88
|
roborock/devices/traits/v1/smart_wash_params.py,sha256=fAhJpMgUUP1kJcfEAAPeD3anMmCsuX7I1SEoUrUgfjE,504
|
|
87
|
-
roborock/devices/traits/v1/status.py,sha256=
|
|
89
|
+
roborock/devices/traits/v1/status.py,sha256=Y5VBJPZQhmlGDPQvPIp8mFCFsUf4hheZ5k_nnxOoXHI,5071
|
|
88
90
|
roborock/devices/traits/v1/valley_electricity_timer.py,sha256=vE9bLJe8TdFGVh_9mDRkEoC3_DTBkDcdlSMSiYq0YTw,1863
|
|
89
91
|
roborock/devices/traits/v1/volume.py,sha256=etRIYE0ynZZLEo2ez7Dm7_JbijmElrdjpn59hyLWqsQ,814
|
|
90
92
|
roborock/devices/traits/v1/wash_towel_mode.py,sha256=MpVIGYVBC-WNSSFy6PPN-1eeIgJm_XoEzMSTEp9R29g,1913
|
|
@@ -110,8 +112,8 @@ roborock/protocols/a01_protocol.py,sha256=JCIhUuVNamcENu0gtZR2x8rBz6dpAZLcbA-NQg
|
|
|
110
112
|
roborock/protocols/b01_q10_protocol.py,sha256=Ac3_VHJ2DAwBiXFMPYOHExEV0prxS9Ju0djiC8nKtiI,4935
|
|
111
113
|
roborock/protocols/b01_q7_protocol.py,sha256=o8repRrxhxknzspsg1K27ds643rs87ilWXbj8RJtOCs,4619
|
|
112
114
|
roborock/protocols/v1_protocol.py,sha256=uA-EMXd1AUBAXjkjAm0VxZlPLre0v2X9Kei5UqyPzaY,11141
|
|
113
|
-
python_roborock-5.
|
|
114
|
-
python_roborock-5.
|
|
115
|
-
python_roborock-5.
|
|
116
|
-
python_roborock-5.
|
|
117
|
-
python_roborock-5.
|
|
115
|
+
python_roborock-5.26.0.dist-info/METADATA,sha256=S6gc78zARVWGikbdJL_rBkkWOMFx-q5COvsyzCSjblc,5281
|
|
116
|
+
python_roborock-5.26.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
117
|
+
python_roborock-5.26.0.dist-info/entry_points.txt,sha256=EvC1nMqi9ZXKgZnqlNXA33v_3nzgPNjnM0mzWlrehnY,47
|
|
118
|
+
python_roborock-5.26.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
119
|
+
python_roborock-5.26.0.dist-info/RECORD,,
|
roborock/data/__init__.py
CHANGED
|
@@ -289,6 +289,70 @@ class YXCarpetCleanType(RoborockModeEnum):
|
|
|
289
289
|
CROSS = "cross", 3
|
|
290
290
|
|
|
291
291
|
|
|
292
|
+
class YXFault(RoborockModeEnum):
|
|
293
|
+
"""Q10 (B01/ss07) ``dpFault`` (90) codes, from the ss07 fault spec.
|
|
294
|
+
|
|
295
|
+
``dpFault`` is *overloaded*: several values are lifecycle/status rather than
|
|
296
|
+
errors (e.g. 400 = scheduled clean starting, 501 = returning to dock,
|
|
297
|
+
502 = recharge). A non-zero fault is not necessarily a blocking error.
|
|
298
|
+
The converse also holds: the device can sit in its error state (``dpStatus``
|
|
299
|
+
12) with ``dpFault`` still 0 (observed live with the dust-bin module
|
|
300
|
+
removed), so a ``None``/``NONE`` fault does not imply the absence of an
|
|
301
|
+
error condition.
|
|
302
|
+
|
|
303
|
+
These labels differ from the Q7 ``B01Fault`` for several shared numbers
|
|
304
|
+
(500, 501, 503, 569, 570) -- so this is a Q10-specific map, not a reuse of
|
|
305
|
+
``B01Fault``. Codes marked "hw-confirmed" were observed firing live on a
|
|
306
|
+
physical ss07 in a context matching the label; the rest are from the spec
|
|
307
|
+
only, not yet observed live (single device).
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
UNKNOWN = "unknown", -1
|
|
311
|
+
NONE = "none", 0
|
|
312
|
+
LIDAR_BLOCKED = "lidar_blocked", 1
|
|
313
|
+
BUMPER_STUCK = "bumper_stuck", 2 # hw-confirmed (bumper held in during commanded motion)
|
|
314
|
+
ROBOT_SUSPENDED = "robot_suspended", 3 # hw-confirmed (wheels lifted off the floor mid-task)
|
|
315
|
+
CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4 # hw-confirmed (one side lifted mid-task)
|
|
316
|
+
MAIN_BRUSH_STUCK = "main_brush_stuck", 5 # hw-confirmed (two physical brush jams)
|
|
317
|
+
MAIN_WHEELS_STUCK = "main_wheels_stuck", 7
|
|
318
|
+
ROBOT_TRAPPED = "robot_trapped", 8 # hw-confirmed
|
|
319
|
+
CHECK_DUSTBIN_FILTER = "check_dustbin_filter", 9
|
|
320
|
+
LOW_BATTERY = "low_battery", 12 # hw-confirmed (fired at 14% mid clean)
|
|
321
|
+
TEMPERATURE_THRESHOLD = "temperature_threshold", 14
|
|
322
|
+
ROBOT_TILTED = "robot_tilted", 16
|
|
323
|
+
LIDAR_COVER_OBSTRUCTED = "lidar_cover_obstructed", 21
|
|
324
|
+
NO_GO_ZONE_DETECTED = "no_go_zone_detected", 24
|
|
325
|
+
MOPPING_MODULE_STUCK = "mopping_module_stuck", 27
|
|
326
|
+
CARPET_AVOIDANCE = "carpet_avoidance", 28
|
|
327
|
+
CANNOT_CROSS_CARPET = "cannot_cross_carpet", 29
|
|
328
|
+
INSTALL_DUST_BAG = "install_dust_bag", 46 # hw-confirmed (docked with bag removed; fires at dock contact)
|
|
329
|
+
MOP_MOUNT_FELL_OFF = "mop_mount_fell_off", 54
|
|
330
|
+
LIDAR_DIRTY = "lidar_dirty", 58
|
|
331
|
+
FILTER_SERVICE_LIFE = "filter_service_life", 301
|
|
332
|
+
MAIN_BRUSH_SERVICE_LIFE = "main_brush_service_life", 302
|
|
333
|
+
SIDE_BRUSH_SERVICE_LIFE = "side_brush_service_life", 303
|
|
334
|
+
SENSOR_NEEDS_CLEANING = "sensor_needs_cleaning", 304 # hw-confirmed (fired during auto-empty)
|
|
335
|
+
DUST_BAG_FULL = "dust_bag_full", 310 # inferred from auto-empty context; not hw-confirmed
|
|
336
|
+
STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # hw-confirmed x3; lifecycle, not an error
|
|
337
|
+
# hw-confirmed (a due scheduled clean fired mid-clean and was ignored); lifecycle, not an error
|
|
338
|
+
CLEANING_IN_PROGRESS = "cleaning_in_progress", 407
|
|
339
|
+
EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked); spec-only
|
|
340
|
+
# hw-confirmed, fires per completed task; ss07 != Q7 (robot_suspended)
|
|
341
|
+
CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501
|
|
342
|
+
LOW_BATTERY_RESUME = "low_battery_resume", 502 # hw-confirmed; lifecycle
|
|
343
|
+
DOCKING_ERROR = "docking_error", 503 # hw-confirmed; ss07 != Q7 (dustbin_not_installed)
|
|
344
|
+
POSITIONING_FAILED = "positioning_failed", 556 # hw-confirmed; relocalization
|
|
345
|
+
# hw-confirmed (3rd auto-empty in ~15 min: dock refuses to run the cycle); ss07 != Q7 (main_wheels_entangled)
|
|
346
|
+
TOO_FREQUENT_EMPTYING = "too_frequent_emptying", 569
|
|
347
|
+
CANNOT_REACH_TARGET = "cannot_reach_target", 570 # hw-confirmed; ss07 != Q7 (main_brush_entangled)
|
|
348
|
+
OFFLINE_WARNING_ASLEEP = "offline_warning_asleep", 588
|
|
349
|
+
OFFLINE_WARNING_LOW_BATTERY = "offline_warning_low_battery", 589
|
|
350
|
+
DND_AUTO_TOPUP_DISABLED = "dnd_auto_topup_disabled", 591
|
|
351
|
+
CLEAN_CARPET_ULTRASONIC_SENSORS = "clean_carpet_ultrasonic_sensors", 707
|
|
352
|
+
ROBOT_ERROR_RESET = "robot_error_reset", 1002
|
|
353
|
+
VOICE_PACK_UPDATE_AVAILABLE = "voice_pack_update_available", 3001
|
|
354
|
+
|
|
355
|
+
|
|
292
356
|
class RemoteCommand(IntEnum):
|
|
293
357
|
FORWARD = 0
|
|
294
358
|
LEFT = 2
|
|
@@ -23,6 +23,7 @@ from .b01_q10_code_mappings import (
|
|
|
23
23
|
YXDeviceDustCollectionFrequency,
|
|
24
24
|
YXDeviceState,
|
|
25
25
|
YXFanLevel,
|
|
26
|
+
YXFault,
|
|
26
27
|
YXStartMethod,
|
|
27
28
|
YXWaterLevel,
|
|
28
29
|
)
|
|
@@ -165,7 +166,7 @@ class Q10Status(RoborockBase):
|
|
|
165
166
|
clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE})
|
|
166
167
|
back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE})
|
|
167
168
|
cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_PROGRESS})
|
|
168
|
-
fault:
|
|
169
|
+
fault: YXFault | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT})
|
|
169
170
|
|
|
170
171
|
# Additional state reported in the device's full status dump.
|
|
171
172
|
clean_line: YXCleanLine | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_LINE})
|
|
@@ -203,6 +204,11 @@ class Q10Status(RoborockBase):
|
|
|
203
204
|
filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
|
|
204
205
|
sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
|
|
205
206
|
|
|
207
|
+
@property
|
|
208
|
+
def fault_name(self) -> str | None:
|
|
209
|
+
"""Returns the name of the current fault."""
|
|
210
|
+
return self.fault.value if self.fault is not None else None
|
|
211
|
+
|
|
206
212
|
|
|
207
213
|
@dataclass
|
|
208
214
|
class SoundVolume(RoborockBase):
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Data containers for Roborock mower devices."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from roborock.data.containers import RoborockBase
|
|
7
|
+
from roborock.roborock_message import RoborockMowerDataProtocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class MowerStatus(RoborockBase):
|
|
12
|
+
"""Core mower status backed by mower DPS updates."""
|
|
13
|
+
|
|
14
|
+
error_code: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.ERROR_CODE})
|
|
15
|
+
battery: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BATTERY})
|
|
16
|
+
mow_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_TYPE})
|
|
17
|
+
mow_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_STATE})
|
|
18
|
+
mapping_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_TYPE})
|
|
19
|
+
mapping_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_STATE})
|
|
20
|
+
ota_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OTA_STATE})
|
|
21
|
+
charge_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_STATE})
|
|
22
|
+
dock_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.DOCK_STATE})
|
|
23
|
+
charge_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_TYPE})
|
|
24
|
+
pend_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.PEND_TYPE})
|
|
25
|
+
remote_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.REMOTE_STATE})
|
|
26
|
+
mow_start_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_START_TYPE})
|
|
27
|
+
mow_eff_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_EFF_MODE})
|
|
28
|
+
mow_height: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_HEIGHT})
|
|
29
|
+
mow_direction_angle: int | None = field(
|
|
30
|
+
default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_DIRECTION_ANGLE}
|
|
31
|
+
)
|
|
32
|
+
mow_pattern: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PATTERN})
|
|
33
|
+
mow_conf_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_CONF_MODE})
|
|
34
|
+
offline_status: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OFFLINE_STATUS})
|
|
35
|
+
mow_progress: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PROGRESS})
|
|
36
|
+
blade_lifespan: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BLADE_LIFESPAN})
|
|
37
|
+
fc_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.FC_STATE})
|
|
38
|
+
gps_coordinate: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.GPS_COORDINATE})
|
|
39
|
+
off_dock_no_task_status: int | None = field(
|
|
40
|
+
default=None, metadata={"dps": RoborockMowerDataProtocol.OFF_DOCK_NO_TASK_STATUS}
|
|
41
|
+
)
|
|
42
|
+
afs_status: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.AFS_STATUS})
|
|
43
|
+
network_channel: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.NETWORK_CHANNEL})
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import typing
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import TypeVar
|
|
4
6
|
|
|
7
|
+
from ...exceptions import RoborockUnsupportedFeature
|
|
5
8
|
from ..code_mappings import RoborockModeEnum
|
|
6
9
|
|
|
7
10
|
if typing.TYPE_CHECKING:
|
|
@@ -68,6 +71,22 @@ class WashTowelModes(RoborockModeEnum):
|
|
|
68
71
|
SUPER_DEEP = ("super_deep", 8)
|
|
69
72
|
|
|
70
73
|
|
|
74
|
+
class CleaningMode(StrEnum):
|
|
75
|
+
"""High-level cleaning intent derived from the lower-level motor settings.
|
|
76
|
+
|
|
77
|
+
Prefer this abstraction when you want to present or switch between the
|
|
78
|
+
user-facing cleaning behaviors exposed by the app. The lower-level
|
|
79
|
+
`VacuumModes`, `WaterModes`, and `CleanRoutes` enums are still useful for
|
|
80
|
+
fine-grained selection.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
VACUUM = "vacuum"
|
|
84
|
+
VAC_AND_MOP = "vac_and_mop"
|
|
85
|
+
MOP = "mop"
|
|
86
|
+
CUSTOM = "custom"
|
|
87
|
+
SMART_MODE = "smart_mode"
|
|
88
|
+
|
|
89
|
+
|
|
71
90
|
WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes] = {
|
|
72
91
|
200: WaterModes.OFF,
|
|
73
92
|
221: WaterModes.PURE_WATER_FLOW_START,
|
|
@@ -78,6 +97,8 @@ WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes] = {
|
|
|
78
97
|
250: WaterModes.PURE_WATER_FLOW_END,
|
|
79
98
|
}
|
|
80
99
|
|
|
100
|
+
ModeEnumT = TypeVar("ModeEnumT", bound=RoborockModeEnum)
|
|
101
|
+
|
|
81
102
|
|
|
82
103
|
def get_wash_towel_modes(features: DeviceFeatures) -> list[WashTowelModes]:
|
|
83
104
|
"""Get the valid wash towel modes for the device"""
|
|
@@ -174,7 +195,151 @@ def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
|
|
|
174
195
|
return {mode.code: mode.value for mode in get_water_modes(features)}
|
|
175
196
|
|
|
176
197
|
|
|
177
|
-
def
|
|
198
|
+
def get_cleaning_mode_options(features: DeviceFeatures) -> list[CleaningMode]:
|
|
199
|
+
"""Return the supported high-level cleaning modes for the device.
|
|
200
|
+
|
|
201
|
+
These options are the preferred user-facing choices because they bundle the
|
|
202
|
+
correct fan, water, and mop-route settings together for the device. Callers
|
|
203
|
+
should generally present these instead of mixing lower-level mode enums
|
|
204
|
+
unless they explicitly need fine-grained control.
|
|
205
|
+
"""
|
|
206
|
+
if not features.is_support_water_mode:
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
supported_water_modes = get_water_modes(features)
|
|
210
|
+
options = [CleaningMode.VACUUM, CleaningMode.VAC_AND_MOP]
|
|
211
|
+
if features.is_pure_clean_mop_supported:
|
|
212
|
+
options.append(CleaningMode.MOP)
|
|
213
|
+
if features.is_customized_clean_supported and WaterModes.CUSTOMIZED in supported_water_modes:
|
|
214
|
+
options.append(CleaningMode.CUSTOM)
|
|
215
|
+
if features.is_smart_clean_mode_set_supported and WaterModes.SMART_MODE in supported_water_modes:
|
|
216
|
+
options.append(CleaningMode.SMART_MODE)
|
|
217
|
+
return options
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_mop_only_vacuum_mode(features: DeviceFeatures) -> VacuumModes:
|
|
221
|
+
"""Determine the vacuum mode to use when you just want to mop.
|
|
222
|
+
|
|
223
|
+
There are three cases that must be handled:
|
|
224
|
+
1. The device does not support only mopping.
|
|
225
|
+
2. The device supports raising the vacuum brush while mopping
|
|
226
|
+
3. All other cases.
|
|
227
|
+
"""
|
|
228
|
+
if not features.is_pure_clean_mop_supported:
|
|
229
|
+
raise RoborockUnsupportedFeature("Mop-only cleaning is not supported")
|
|
230
|
+
if features.is_support_main_brush_up_down_supported:
|
|
231
|
+
return VacuumModes.OFF_RAISE_MAIN_BRUSH
|
|
232
|
+
return VacuumModes.OFF
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _get_default_mopping_water_mode(features: DeviceFeatures) -> WaterModes:
|
|
236
|
+
"""Pick a sensible default water mode when mopping for the device."""
|
|
237
|
+
# Water-slide devices use a disjoint set of water codes; pick a mid-flow
|
|
238
|
+
# slide code instead of the standard 202, which they don't accept.
|
|
239
|
+
if features.is_water_slide_mode_supported:
|
|
240
|
+
return WaterModes.PURE_WATER_FLOW_MIDDLE
|
|
241
|
+
return WaterModes.STANDARD
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _get_clean_motor_mode_params(
|
|
245
|
+
mode: CleaningMode,
|
|
246
|
+
features: DeviceFeatures,
|
|
247
|
+
) -> tuple[VacuumModes, WaterModes, CleanRoutes]:
|
|
248
|
+
"""Return (fan_power, water_box_mode, mop_mode) enums for the high-level mode."""
|
|
249
|
+
if mode == CleaningMode.VACUUM:
|
|
250
|
+
return (VacuumModes.BALANCED, WaterModes.OFF, CleanRoutes.STANDARD)
|
|
251
|
+
if mode == CleaningMode.VAC_AND_MOP:
|
|
252
|
+
return (VacuumModes.BALANCED, _get_default_mopping_water_mode(features), CleanRoutes.STANDARD)
|
|
253
|
+
if mode == CleaningMode.MOP:
|
|
254
|
+
return (
|
|
255
|
+
get_mop_only_vacuum_mode(features),
|
|
256
|
+
_get_default_mopping_water_mode(features),
|
|
257
|
+
CleanRoutes.STANDARD,
|
|
258
|
+
)
|
|
259
|
+
if mode == CleaningMode.CUSTOM:
|
|
260
|
+
return (VacuumModes.CUSTOMIZED, WaterModes.CUSTOMIZED, CleanRoutes.CUSTOMIZED)
|
|
261
|
+
if mode == CleaningMode.SMART_MODE:
|
|
262
|
+
return (VacuumModes.SMART_MODE, WaterModes.SMART_MODE, CleanRoutes.SMART_MODE)
|
|
263
|
+
raise RoborockUnsupportedFeature(f"Cleaning mode {mode.value!r} is not supported")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def resolve_cleaning_mode(cleaning_mode: str | CleaningMode) -> CleaningMode:
|
|
267
|
+
"""Resolve a string or enum into a CleaningMode value."""
|
|
268
|
+
if isinstance(cleaning_mode, CleaningMode):
|
|
269
|
+
return cleaning_mode
|
|
270
|
+
try:
|
|
271
|
+
return CleaningMode(cleaning_mode)
|
|
272
|
+
except ValueError as err:
|
|
273
|
+
raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode!r} is not supported") from err
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_cleaning_mode_parameters(cleaning_mode: CleaningMode, features: DeviceFeatures) -> list[dict[str, int]]:
|
|
277
|
+
"""Get the RPC payload for switching the high-level cleaning mode."""
|
|
278
|
+
if cleaning_mode not in get_cleaning_mode_options(features):
|
|
279
|
+
raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode.value!r} is not supported")
|
|
280
|
+
|
|
281
|
+
fan_power, water_box_mode, mop_mode = _get_clean_motor_mode_params(cleaning_mode, features)
|
|
282
|
+
params: dict[str, int] = {"fan_power": fan_power.code, "water_box_mode": water_box_mode.code}
|
|
283
|
+
if features.is_clean_route_setting_supported:
|
|
284
|
+
params["mop_mode"] = mop_mode.code
|
|
285
|
+
return [params]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _resolve_mode_code(value: int | ModeEnumT | None, mode_cls: type[ModeEnumT]) -> ModeEnumT | None:
|
|
289
|
+
"""Resolve a raw code or enum into a RoborockModeEnum."""
|
|
290
|
+
if value is None:
|
|
291
|
+
return None
|
|
292
|
+
if isinstance(value, mode_cls):
|
|
293
|
+
return value
|
|
294
|
+
return mode_cls.from_code_optional(int(value))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _resolve_clean_mode(value: int | VacuumModes | None, features: DeviceFeatures) -> VacuumModes | None:
|
|
298
|
+
"""Resolve a vacuum mode code, accounting for feature-specific code aliases."""
|
|
299
|
+
if value is None or isinstance(value, VacuumModes):
|
|
300
|
+
return value
|
|
301
|
+
if value == VacuumModes.OFF.code:
|
|
302
|
+
if features.is_pure_clean_mop_supported:
|
|
303
|
+
return get_mop_only_vacuum_mode(features)
|
|
304
|
+
return VacuumModes.GENTLE
|
|
305
|
+
return VacuumModes.from_code_optional(value)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def get_current_cleaning_mode(
|
|
309
|
+
clean_mode: int | VacuumModes | None,
|
|
310
|
+
water_mode: int | WaterModes | None,
|
|
311
|
+
mop_mode: int | CleanRoutes | None,
|
|
312
|
+
features: DeviceFeatures,
|
|
313
|
+
) -> CleaningMode | None:
|
|
314
|
+
"""Classify the current high-level cleaning mode from individual mode codes."""
|
|
315
|
+
if not features.is_support_water_mode:
|
|
316
|
+
return None
|
|
317
|
+
clean_mode_enum = _resolve_clean_mode(clean_mode, features)
|
|
318
|
+
water_mode_enum = _resolve_mode_code(water_mode, WaterModes)
|
|
319
|
+
mop_mode_enum = _resolve_mode_code(mop_mode, CleanRoutes)
|
|
320
|
+
if clean_mode_enum is None or water_mode_enum is None:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
if is_smart_mode_set(water_mode_enum, clean_mode_enum, mop_mode_enum):
|
|
324
|
+
return CleaningMode.SMART_MODE
|
|
325
|
+
if is_mode_customized(clean_mode_enum, water_mode_enum, mop_mode_enum):
|
|
326
|
+
return CleaningMode.CUSTOM
|
|
327
|
+
if water_mode_enum != WaterModes.OFF:
|
|
328
|
+
try:
|
|
329
|
+
if clean_mode_enum == get_mop_only_vacuum_mode(features):
|
|
330
|
+
return CleaningMode.MOP
|
|
331
|
+
except RoborockUnsupportedFeature:
|
|
332
|
+
pass
|
|
333
|
+
if water_mode_enum == WaterModes.OFF:
|
|
334
|
+
return CleaningMode.VACUUM
|
|
335
|
+
return CleaningMode.VAC_AND_MOP
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def is_mode_customized(
|
|
339
|
+
clean_mode: VacuumModes | None,
|
|
340
|
+
water_mode: WaterModes | None,
|
|
341
|
+
mop_mode: CleanRoutes | None,
|
|
342
|
+
) -> bool:
|
|
178
343
|
"""Check if any of the cleaning modes are set to a custom value."""
|
|
179
344
|
return (
|
|
180
345
|
clean_mode == VacuumModes.CUSTOMIZED
|
|
@@ -183,7 +348,11 @@ def is_mode_customized(clean_mode: VacuumModes, water_mode: WaterModes, mop_mode
|
|
|
183
348
|
)
|
|
184
349
|
|
|
185
350
|
|
|
186
|
-
def is_smart_mode_set(
|
|
351
|
+
def is_smart_mode_set(
|
|
352
|
+
water_mode: WaterModes | None,
|
|
353
|
+
clean_mode: VacuumModes | None,
|
|
354
|
+
mop_mode: CleanRoutes | None,
|
|
355
|
+
) -> bool:
|
|
187
356
|
"""Check if the smart mode is set for the given water mode and clean mode"""
|
|
188
357
|
return (
|
|
189
358
|
water_mode == WaterModes.SMART_MODE
|
|
@@ -26,6 +26,8 @@ from roborock.protocol import create_mqtt_params
|
|
|
26
26
|
from roborock.web_api import RoborockApiClient, UserWebApiClient
|
|
27
27
|
|
|
28
28
|
from .cache import Cache, DeviceCache, NoCache
|
|
29
|
+
from .rpc.b01_q7_channel import create_b01_q7_channel
|
|
30
|
+
from .rpc.b01_q10_channel import create_b01_q10_channel
|
|
29
31
|
from .rpc.v1_channel import create_v1_channel
|
|
30
32
|
from .traits import Trait, a01, b01, v1
|
|
31
33
|
from .transport.channel import Channel
|
|
@@ -254,13 +256,22 @@ async def create_device_manager(
|
|
|
254
256
|
channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
|
|
255
257
|
trait = a01.create(product, channel)
|
|
256
258
|
case DeviceVersion.B01:
|
|
257
|
-
|
|
259
|
+
mqtt_channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
|
|
258
260
|
model_part = product.model.split(".")[-1]
|
|
259
261
|
if "ss" in model_part:
|
|
262
|
+
b01_q10_channel = create_b01_q10_channel(mqtt_channel)
|
|
263
|
+
channel = b01_q10_channel
|
|
260
264
|
trait = b01.q10.create(channel)
|
|
261
265
|
elif "sc" in model_part:
|
|
262
266
|
# Q7 devices start with 'sc' in their model naming.
|
|
263
|
-
|
|
267
|
+
b01_q7_channel = create_b01_q7_channel(device, product, mqtt_channel)
|
|
268
|
+
channel = b01_q7_channel
|
|
269
|
+
trait = b01.q7.create(
|
|
270
|
+
product,
|
|
271
|
+
device,
|
|
272
|
+
rpc_channel=b01_q7_channel,
|
|
273
|
+
map_rpc_channel=b01_q7_channel,
|
|
274
|
+
)
|
|
264
275
|
else:
|
|
265
276
|
raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}")
|
|
266
277
|
case _:
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Thin wrapper around the MQTT channel for Roborock B01 Q10 devices."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections.abc import AsyncGenerator
|
|
4
|
+
from collections.abc import AsyncGenerator, Callable
|
|
5
|
+
from typing import Protocol
|
|
5
6
|
|
|
6
7
|
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
8
|
+
from roborock.devices.transport.channel import Channel
|
|
7
9
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
8
10
|
from roborock.exceptions import RoborockException
|
|
9
11
|
from roborock.protocols.b01_q10_protocol import (
|
|
@@ -12,10 +14,53 @@ from roborock.protocols.b01_q10_protocol import (
|
|
|
12
14
|
decode_message,
|
|
13
15
|
encode_mqtt_payload,
|
|
14
16
|
)
|
|
17
|
+
from roborock.roborock_message import RoborockMessage
|
|
15
18
|
|
|
16
19
|
_LOGGER = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
class Q10RpcChannel(Protocol):
|
|
23
|
+
"""Protocol for Q10 RPC channels."""
|
|
24
|
+
|
|
25
|
+
async def send_command(
|
|
26
|
+
self,
|
|
27
|
+
command: B01_Q10_DP,
|
|
28
|
+
params: ParamsType = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Send a command on the MQTT channel, without waiting for a response."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class B01Q10Channel(Channel, Q10RpcChannel):
|
|
35
|
+
"""Unified B01 Q10 channel wrapping MQTT transport."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, mqtt_channel: MqttChannel) -> None:
|
|
38
|
+
self._mqtt_channel = mqtt_channel
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_connected(self) -> bool:
|
|
42
|
+
return self._mqtt_channel.is_connected
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_local_connected(self) -> bool:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
|
|
49
|
+
return await self._mqtt_channel.subscribe(callback)
|
|
50
|
+
|
|
51
|
+
async def subscribe_stream(self) -> AsyncGenerator[Q10Message, None]:
|
|
52
|
+
"""Stream decoded Q10 messages received via MQTT."""
|
|
53
|
+
async for msg in stream_decoded_messages(self._mqtt_channel):
|
|
54
|
+
yield msg
|
|
55
|
+
|
|
56
|
+
async def send_command(
|
|
57
|
+
self,
|
|
58
|
+
command: B01_Q10_DP,
|
|
59
|
+
params: ParamsType = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
await send_command(self._mqtt_channel, command, params)
|
|
62
|
+
|
|
63
|
+
|
|
19
64
|
async def stream_decoded_messages(
|
|
20
65
|
mqtt_channel: MqttChannel,
|
|
21
66
|
) -> AsyncGenerator[Q10Message, None]:
|
|
@@ -59,3 +104,8 @@ async def send_command(
|
|
|
59
104
|
ex,
|
|
60
105
|
)
|
|
61
106
|
raise
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_b01_q10_channel(mqtt_channel: MqttChannel) -> B01Q10Channel:
|
|
110
|
+
"""Create a B01Q10Channel wrapping MQTT transport."""
|
|
111
|
+
return B01Q10Channel(mqtt_channel)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
"""Thin wrapper around the MQTT channel for Roborock B01 Q7 devices."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
3
|
import asyncio
|
|
6
4
|
import json
|
|
7
5
|
import logging
|
|
8
6
|
from collections.abc import Callable
|
|
9
|
-
from typing import TypeAlias, TypeVar
|
|
7
|
+
from typing import Protocol, TypeAlias, TypeVar
|
|
10
8
|
|
|
9
|
+
from roborock.data import HomeDataDevice, HomeDataProduct
|
|
10
|
+
from roborock.devices.transport.channel import Channel
|
|
11
11
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
12
12
|
from roborock.exceptions import RoborockException
|
|
13
13
|
from roborock.protocols.b01_q7_protocol import (
|
|
14
|
+
B01_Q7_DPS,
|
|
14
15
|
B01_VERSION,
|
|
16
|
+
CommandType,
|
|
15
17
|
MapKey,
|
|
18
|
+
ParamsType,
|
|
16
19
|
Q7RequestMessage,
|
|
20
|
+
create_map_key,
|
|
17
21
|
decode_map_payload,
|
|
18
22
|
decode_rpc_response,
|
|
19
23
|
encode_mqtt_payload,
|
|
@@ -26,6 +30,30 @@ _T = TypeVar("_T")
|
|
|
26
30
|
DecodedB01Response: TypeAlias = dict[str, object] | str
|
|
27
31
|
|
|
28
32
|
|
|
33
|
+
class Q7RpcChannel(Protocol):
|
|
34
|
+
"""Protocol for Q7 RPC channels."""
|
|
35
|
+
|
|
36
|
+
async def send_command(
|
|
37
|
+
self,
|
|
38
|
+
command: CommandType,
|
|
39
|
+
params: ParamsType = None,
|
|
40
|
+
) -> DecodedB01Response:
|
|
41
|
+
"""Send a command and get a decoded response."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Q7MapRpcChannel(Protocol):
|
|
46
|
+
"""Protocol for Q7 map RPC channels."""
|
|
47
|
+
|
|
48
|
+
async def send_map_command(
|
|
49
|
+
self,
|
|
50
|
+
command: CommandType,
|
|
51
|
+
params: ParamsType = None,
|
|
52
|
+
) -> bytes:
|
|
53
|
+
"""Send a map command and get decoded bytes."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
29
57
|
def _matches_map_response(response_message: RoborockMessage, *, version: bytes | None) -> bytes | None:
|
|
30
58
|
"""Return raw map payload bytes for matching MAP_RESPONSE messages."""
|
|
31
59
|
if (
|
|
@@ -120,39 +148,55 @@ async def send_decoded_command(
|
|
|
120
148
|
raise RoborockException(f"B01 command timed out after {_TIMEOUT}s ({request_message})") from ex
|
|
121
149
|
except RoborockException as ex:
|
|
122
150
|
_LOGGER.warning(
|
|
123
|
-
"Error sending B01 decoded command (%
|
|
151
|
+
"Error sending B01 decoded command (%s): %s",
|
|
124
152
|
request_message,
|
|
125
153
|
ex,
|
|
126
154
|
)
|
|
127
155
|
raise
|
|
128
156
|
except Exception as ex:
|
|
129
157
|
_LOGGER.exception(
|
|
130
|
-
"Error sending B01 decoded command (%
|
|
158
|
+
"Error sending B01 decoded command (%s): %s",
|
|
131
159
|
request_message,
|
|
132
160
|
ex,
|
|
133
161
|
)
|
|
134
162
|
raise
|
|
135
163
|
|
|
136
164
|
|
|
137
|
-
class
|
|
138
|
-
"""
|
|
165
|
+
class B01Q7Channel(Channel, Q7RpcChannel, Q7MapRpcChannel):
|
|
166
|
+
"""Unified B01 Q7 channel wrapping MQTT transport."""
|
|
139
167
|
|
|
140
168
|
def __init__(self, mqtt_channel: MqttChannel, map_key: MapKey) -> None:
|
|
141
169
|
self._mqtt_channel = mqtt_channel
|
|
142
170
|
self._map_key = map_key
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
@property
|
|
173
|
+
def is_connected(self) -> bool:
|
|
174
|
+
return self._mqtt_channel.is_connected
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def is_local_connected(self) -> bool:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
|
|
181
|
+
return await self._mqtt_channel.subscribe(callback)
|
|
182
|
+
|
|
183
|
+
async def send_command(
|
|
184
|
+
self,
|
|
185
|
+
command: CommandType,
|
|
186
|
+
params: ParamsType = None,
|
|
187
|
+
) -> DecodedB01Response:
|
|
188
|
+
return await send_decoded_command(
|
|
189
|
+
self._mqtt_channel,
|
|
190
|
+
Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
|
|
191
|
+
)
|
|
155
192
|
|
|
193
|
+
async def send_map_command(
|
|
194
|
+
self,
|
|
195
|
+
command: CommandType,
|
|
196
|
+
params: ParamsType = None,
|
|
197
|
+
) -> bytes:
|
|
198
|
+
"""Send a map upload command and return decoded SCMap bytes."""
|
|
199
|
+
request_message = Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params)
|
|
156
200
|
try:
|
|
157
201
|
raw_payload = await _send_command(
|
|
158
202
|
self._mqtt_channel,
|
|
@@ -163,3 +207,17 @@ class MapRpcChannel:
|
|
|
163
207
|
raise RoborockException(f"B01 map command timed out after {_TIMEOUT}s ({request_message})") from ex
|
|
164
208
|
|
|
165
209
|
return decode_map_payload(raw_payload, map_key=self._map_key)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def create_b01_q7_channel(
|
|
213
|
+
device: HomeDataDevice,
|
|
214
|
+
product: HomeDataProduct,
|
|
215
|
+
mqtt_channel: MqttChannel,
|
|
216
|
+
) -> B01Q7Channel:
|
|
217
|
+
"""Create a B01Q7Channel for the given device."""
|
|
218
|
+
if device.sn is None or product.model is None:
|
|
219
|
+
raise RoborockException(
|
|
220
|
+
f"Device serial number and product model are required (sn: {device.sn}, model: {product.model})"
|
|
221
|
+
)
|
|
222
|
+
map_key = create_map_key(serial=device.sn, model=product.model)
|
|
223
|
+
return B01Q7Channel(mqtt_channel, map_key)
|
|
@@ -4,9 +4,8 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
6
|
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
7
|
-
from roborock.devices.rpc.b01_q10_channel import
|
|
7
|
+
from roborock.devices.rpc.b01_q10_channel import B01Q10Channel
|
|
8
8
|
from roborock.devices.traits import Trait
|
|
9
|
-
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
10
9
|
from roborock.map.b01_q10_map_parser import Q10MapPacket, Q10TracePacket
|
|
11
10
|
from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message
|
|
12
11
|
|
|
@@ -83,7 +82,7 @@ class Q10PropertiesApi(Trait):
|
|
|
83
82
|
clean_history: CleanHistoryTrait
|
|
84
83
|
"""Trait for fetching the device clean-record history (``dpCleanRecord``)."""
|
|
85
84
|
|
|
86
|
-
def __init__(self, channel:
|
|
85
|
+
def __init__(self, channel: B01Q10Channel) -> None:
|
|
87
86
|
"""Initialize the B01Props API."""
|
|
88
87
|
self._channel = channel
|
|
89
88
|
self.command = CommandTrait(channel)
|
|
@@ -134,7 +133,7 @@ class Q10PropertiesApi(Trait):
|
|
|
134
133
|
|
|
135
134
|
async def _subscribe_loop(self) -> None:
|
|
136
135
|
"""Persistent loop dispatching decoded messages to the read-model traits."""
|
|
137
|
-
async for message in
|
|
136
|
+
async for message in self._channel.subscribe_stream():
|
|
138
137
|
self._handle_message(message)
|
|
139
138
|
|
|
140
139
|
def _handle_message(self, message: Q10Message) -> None:
|
|
@@ -157,6 +156,6 @@ class Q10PropertiesApi(Trait):
|
|
|
157
156
|
trait.update_from_dps(message.dps)
|
|
158
157
|
|
|
159
158
|
|
|
160
|
-
def create(channel:
|
|
159
|
+
def create(channel: B01Q10Channel) -> Q10PropertiesApi:
|
|
161
160
|
"""Create traits for B01 devices."""
|
|
162
161
|
return Q10PropertiesApi(channel)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
4
|
-
from roborock.devices.rpc.b01_q10_channel import
|
|
5
|
-
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
4
|
+
from roborock.devices.rpc.b01_q10_channel import Q10RpcChannel
|
|
6
5
|
from roborock.protocols.b01_q10_protocol import ParamsType
|
|
7
6
|
|
|
8
7
|
|
|
@@ -15,9 +14,9 @@ class CommandTrait:
|
|
|
15
14
|
available.
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
|
-
def __init__(self,
|
|
17
|
+
def __init__(self, rpc_channel: Q10RpcChannel) -> None:
|
|
19
18
|
"""Initialize the CommandTrait."""
|
|
20
|
-
self.
|
|
19
|
+
self._rpc_channel = rpc_channel
|
|
21
20
|
|
|
22
21
|
async def send(self, command: B01_Q10_DP, params: ParamsType = None) -> Any:
|
|
23
22
|
"""Send a command to the device.
|
|
@@ -27,6 +26,6 @@ class CommandTrait:
|
|
|
27
26
|
caller to ensure that any traits affected by the command are refreshed
|
|
28
27
|
as needed.
|
|
29
28
|
"""
|
|
30
|
-
if not self.
|
|
29
|
+
if not self._rpc_channel:
|
|
31
30
|
raise ValueError("Device trait in invalid state")
|
|
32
|
-
return await send_command(
|
|
31
|
+
return await self._rpc_channel.send_command(command, params=params)
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
Potentially other devices may fall into this category in the future.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
6
|
from typing import Any
|
|
9
7
|
|
|
10
8
|
from roborock import B01Props
|
|
@@ -18,11 +16,10 @@ from roborock.data.b01_q7.b01_q7_code_mappings import (
|
|
|
18
16
|
SCWindMapping,
|
|
19
17
|
WaterLevelMapping,
|
|
20
18
|
)
|
|
21
|
-
from roborock.devices.rpc.b01_q7_channel import
|
|
19
|
+
from roborock.devices.rpc.b01_q7_channel import Q7MapRpcChannel, Q7RpcChannel
|
|
22
20
|
from roborock.devices.traits import Trait
|
|
23
|
-
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
24
21
|
from roborock.exceptions import RoborockException
|
|
25
|
-
from roborock.protocols.b01_q7_protocol import
|
|
22
|
+
from roborock.protocols.b01_q7_protocol import CommandType, ParamsType
|
|
26
23
|
from roborock.roborock_message import RoborockB01Props
|
|
27
24
|
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
28
25
|
|
|
@@ -53,10 +50,14 @@ class Q7PropertiesApi(Trait):
|
|
|
53
50
|
"""Trait for fetching parsed current map content."""
|
|
54
51
|
|
|
55
52
|
def __init__(
|
|
56
|
-
self,
|
|
53
|
+
self,
|
|
54
|
+
rpc_channel: Q7RpcChannel,
|
|
55
|
+
map_rpc_channel: Q7MapRpcChannel,
|
|
56
|
+
device: HomeDataDevice,
|
|
57
|
+
product: HomeDataProduct,
|
|
57
58
|
) -> None:
|
|
58
59
|
"""Initialize the Q7 API."""
|
|
59
|
-
self.
|
|
60
|
+
self._rpc_channel = rpc_channel
|
|
60
61
|
self._map_rpc_channel = map_rpc_channel
|
|
61
62
|
self._device = device
|
|
62
63
|
self._product = product
|
|
@@ -64,8 +65,8 @@ class Q7PropertiesApi(Trait):
|
|
|
64
65
|
if not device.sn or not product.model:
|
|
65
66
|
raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
|
|
66
67
|
|
|
67
|
-
self.clean_summary = CleanSummaryTrait(
|
|
68
|
-
self.map = MapTrait(
|
|
68
|
+
self.clean_summary = CleanSummaryTrait(rpc_channel)
|
|
69
|
+
self.map = MapTrait(rpc_channel)
|
|
69
70
|
self.map_content = MapContentTrait(
|
|
70
71
|
self._map_rpc_channel,
|
|
71
72
|
self.map,
|
|
@@ -199,17 +200,23 @@ class Q7PropertiesApi(Trait):
|
|
|
199
200
|
|
|
200
201
|
async def send(self, command: CommandType, params: ParamsType) -> Any:
|
|
201
202
|
"""Send a command to the device."""
|
|
202
|
-
return await
|
|
203
|
-
self._channel,
|
|
204
|
-
Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
|
|
205
|
-
)
|
|
203
|
+
return await self._rpc_channel.send_command(command, params)
|
|
206
204
|
|
|
207
205
|
|
|
208
|
-
def create(
|
|
206
|
+
def create(
|
|
207
|
+
product: HomeDataProduct,
|
|
208
|
+
device: HomeDataDevice,
|
|
209
|
+
rpc_channel: Q7RpcChannel,
|
|
210
|
+
map_rpc_channel: Q7MapRpcChannel,
|
|
211
|
+
) -> Q7PropertiesApi:
|
|
209
212
|
"""Create traits for B01 Q7 devices."""
|
|
210
213
|
if device.sn is None or product.model is None:
|
|
211
214
|
raise RoborockException(
|
|
212
|
-
f"Device serial number and product model are required (sn
|
|
215
|
+
f"Device serial number and product model are required (sn: {device.sn}, model: {product.model})"
|
|
213
216
|
)
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
return Q7PropertiesApi(
|
|
218
|
+
rpc_channel=rpc_channel,
|
|
219
|
+
map_rpc_channel=map_rpc_channel,
|
|
220
|
+
device=device,
|
|
221
|
+
product=product,
|
|
222
|
+
)
|
|
@@ -7,11 +7,9 @@ and a `record_list` whose items contain a JSON string in `detail`.
|
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
9
|
from roborock import CleanRecordDetail, CleanRecordList, CleanRecordSummary
|
|
10
|
-
from roborock.devices.rpc.b01_q7_channel import
|
|
10
|
+
from roborock.devices.rpc.b01_q7_channel import Q7RpcChannel
|
|
11
11
|
from roborock.devices.traits import Trait
|
|
12
|
-
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
13
12
|
from roborock.exceptions import RoborockException
|
|
14
|
-
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
|
|
15
13
|
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
16
14
|
|
|
17
15
|
__all__ = [
|
|
@@ -24,11 +22,11 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
24
22
|
class CleanSummaryTrait(CleanRecordSummary, Trait):
|
|
25
23
|
"""B01/Q7 clean summary + clean record access (via record list service)."""
|
|
26
24
|
|
|
27
|
-
def __init__(self, channel:
|
|
25
|
+
def __init__(self, channel: Q7RpcChannel) -> None:
|
|
28
26
|
"""Initialize the clean summary trait.
|
|
29
27
|
|
|
30
28
|
Args:
|
|
31
|
-
channel:
|
|
29
|
+
channel: RPC channel used to communicate with the device.
|
|
32
30
|
"""
|
|
33
31
|
super().__init__()
|
|
34
32
|
self._channel = channel
|
|
@@ -46,9 +44,9 @@ class CleanSummaryTrait(CleanRecordSummary, Trait):
|
|
|
46
44
|
|
|
47
45
|
async def _get_record_list(self) -> CleanRecordList:
|
|
48
46
|
"""Fetch the raw device clean record list (`service.get_record_list`)."""
|
|
49
|
-
result = await
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
result = await self._channel.send_command(
|
|
48
|
+
command=RoborockB01Q7Methods.GET_RECORD_LIST,
|
|
49
|
+
params={},
|
|
52
50
|
)
|
|
53
51
|
|
|
54
52
|
if not isinstance(result, dict):
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""Map trait for B01 Q7 devices."""
|
|
2
2
|
|
|
3
3
|
from roborock.data import Q7MapList
|
|
4
|
-
from roborock.devices.rpc.b01_q7_channel import
|
|
4
|
+
from roborock.devices.rpc.b01_q7_channel import Q7RpcChannel
|
|
5
5
|
from roborock.devices.traits import Trait
|
|
6
|
-
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
7
6
|
from roborock.exceptions import RoborockException
|
|
8
|
-
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
|
|
9
7
|
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
10
8
|
|
|
11
9
|
|
|
@@ -16,15 +14,15 @@ class MapTrait(Q7MapList, Trait):
|
|
|
16
14
|
current map ID to fetch.
|
|
17
15
|
"""
|
|
18
16
|
|
|
19
|
-
def __init__(self, channel:
|
|
17
|
+
def __init__(self, channel: Q7RpcChannel) -> None:
|
|
20
18
|
super().__init__()
|
|
21
19
|
self._channel = channel
|
|
22
20
|
|
|
23
21
|
async def refresh(self) -> None:
|
|
24
22
|
"""Refresh cached map list metadata from the device."""
|
|
25
|
-
response = await
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
response = await self._channel.send_command(
|
|
24
|
+
command=RoborockB01Q7Methods.GET_MAP_LIST,
|
|
25
|
+
params={},
|
|
28
26
|
)
|
|
29
27
|
if not isinstance(response, dict):
|
|
30
28
|
raise RoborockException(
|
|
@@ -14,11 +14,10 @@ from dataclasses import dataclass
|
|
|
14
14
|
from vacuum_map_parser_base.map_data import MapData
|
|
15
15
|
|
|
16
16
|
from roborock.data import RoborockBase
|
|
17
|
-
from roborock.devices.rpc.b01_q7_channel import
|
|
17
|
+
from roborock.devices.rpc.b01_q7_channel import Q7MapRpcChannel
|
|
18
18
|
from roborock.devices.traits import Trait
|
|
19
19
|
from roborock.exceptions import RoborockException
|
|
20
20
|
from roborock.map.b01_map_parser import B01MapParser, B01MapParserConfig
|
|
21
|
-
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
|
|
22
21
|
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
23
22
|
|
|
24
23
|
from .map import MapTrait
|
|
@@ -55,7 +54,7 @@ class MapContentTrait(MapContent, Trait):
|
|
|
55
54
|
|
|
56
55
|
def __init__(
|
|
57
56
|
self,
|
|
58
|
-
map_rpc_channel:
|
|
57
|
+
map_rpc_channel: Q7MapRpcChannel,
|
|
59
58
|
map_trait: MapTrait,
|
|
60
59
|
*,
|
|
61
60
|
map_parser_config: B01MapParserConfig | None = None,
|
|
@@ -77,13 +76,11 @@ class MapContentTrait(MapContent, Trait):
|
|
|
77
76
|
if (map_id := self._map_trait.current_map_id) is None:
|
|
78
77
|
raise RoborockException("Unable to determine current map ID")
|
|
79
78
|
|
|
80
|
-
request = Q7RequestMessage(
|
|
81
|
-
dps=B01_Q7_DPS,
|
|
82
|
-
command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
|
|
83
|
-
params={"map_id": map_id},
|
|
84
|
-
)
|
|
85
79
|
async with self._map_command_lock:
|
|
86
|
-
raw_payload = await self._map_rpc_channel.send_map_command(
|
|
80
|
+
raw_payload = await self._map_rpc_channel.send_map_command(
|
|
81
|
+
RoborockB01Q7Methods.UPLOAD_BY_MAPID,
|
|
82
|
+
{"map_id": map_id},
|
|
83
|
+
)
|
|
87
84
|
|
|
88
85
|
try:
|
|
89
86
|
parsed_data = self._map_parser.parse(raw_payload)
|
|
@@ -3,14 +3,19 @@ from functools import cached_property
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from roborock import (
|
|
6
|
+
CleaningMode,
|
|
6
7
|
CleanRoutes,
|
|
7
8
|
StatusV2,
|
|
8
9
|
VacuumModes,
|
|
9
10
|
WaterModes,
|
|
10
11
|
get_clean_modes,
|
|
11
12
|
get_clean_routes,
|
|
13
|
+
get_cleaning_mode_options,
|
|
14
|
+
get_cleaning_mode_parameters,
|
|
15
|
+
get_current_cleaning_mode,
|
|
12
16
|
get_water_mode_mapping,
|
|
13
17
|
get_water_modes,
|
|
18
|
+
resolve_cleaning_mode,
|
|
14
19
|
)
|
|
15
20
|
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
|
|
16
21
|
from roborock.roborock_message import RoborockDataProtocol
|
|
@@ -42,10 +47,11 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
|
|
|
42
47
|
- Water Mode
|
|
43
48
|
- Mop Route
|
|
44
49
|
|
|
45
|
-
You should
|
|
46
|
-
(i.e. fan_speed_options
|
|
47
|
-
Then you can
|
|
48
|
-
|
|
50
|
+
You should use the _options version of the attribute to know which are
|
|
51
|
+
supported for your device (i.e. fan_speed_options)
|
|
52
|
+
Then you can use the _mapping to convert an int value to the actual Enum.
|
|
53
|
+
(i.e. fan_speed_mapping)
|
|
54
|
+
You can use the _name property to get the str value of the enum. (i.e. fan_speed_name)
|
|
49
55
|
|
|
50
56
|
"""
|
|
51
57
|
|
|
@@ -83,6 +89,10 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
|
|
|
83
89
|
def mop_route_mapping(self) -> dict[int, str]:
|
|
84
90
|
return {route.code: route.value for route in self.mop_route_options}
|
|
85
91
|
|
|
92
|
+
@cached_property
|
|
93
|
+
def cleaning_mode_options(self) -> list[CleaningMode]:
|
|
94
|
+
return get_cleaning_mode_options(self._device_features_trait)
|
|
95
|
+
|
|
86
96
|
@property
|
|
87
97
|
def fan_speed_name(self) -> str | None:
|
|
88
98
|
if self.fan_power is None:
|
|
@@ -101,6 +111,28 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
|
|
|
101
111
|
return None
|
|
102
112
|
return self.mop_route_mapping.get(self.mop_mode)
|
|
103
113
|
|
|
114
|
+
@property
|
|
115
|
+
def current_cleaning_mode(self) -> CleaningMode | None:
|
|
116
|
+
return get_current_cleaning_mode(
|
|
117
|
+
clean_mode=self.fan_power,
|
|
118
|
+
water_mode=self.water_box_mode,
|
|
119
|
+
mop_mode=self.mop_mode,
|
|
120
|
+
features=self._device_features_trait,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def current_cleaning_mode_name(self) -> str | None:
|
|
125
|
+
if (cleaning_mode := self.current_cleaning_mode) is None:
|
|
126
|
+
return None
|
|
127
|
+
return cleaning_mode.value
|
|
128
|
+
|
|
129
|
+
async def set_cleaning_mode(self, cleaning_mode: str | CleaningMode) -> None:
|
|
130
|
+
"""Set the preferred high-level cleaning mode for the device."""
|
|
131
|
+
await self.rpc_channel.send_command(
|
|
132
|
+
RoborockCommand.SET_CLEAN_MOTOR_MODE,
|
|
133
|
+
params=get_cleaning_mode_parameters(resolve_cleaning_mode(cleaning_mode), self._device_features_trait),
|
|
134
|
+
)
|
|
135
|
+
|
|
104
136
|
def update_from_dps(self, decoded_dps: dict[RoborockDataProtocol, Any]) -> None:
|
|
105
137
|
"""Update the trait from data protocol push message data.
|
|
106
138
|
|
roborock/roborock_message.py
CHANGED
|
@@ -40,6 +40,45 @@ class RoborockDataProtocol(RoborockEnum):
|
|
|
40
40
|
raise ValueError(f"{key} not a valid key for Data Protocol")
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
class RoborockMowerDataProtocol(RoborockEnum):
|
|
44
|
+
UNKNOWN = 0
|
|
45
|
+
ERROR_CODE = 120
|
|
46
|
+
BATTERY = 121
|
|
47
|
+
MOW_TYPE = 122
|
|
48
|
+
MOW_STATE = 123
|
|
49
|
+
MAPPING_TYPE = 124
|
|
50
|
+
MAPPING_STATE = 125
|
|
51
|
+
OTA_STATE = 126
|
|
52
|
+
CHARGE_STATE = 127
|
|
53
|
+
DOCK_STATE = 128
|
|
54
|
+
CHARGE_TYPE = 129
|
|
55
|
+
PEND_TYPE = 130
|
|
56
|
+
REMOTE_STATE = 131
|
|
57
|
+
MOW_START_TYPE = 132
|
|
58
|
+
MOW_EFF_MODE = 133
|
|
59
|
+
MOW_HEIGHT = 134
|
|
60
|
+
MOW_DIRECTION_ANGLE = 135
|
|
61
|
+
MOW_PATTERN = 136
|
|
62
|
+
MOW_CONF_MODE = 137
|
|
63
|
+
OFFLINE_STATUS = 138
|
|
64
|
+
MOW_PROGRESS = 139
|
|
65
|
+
BLADE_LIFESPAN = 140
|
|
66
|
+
FC_STATE = 141
|
|
67
|
+
GPS_COORDINATE = 142
|
|
68
|
+
OFF_DOCK_NO_TASK_STATUS = 143
|
|
69
|
+
AFS_STATUS = 144
|
|
70
|
+
NETWORK_CHANNEL = 145
|
|
71
|
+
START = 201
|
|
72
|
+
DOCK = 202
|
|
73
|
+
PAUSE = 203
|
|
74
|
+
RESUME = 204
|
|
75
|
+
STOP = 205
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _missing_(cls: type[Self], key) -> Self:
|
|
79
|
+
raise ValueError(f"{key} not a valid key for Mower Data Protocol")
|
|
80
|
+
|
|
81
|
+
|
|
43
82
|
class RoborockDyadDataProtocol(RoborockEnum):
|
|
44
83
|
DRYING_STATUS = 134
|
|
45
84
|
START = 200
|
|
File without changes
|
|
File without changes
|
|
File without changes
|