python-roborock 2.37.0__tar.gz → 2.39.0__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.
- {python_roborock-2.37.0 → python_roborock-2.39.0}/PKG-INFO +1 -1
- {python_roborock-2.37.0 → python_roborock-2.39.0}/pyproject.toml +1 -1
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/__init__.py +1 -0
- python_roborock-2.39.0/roborock/b01_containers.py +417 -0
- python_roborock-2.39.0/roborock/callbacks.py +126 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/clean_modes.py +1 -14
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/cli.py +158 -2
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/code_mappings.py +21 -1
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/containers.py +3 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/device_features.py +1 -1
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/device_manager.py +2 -2
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/local_channel.py +5 -20
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/mqtt_channel.py +3 -13
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/traits/b01/props.py +3 -1
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/mqtt/roborock_session.py +7 -13
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/protocols/b01_protocol.py +10 -1
- {python_roborock-2.37.0 → python_roborock-2.39.0}/LICENSE +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/README.md +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/api.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/cloud_api.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/command_cache.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/const.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/README.md +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/a01_channel.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/b01_channel.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/cache.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/channel.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/device.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/traits/dyad.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/traits/status.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/traits/trait.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/traits/zeo.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/v1_channel.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/devices/v1_rpc_channel.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/exceptions.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/protocol.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/py.typed +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/roborock_future.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/roborock_message.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/util.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/web_api.py +0 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from .code_mappings import RoborockModeEnum
|
|
4
|
+
from .containers import RoborockBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkStatusMapping(RoborockModeEnum):
|
|
8
|
+
"""Maps the general status of the robot."""
|
|
9
|
+
|
|
10
|
+
SLEEPING = ("sleeping", 0)
|
|
11
|
+
WAITING_FOR_ORDERS = ("waiting_for_orders", 1)
|
|
12
|
+
PAUSED = ("paused", 2)
|
|
13
|
+
DOCKING = ("docking", 3)
|
|
14
|
+
CHARGING = ("charging", 4)
|
|
15
|
+
SWEEP_MOPING = ("sweep_moping", 5)
|
|
16
|
+
SWEEP_MOPING_2 = ("sweep_moping_2", 6)
|
|
17
|
+
MOPING = ("moping", 7)
|
|
18
|
+
UPDATING = ("updating", 8)
|
|
19
|
+
MOP_CLEANING = ("mop_cleaning", 9)
|
|
20
|
+
MOP_AIRDRYING = ("mop_airdrying", 10)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SCWindMapping(RoborockModeEnum):
|
|
24
|
+
"""Maps suction power levels."""
|
|
25
|
+
|
|
26
|
+
SILENCE = ("quiet", 0)
|
|
27
|
+
STANDARD = ("balanced", 1)
|
|
28
|
+
STRONG = ("turbo", 2)
|
|
29
|
+
SUPER_STRONG = ("max", 3)
|
|
30
|
+
MAX = ("max_plus", 4)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WaterLevelMapping(RoborockModeEnum):
|
|
34
|
+
"""Maps water flow levels."""
|
|
35
|
+
|
|
36
|
+
LOW = ("low", 0)
|
|
37
|
+
MEDIUM = ("medium", 1)
|
|
38
|
+
HIGH = ("high", 2)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CleanTypeMapping(RoborockModeEnum):
|
|
42
|
+
"""Maps the type of cleaning (Vacuum, Mop, or both)."""
|
|
43
|
+
|
|
44
|
+
VACUUM = ("vacuum", 0)
|
|
45
|
+
VAC_AND_MOP = ("vac_and_mop", 1)
|
|
46
|
+
MOP = ("mop", 2)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CleanRepeatMapping(RoborockModeEnum):
|
|
50
|
+
"""Maps the cleaning repeat parameter."""
|
|
51
|
+
|
|
52
|
+
ONCE = ("once", 0)
|
|
53
|
+
TWICE = ("twice", 1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WorkModeMapping(RoborockModeEnum):
|
|
57
|
+
"""Maps the detailed work modes of the robot."""
|
|
58
|
+
|
|
59
|
+
IDLE = ("idle", 0)
|
|
60
|
+
AUTO = ("auto", 1)
|
|
61
|
+
MANUAL = ("manual", 2)
|
|
62
|
+
AREA = ("area", 3)
|
|
63
|
+
AUTO_PAUSE = ("auto_pause", 4)
|
|
64
|
+
BACK_CHARGE = ("back_charge", 5)
|
|
65
|
+
POINT = ("point", 6)
|
|
66
|
+
NAVI = ("navi", 7)
|
|
67
|
+
AREA_PAUSE = ("area_pause", 8)
|
|
68
|
+
NAVI_PAUSE = ("navi_pause", 9)
|
|
69
|
+
GLOBAL_GO_HOME = ("global_go_home", 10)
|
|
70
|
+
GLOBAL_BROKEN = ("global_broken", 11)
|
|
71
|
+
NAVI_GO_HOME = ("navi_go_home", 12)
|
|
72
|
+
POINT_GO_HOME = ("point_go_home", 13)
|
|
73
|
+
NAVI_IDLE = ("navi_idle", 14)
|
|
74
|
+
SCREW = ("screw", 20)
|
|
75
|
+
SCREW_GO_HOME = ("screw_go_home", 21)
|
|
76
|
+
POINT_IDLE = ("point_idle", 22)
|
|
77
|
+
SCREW_IDLE = ("screw_idle", 23)
|
|
78
|
+
BORDER = ("border", 25)
|
|
79
|
+
BORDER_GO_HOME = ("border_go_home", 26)
|
|
80
|
+
BORDER_PAUSE = ("border_pause", 27)
|
|
81
|
+
BORDER_BROKEN = ("border_broken", 28)
|
|
82
|
+
BORDER_IDLE = ("border_idle", 29)
|
|
83
|
+
PLAN_AREA = ("plan_area", 30)
|
|
84
|
+
PLAN_AREA_PAUSE = ("plan_area_pause", 31)
|
|
85
|
+
PLAN_AREA_GO_HOME = ("plan_area_go_home", 32)
|
|
86
|
+
PLAN_AREA_BROKEN = ("plan_area_broken", 33)
|
|
87
|
+
PLAN_AREA_IDLE = ("plan_area_idle", 35)
|
|
88
|
+
MOPPING = ("mopping", 36)
|
|
89
|
+
MOPPING_PAUSE = ("mopping_pause", 37)
|
|
90
|
+
MOPPING_GO_HOME = ("mopping_go_home", 38)
|
|
91
|
+
MOPPING_BROKEN = ("mopping_broken", 39)
|
|
92
|
+
MOPPING_IDLE = ("mopping_idle", 40)
|
|
93
|
+
EXPLORING = ("exploring", 45)
|
|
94
|
+
EXPLORE_PAUSE = ("explore_pause", 46)
|
|
95
|
+
EXPLORE_GO_HOME = ("explore_go_home", 47)
|
|
96
|
+
EXPLORE_BROKEN = ("explore_broken", 48)
|
|
97
|
+
EXPLORE_IDLE = ("explore_idle", 49)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class StationActionMapping(RoborockModeEnum):
|
|
101
|
+
"""Maps actions for the cleaning/drying station."""
|
|
102
|
+
|
|
103
|
+
STOP_CLEAN_OR_AIRDRY = ("stop_clean_or_airdry", 0)
|
|
104
|
+
MOP_CLEAN = ("mop_clean", 1)
|
|
105
|
+
MOP_AIRDRY = ("mop_airdry", 2)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CleanTaskTypeMapping(RoborockModeEnum):
|
|
109
|
+
"""Maps the high-level type of cleaning task selected."""
|
|
110
|
+
|
|
111
|
+
ALL = ("full", 0)
|
|
112
|
+
ROOM = ("room", 1)
|
|
113
|
+
AREA = ("zones", 4)
|
|
114
|
+
ROOM_NORMAL = ("room_normal", 5)
|
|
115
|
+
CUSTOM_MODE = ("customize", 6)
|
|
116
|
+
ALL_CUSTOM = ("all_custom", 11)
|
|
117
|
+
AREA_CUSTOM = ("area_custom", 99)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CarpetModeMapping(RoborockModeEnum):
|
|
121
|
+
"""Maps carpet handling parameters."""
|
|
122
|
+
|
|
123
|
+
FOLLOW_GLOBAL = ("follow_global", 0)
|
|
124
|
+
ON = ("on", 1)
|
|
125
|
+
OFF = ("off", 2)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class NetStatus(RoborockBase):
|
|
130
|
+
"""Represents the network status of the device."""
|
|
131
|
+
|
|
132
|
+
rssi: str
|
|
133
|
+
loss: int
|
|
134
|
+
ping: int
|
|
135
|
+
ip: str
|
|
136
|
+
mac: str
|
|
137
|
+
ssid: str
|
|
138
|
+
frequency: int
|
|
139
|
+
bssid: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class OrderTotal(RoborockBase):
|
|
144
|
+
"""Represents the order total information."""
|
|
145
|
+
|
|
146
|
+
total: int
|
|
147
|
+
enable: int
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class Privacy(RoborockBase):
|
|
152
|
+
"""Represents the privacy settings of the device."""
|
|
153
|
+
|
|
154
|
+
ai_recognize: int
|
|
155
|
+
dirt_recognize: int
|
|
156
|
+
pet_recognize: int
|
|
157
|
+
carpet_turbo: int
|
|
158
|
+
carpet_avoid: int
|
|
159
|
+
carpet_show: int
|
|
160
|
+
map_uploads: int
|
|
161
|
+
ai_agent: int
|
|
162
|
+
ai_avoidance: int
|
|
163
|
+
record_uploads: int
|
|
164
|
+
along_floor: int
|
|
165
|
+
auto_upgrade: int
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class PvCharging(RoborockBase):
|
|
170
|
+
"""Represents the photovoltaic charging status."""
|
|
171
|
+
|
|
172
|
+
status: int
|
|
173
|
+
begin_time: int
|
|
174
|
+
end_time: int
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class Recommend(RoborockBase):
|
|
179
|
+
"""Represents cleaning recommendations."""
|
|
180
|
+
|
|
181
|
+
sill: int
|
|
182
|
+
wall: int
|
|
183
|
+
room_id: list[int] = field(default_factory=list)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class B01Fault(RoborockModeEnum):
|
|
187
|
+
"""B01 fault codes and their descriptions."""
|
|
188
|
+
|
|
189
|
+
F_0 = ("fault_0", 0)
|
|
190
|
+
F_407 = ("cleaning_in_progress", 407) # Cleaning in progress. Scheduled cleanup ignored.
|
|
191
|
+
F_500 = (
|
|
192
|
+
"lidar_blocked",
|
|
193
|
+
500,
|
|
194
|
+
) # LiDAR turret or laser blocked. Check for obstruction and retry. LiDAR sensor obstructed or stuck.
|
|
195
|
+
# Remove foreign objects if any. If the problem persists, move the robot away and restart.
|
|
196
|
+
F_501 = (
|
|
197
|
+
"robot_suspended",
|
|
198
|
+
501,
|
|
199
|
+
) # Robot suspended. Move the robot away and restart. Cliff sensors dirty. Wipe them clean.
|
|
200
|
+
F_502 = (
|
|
201
|
+
"low_battery",
|
|
202
|
+
502,
|
|
203
|
+
) # Low battery. Recharge now. Battery low. Put the robot on the dock to charge it to 20% before starting.
|
|
204
|
+
F_503 = (
|
|
205
|
+
"dustbin_not_installed",
|
|
206
|
+
503,
|
|
207
|
+
) # Check that the dustbin and filter are installed properly. Reinstall the dustbin and filter in place.
|
|
208
|
+
# If the problem persists, replace the filter.
|
|
209
|
+
F_504 = ("fault_504", 504)
|
|
210
|
+
F_505 = ("fault_505", 505)
|
|
211
|
+
F_506 = ("fault_506", 506)
|
|
212
|
+
F_507 = ("fault_507", 507)
|
|
213
|
+
F_508 = ("fault_508", 508)
|
|
214
|
+
F_509 = ("cliff_sensor_error", 509) # Cliff sensors error. Clean them, move the robot away from drops, and restart.
|
|
215
|
+
F_510 = (
|
|
216
|
+
"bumper_stuck",
|
|
217
|
+
510,
|
|
218
|
+
) # Bumper stuck. Clean it and lightly tap to release it. Tap it repeatedly to release it. If no foreign object
|
|
219
|
+
# exists, move the robot away and restart.
|
|
220
|
+
F_511 = (
|
|
221
|
+
"docking_error",
|
|
222
|
+
511,
|
|
223
|
+
) # Docking error. Put the robot on the dock. Clear obstacles around the dock, clean charging contacts, and put
|
|
224
|
+
# the robot on the dock.
|
|
225
|
+
F_512 = (
|
|
226
|
+
"docking_error",
|
|
227
|
+
512,
|
|
228
|
+
) # Docking error. Put the robot on the dock. Clear obstacles around the dock, clean charging contacts, and put
|
|
229
|
+
# the robot on the dock.
|
|
230
|
+
F_513 = (
|
|
231
|
+
"robot_trapped",
|
|
232
|
+
513,
|
|
233
|
+
) # Robot trapped. Move the robot away and restart. Clear obstacles around robot or move robot away and restart.
|
|
234
|
+
F_514 = (
|
|
235
|
+
"robot_trapped",
|
|
236
|
+
514,
|
|
237
|
+
) # Robot trapped. Move the robot away and restart. Clear obstacles around robot or move robot away and restart.
|
|
238
|
+
F_515 = ("fault_515", 515)
|
|
239
|
+
F_517 = ("fault_517", 517)
|
|
240
|
+
F_518 = (
|
|
241
|
+
"low_battery",
|
|
242
|
+
518,
|
|
243
|
+
) # Low battery. Recharge now. Battery low. Put the robot on the dock to charge it to 20% before starting.
|
|
244
|
+
F_519 = ("fault_519", 519)
|
|
245
|
+
F_520 = ("fault_520", 520)
|
|
246
|
+
F_521 = ("fault_521", 521)
|
|
247
|
+
F_522 = ("mop_not_installed", 522) # Check that the mop is properly installed. Mop not installed. Reinstall it.
|
|
248
|
+
F_523 = ("fault_523", 523)
|
|
249
|
+
F_525 = ("fault_525", 525)
|
|
250
|
+
F_526 = ("fault_526", 526)
|
|
251
|
+
F_527 = ("fault_527", 527)
|
|
252
|
+
F_528 = ("fault_528", 528)
|
|
253
|
+
F_529 = ("fault_529", 529)
|
|
254
|
+
F_530 = ("fault_530", 530)
|
|
255
|
+
F_531 = ("fault_531", 531)
|
|
256
|
+
F_532 = ("fault_532", 532)
|
|
257
|
+
F_533 = ("long_sleep", 533) # About to shut down after a long time of sleep. Charge the robot.
|
|
258
|
+
F_534 = (
|
|
259
|
+
"low_battery_shutdown",
|
|
260
|
+
534,
|
|
261
|
+
) # Low battery. Turning off. About to shut down due to low battery. Charge the robot.
|
|
262
|
+
F_535 = ("fault_535", 535)
|
|
263
|
+
F_536 = ("fault_536", 536)
|
|
264
|
+
F_540 = ("fault_540", 540)
|
|
265
|
+
F_541 = ("fault_541", 541)
|
|
266
|
+
F_542 = ("fault_542", 542)
|
|
267
|
+
F_550 = ("fault_550", 550)
|
|
268
|
+
F_551 = ("fault_551", 551)
|
|
269
|
+
F_559 = ("fault_559", 559)
|
|
270
|
+
F_560 = ("side_brush_entangled", 560) # Side brush entangled. Remove and clean it.
|
|
271
|
+
F_561 = ("fault_561", 561)
|
|
272
|
+
F_562 = ("fault_562", 562)
|
|
273
|
+
F_563 = ("fault_563", 563)
|
|
274
|
+
F_564 = ("fault_564", 564)
|
|
275
|
+
F_565 = ("fault_565", 565)
|
|
276
|
+
F_566 = ("fault_566", 566)
|
|
277
|
+
F_567 = ("fault_567", 567)
|
|
278
|
+
F_568 = ("main_wheels_entangled", 568) # Clean main wheels, move the robot away and restart.
|
|
279
|
+
F_569 = ("main_wheels_entangled", 569) # Clean main wheels, move the robot away and restart.
|
|
280
|
+
F_570 = ("main_brush_entangled", 570) # Main brush entangled. Remove and clean it and its bearing.
|
|
281
|
+
F_571 = ("fault_571", 571)
|
|
282
|
+
F_572 = ("main_brush_entangled", 572) # Main brush entangled. Remove and clean it and its bearing.
|
|
283
|
+
F_573 = ("fault_573", 573)
|
|
284
|
+
F_574 = ("fault_574", 574)
|
|
285
|
+
F_580 = ("fault_580", 580)
|
|
286
|
+
F_581 = ("fault_581", 581)
|
|
287
|
+
F_582 = ("fault_582", 582)
|
|
288
|
+
F_583 = ("fault_583", 583)
|
|
289
|
+
F_584 = ("fault_584", 584)
|
|
290
|
+
F_585 = ("fault_585", 585)
|
|
291
|
+
F_586 = ("fault_586", 586)
|
|
292
|
+
F_587 = ("fault_587", 587)
|
|
293
|
+
F_588 = ("fault_588", 588)
|
|
294
|
+
F_589 = ("fault_589", 589)
|
|
295
|
+
F_590 = ("fault_590", 590)
|
|
296
|
+
F_591 = ("fault_591", 591)
|
|
297
|
+
F_592 = ("fault_592", 592)
|
|
298
|
+
F_593 = ("fault_593", 593)
|
|
299
|
+
F_594 = (
|
|
300
|
+
"dust_bag_not_installed",
|
|
301
|
+
594,
|
|
302
|
+
) # Make sure the dust bag is properly installed. Dust bag not installed. Check that it is installed properly.
|
|
303
|
+
F_601 = ("fault_601", 601)
|
|
304
|
+
F_602 = ("fault_602", 602)
|
|
305
|
+
F_603 = ("fault_603", 603)
|
|
306
|
+
F_604 = ("fault_604", 604)
|
|
307
|
+
F_605 = ("fault_605", 605)
|
|
308
|
+
F_611 = ("positioning_failed", 611) # Positioning failed. Move the robot back to the dock and remap.
|
|
309
|
+
F_612 = (
|
|
310
|
+
"map_changed",
|
|
311
|
+
612,
|
|
312
|
+
) # Map changed. Positioning failed. Try again. New environment detected. Map changed. Positioning failed.
|
|
313
|
+
# Try again after remapping.
|
|
314
|
+
F_629 = ("mop_mount_fell_off", 629) # Mop cloth mount fell off. Reinstall it to resume working.
|
|
315
|
+
F_668 = (
|
|
316
|
+
"system_error",
|
|
317
|
+
668,
|
|
318
|
+
) # Robot error. Reset the system. Fan error. Reset the system. If the problem persists, contact customer service.
|
|
319
|
+
F_2000 = ("fault_2000", 2000)
|
|
320
|
+
F_2003 = ("low_battery_schedule_canceled", 2003) # Battery level below 20%. Scheduled task canceled.
|
|
321
|
+
F_2007 = (
|
|
322
|
+
"cannot_reach_target",
|
|
323
|
+
2007,
|
|
324
|
+
) # Unable to reach the target. Cleaning ended. Ensure the door to the target area is open or unobstructed.
|
|
325
|
+
F_2012 = (
|
|
326
|
+
"cannot_reach_target",
|
|
327
|
+
2012,
|
|
328
|
+
) # Unable to reach the target. Cleaning ended. Ensure the door to the target area is open or unobstructed.
|
|
329
|
+
F_2013 = ("fault_2013", 2013)
|
|
330
|
+
F_2015 = ("fault_2015", 2015)
|
|
331
|
+
F_2017 = ("fault_2017", 2017)
|
|
332
|
+
F_2100 = (
|
|
333
|
+
"low_battery_resume_later",
|
|
334
|
+
2100,
|
|
335
|
+
) # Low battery. Resume cleaning after recharging. Low battery. Starting to recharge. Resume cleaning after
|
|
336
|
+
# charging.
|
|
337
|
+
F_2101 = ("fault_2101", 2101)
|
|
338
|
+
F_2102 = ("cleaning_complete", 2102) # Cleaning completed. Returning to the dock.
|
|
339
|
+
F_2103 = ("fault_2103", 2103)
|
|
340
|
+
F_2104 = ("fault_2104", 2104)
|
|
341
|
+
F_2105 = ("fault_2105", 2105)
|
|
342
|
+
F_2108 = ("fault_2108", 2108)
|
|
343
|
+
F_2109 = ("fault_2109", 2109)
|
|
344
|
+
F_2110 = ("fault_2110", 2110)
|
|
345
|
+
F_2111 = ("fault_2111", 2111)
|
|
346
|
+
F_2112 = ("fault_2112", 2112)
|
|
347
|
+
F_2113 = ("fault_2113", 2113)
|
|
348
|
+
F_2114 = ("fault_2114", 2114)
|
|
349
|
+
F_2115 = ("fault_2115", 2115)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass
|
|
353
|
+
class B01Props(RoborockBase):
|
|
354
|
+
"""
|
|
355
|
+
Represents the complete properties and status for a Roborock B01 model.
|
|
356
|
+
This dataclass is generated based on the device's status JSON object.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
status: WorkStatusMapping
|
|
360
|
+
fault: B01Fault
|
|
361
|
+
wind: SCWindMapping
|
|
362
|
+
water: int
|
|
363
|
+
mode: int
|
|
364
|
+
quantity: int
|
|
365
|
+
alarm: int
|
|
366
|
+
volume: int
|
|
367
|
+
hypa: int
|
|
368
|
+
main_brush: int
|
|
369
|
+
side_brush: int
|
|
370
|
+
mop_life: int
|
|
371
|
+
main_sensor: int
|
|
372
|
+
net_status: NetStatus
|
|
373
|
+
repeat_state: int
|
|
374
|
+
tank_state: int
|
|
375
|
+
sweep_type: int
|
|
376
|
+
clean_path_preference: int
|
|
377
|
+
cloth_state: int
|
|
378
|
+
time_zone: int
|
|
379
|
+
time_zone_info: str
|
|
380
|
+
language: int
|
|
381
|
+
cleaning_time: int
|
|
382
|
+
real_clean_time: int
|
|
383
|
+
cleaning_area: int
|
|
384
|
+
custom_type: int
|
|
385
|
+
sound: int
|
|
386
|
+
work_mode: WorkModeMapping
|
|
387
|
+
station_act: int
|
|
388
|
+
charge_state: int
|
|
389
|
+
current_map_id: int
|
|
390
|
+
map_num: int
|
|
391
|
+
dust_action: int
|
|
392
|
+
quiet_is_open: int
|
|
393
|
+
quiet_begin_time: int
|
|
394
|
+
quiet_end_time: int
|
|
395
|
+
clean_finish: int
|
|
396
|
+
voice_type: int
|
|
397
|
+
voice_type_version: int
|
|
398
|
+
order_total: OrderTotal
|
|
399
|
+
build_map: int
|
|
400
|
+
privacy: Privacy
|
|
401
|
+
dust_auto_state: int
|
|
402
|
+
dust_frequency: int
|
|
403
|
+
child_lock: int
|
|
404
|
+
multi_floor: int
|
|
405
|
+
map_save: int
|
|
406
|
+
light_mode: int
|
|
407
|
+
green_laser: int
|
|
408
|
+
dust_bag_used: int
|
|
409
|
+
order_save_mode: int
|
|
410
|
+
manufacturer: str
|
|
411
|
+
back_to_wash: int
|
|
412
|
+
charge_station_type: int
|
|
413
|
+
pv_cut_charge: int
|
|
414
|
+
pv_charging: PvCharging
|
|
415
|
+
serial_number: str
|
|
416
|
+
recommend: Recommend
|
|
417
|
+
add_sweep_status: int
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Module for managing callback utility functions."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
_LOGGER = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
K = TypeVar("K")
|
|
10
|
+
V = TypeVar("V")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def safe_callback(callback: Callable[[V], None], logger: logging.Logger | None = None) -> Callable[[V], None]:
|
|
14
|
+
"""Wrap a callback to catch and log exceptions.
|
|
15
|
+
|
|
16
|
+
This is useful for ensuring that errors in callbacks do not propagate
|
|
17
|
+
and cause unexpected behavior. Any failures during callback execution will be logged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
if logger is None:
|
|
21
|
+
logger = _LOGGER
|
|
22
|
+
|
|
23
|
+
def wrapper(value: V) -> None:
|
|
24
|
+
try:
|
|
25
|
+
callback(value)
|
|
26
|
+
except Exception as ex: # noqa: BLE001
|
|
27
|
+
logger.error("Uncaught error in callback '%s': %s", callback.__name__, ex)
|
|
28
|
+
|
|
29
|
+
return wrapper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CallbackMap(Generic[K, V]):
|
|
33
|
+
"""A mapping of callbacks for specific keys.
|
|
34
|
+
|
|
35
|
+
This allows for registering multiple callbacks for different keys and invoking them
|
|
36
|
+
when a value is received for a specific key.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, logger: logging.Logger | None = None) -> None:
|
|
40
|
+
self._callbacks: dict[K, list[Callable[[V], None]]] = {}
|
|
41
|
+
self._logger = logger or _LOGGER
|
|
42
|
+
|
|
43
|
+
def keys(self) -> list[K]:
|
|
44
|
+
"""Get all keys in the callback map."""
|
|
45
|
+
return list(self._callbacks.keys())
|
|
46
|
+
|
|
47
|
+
def add_callback(self, key: K, callback: Callable[[V], None]) -> Callable[[], None]:
|
|
48
|
+
"""Add a callback for a specific key.
|
|
49
|
+
|
|
50
|
+
Any failures during callback execution will be logged.
|
|
51
|
+
|
|
52
|
+
Returns a callable that can be used to remove the callback.
|
|
53
|
+
"""
|
|
54
|
+
self._callbacks.setdefault(key, []).append(callback)
|
|
55
|
+
|
|
56
|
+
def remove_callback() -> None:
|
|
57
|
+
"""Remove the callback for the specific key."""
|
|
58
|
+
if cb_list := self._callbacks.get(key):
|
|
59
|
+
cb_list.remove(callback)
|
|
60
|
+
if not cb_list:
|
|
61
|
+
del self._callbacks[key]
|
|
62
|
+
|
|
63
|
+
return remove_callback
|
|
64
|
+
|
|
65
|
+
def get_callbacks(self, key: K) -> list[Callable[[V], None]]:
|
|
66
|
+
"""Get all callbacks for a specific key."""
|
|
67
|
+
return self._callbacks.get(key, [])
|
|
68
|
+
|
|
69
|
+
def __call__(self, key: K, value: V) -> None:
|
|
70
|
+
"""Invoke all callbacks for a specific key."""
|
|
71
|
+
for callback in self.get_callbacks(key):
|
|
72
|
+
safe_callback(callback, self._logger)(value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CallbackList(Generic[V]):
|
|
76
|
+
"""A list of callbacks that can be invoked.
|
|
77
|
+
|
|
78
|
+
This combines a list of callbacks into a single callable. Callers can add
|
|
79
|
+
additional callbacks to the list at any time.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, logger: logging.Logger | None = None) -> None:
|
|
83
|
+
self._callbacks: list[Callable[[V], None]] = []
|
|
84
|
+
self._logger = logger or _LOGGER
|
|
85
|
+
|
|
86
|
+
def add_callback(self, callback: Callable[[V], None]) -> Callable[[], None]:
|
|
87
|
+
"""Add a callback to the list.
|
|
88
|
+
|
|
89
|
+
Any failures during callback execution will be logged.
|
|
90
|
+
|
|
91
|
+
Returns a callable that can be used to remove the callback.
|
|
92
|
+
"""
|
|
93
|
+
self._callbacks.append(callback)
|
|
94
|
+
|
|
95
|
+
return lambda: self._callbacks.remove(callback)
|
|
96
|
+
|
|
97
|
+
def __call__(self, value: V) -> None:
|
|
98
|
+
"""Invoke all callbacks in the list."""
|
|
99
|
+
for callback in self._callbacks:
|
|
100
|
+
safe_callback(callback, self._logger)(value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decoder_callback(
|
|
104
|
+
decoder: Callable[[K], list[V]], callback: Callable[[V], None], logger: logging.Logger | None = None
|
|
105
|
+
) -> Callable[[K], None]:
|
|
106
|
+
"""Create a callback that decodes messages using a decoder and invokes a callback.
|
|
107
|
+
|
|
108
|
+
The decoder converts a value into a list of values. The callback is then invoked
|
|
109
|
+
for each value in the list.
|
|
110
|
+
|
|
111
|
+
Any failures during decoding or invoking the callbacks will be logged.
|
|
112
|
+
"""
|
|
113
|
+
if logger is None:
|
|
114
|
+
logger = _LOGGER
|
|
115
|
+
|
|
116
|
+
safe_cb = safe_callback(callback, logger)
|
|
117
|
+
|
|
118
|
+
def wrapper(data: K) -> None:
|
|
119
|
+
if not (messages := decoder(data)):
|
|
120
|
+
logger.warning("Failed to decode message: %s", data)
|
|
121
|
+
return
|
|
122
|
+
for message in messages:
|
|
123
|
+
_LOGGER.debug("Decoded message: %s", message)
|
|
124
|
+
safe_cb(message)
|
|
125
|
+
|
|
126
|
+
return wrapper
|
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from enum import StrEnum
|
|
4
|
-
|
|
5
3
|
from roborock import DeviceFeatures
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
class RoborockModeEnum(StrEnum):
|
|
9
|
-
"""A custom StrEnum that also stores an integer code for each member."""
|
|
10
|
-
|
|
11
|
-
code: int
|
|
12
|
-
|
|
13
|
-
def __new__(cls, value: str, code: int) -> RoborockModeEnum:
|
|
14
|
-
"""Creates a new enum member."""
|
|
15
|
-
member = str.__new__(cls, value)
|
|
16
|
-
member._value_ = value
|
|
17
|
-
member.code = code
|
|
18
|
-
return member
|
|
5
|
+
from .code_mappings import RoborockModeEnum
|
|
19
6
|
|
|
20
7
|
|
|
21
8
|
class CleanModes(RoborockModeEnum):
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
|
+
import yaml
|
|
9
10
|
from pyshark import FileCapture # type: ignore
|
|
10
11
|
from pyshark.capture.live_capture import LiveCapture, UnknownInterfaceException # type: ignore
|
|
11
12
|
from pyshark.packet.packet import Packet # type: ignore
|
|
12
13
|
|
|
13
|
-
from roborock import RoborockException
|
|
14
|
+
from roborock import SHORT_MODEL_TO_ENUM, DeviceFeatures, RoborockCommand, RoborockException
|
|
14
15
|
from roborock.containers import DeviceData, HomeData, HomeDataProduct, LoginData, NetworkInfo, RoborockBase, UserData
|
|
15
16
|
from roborock.devices.cache import Cache, CacheData
|
|
16
17
|
from roborock.devices.device_manager import create_device_manager, create_home_data_api
|
|
@@ -325,6 +326,159 @@ async def parser(_, local_key, device_ip, file):
|
|
|
325
326
|
)
|
|
326
327
|
|
|
327
328
|
|
|
329
|
+
@click.command()
|
|
330
|
+
@click.pass_context
|
|
331
|
+
@run_sync()
|
|
332
|
+
async def get_device_info(ctx: click.Context):
|
|
333
|
+
"""
|
|
334
|
+
Connects to devices and prints their feature information in YAML format.
|
|
335
|
+
"""
|
|
336
|
+
click.echo("Discovering devices...")
|
|
337
|
+
context: RoborockContext = await _load_and_discover(ctx)
|
|
338
|
+
cache_data = context.cache_data()
|
|
339
|
+
|
|
340
|
+
home_data = cache_data.home_data
|
|
341
|
+
|
|
342
|
+
all_devices = home_data.devices + home_data.received_devices
|
|
343
|
+
if not all_devices:
|
|
344
|
+
click.echo("No devices found.")
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
click.echo(f"Found {len(all_devices)} devices. Fetching data...")
|
|
348
|
+
|
|
349
|
+
all_products_data = {}
|
|
350
|
+
|
|
351
|
+
for device in all_devices:
|
|
352
|
+
click.echo(f" - Processing {device.name} ({device.duid})")
|
|
353
|
+
product_info = home_data.product_map[device.product_id]
|
|
354
|
+
device_data = DeviceData(device, product_info.model)
|
|
355
|
+
mqtt_client = RoborockMqttClientV1(cache_data.user_data, device_data)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
init_status_result = await mqtt_client.send_command(
|
|
359
|
+
RoborockCommand.APP_GET_INIT_STATUS,
|
|
360
|
+
)
|
|
361
|
+
product_nickname = SHORT_MODEL_TO_ENUM.get(product_info.model.split(".")[-1]).name
|
|
362
|
+
current_product_data = {
|
|
363
|
+
"Protocol Version": device.pv,
|
|
364
|
+
"Product Nickname": product_nickname,
|
|
365
|
+
"New Feature Info": init_status_result.get("new_feature_info"),
|
|
366
|
+
"New Feature Info Str": init_status_result.get("new_feature_info_str"),
|
|
367
|
+
"Feature Info": init_status_result.get("feature_info"),
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
all_products_data[product_info.model] = current_product_data
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
click.echo(f" - Error processing device {device.name}: {e}", err=True)
|
|
374
|
+
finally:
|
|
375
|
+
await mqtt_client.async_release()
|
|
376
|
+
|
|
377
|
+
if all_products_data:
|
|
378
|
+
click.echo("\n--- Device Information (copy to your YAML file) ---\n")
|
|
379
|
+
# Use yaml.dump to print in a clean, copy-paste friendly format
|
|
380
|
+
click.echo(yaml.dump(all_products_data, sort_keys=False))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@click.command()
|
|
384
|
+
@click.option("--data-file", default="../device_info.yaml", help="Path to the YAML file with device feature data.")
|
|
385
|
+
@click.option("--output-file", default="../SUPPORTED_FEATURES.md", help="Path to the output markdown file.")
|
|
386
|
+
def update_docs(data_file: str, output_file: str):
|
|
387
|
+
"""
|
|
388
|
+
Generates a markdown file by processing raw feature data from a YAML file.
|
|
389
|
+
"""
|
|
390
|
+
data_path = Path(data_file)
|
|
391
|
+
output_path = Path(output_file)
|
|
392
|
+
|
|
393
|
+
if not data_path.exists():
|
|
394
|
+
click.echo(f"Error: Data file not found at '{data_path}'", err=True)
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
click.echo(f"Loading data from {data_path}...")
|
|
398
|
+
with open(data_path, encoding="utf-8") as f:
|
|
399
|
+
product_data_from_yaml = yaml.safe_load(f)
|
|
400
|
+
|
|
401
|
+
if not product_data_from_yaml:
|
|
402
|
+
click.echo("No data found in YAML file. Exiting.", err=True)
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
product_features_map = {}
|
|
406
|
+
all_feature_names = set()
|
|
407
|
+
|
|
408
|
+
# Process the raw data from YAML to build the feature map
|
|
409
|
+
for model, data in product_data_from_yaml.items():
|
|
410
|
+
# Reconstruct the DeviceFeatures object from the raw data in the YAML file
|
|
411
|
+
device_features = DeviceFeatures.from_feature_flags(
|
|
412
|
+
new_feature_info=data.get("New Feature Info"),
|
|
413
|
+
new_feature_info_str=data.get("New Feature Info Str"),
|
|
414
|
+
feature_info=data.get("Feature Info"),
|
|
415
|
+
product_nickname=data.get("Product Nickname"),
|
|
416
|
+
)
|
|
417
|
+
features_dict = asdict(device_features)
|
|
418
|
+
|
|
419
|
+
# This dictionary will hold the final data for the markdown table row
|
|
420
|
+
current_product_data = {
|
|
421
|
+
"Product Nickname": data.get("Product Nickname", ""),
|
|
422
|
+
"Protocol Version": data.get("Protocol Version", ""),
|
|
423
|
+
"New Feature Info": data.get("New Feature Info", ""),
|
|
424
|
+
"New Feature Info Str": data.get("New Feature Info Str", ""),
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
# Populate features from the calculated DeviceFeatures object
|
|
428
|
+
for feature, is_supported in features_dict.items():
|
|
429
|
+
all_feature_names.add(feature)
|
|
430
|
+
if is_supported:
|
|
431
|
+
current_product_data[feature] = "X"
|
|
432
|
+
|
|
433
|
+
supported_codes = data.get("Feature Info", [])
|
|
434
|
+
if isinstance(supported_codes, list):
|
|
435
|
+
for code in supported_codes:
|
|
436
|
+
feature_name = str(code)
|
|
437
|
+
all_feature_names.add(feature_name)
|
|
438
|
+
current_product_data[feature_name] = "X"
|
|
439
|
+
|
|
440
|
+
product_features_map[model] = current_product_data
|
|
441
|
+
|
|
442
|
+
# --- Helper function to write the markdown table ---
|
|
443
|
+
def write_markdown_table(product_features: dict[str, dict[str, any]], all_features: set[str]):
|
|
444
|
+
"""Writes the data into a markdown table (products as columns)."""
|
|
445
|
+
sorted_products = sorted(product_features.keys())
|
|
446
|
+
special_rows = [
|
|
447
|
+
"Product Nickname",
|
|
448
|
+
"Protocol Version",
|
|
449
|
+
"New Feature Info",
|
|
450
|
+
"New Feature Info Str",
|
|
451
|
+
]
|
|
452
|
+
# Regular features are the remaining keys, sorted alphabetically
|
|
453
|
+
# We filter out the special rows to avoid duplicating them.
|
|
454
|
+
sorted_features = sorted(list(all_features - set(special_rows)))
|
|
455
|
+
|
|
456
|
+
header = ["Feature"] + sorted_products
|
|
457
|
+
|
|
458
|
+
click.echo(f"Writing documentation to {output_path}...")
|
|
459
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
460
|
+
f.write("| " + " | ".join(header) + " |\n")
|
|
461
|
+
f.write("|" + "---|" * len(header) + "\n")
|
|
462
|
+
|
|
463
|
+
# Write the special metadata rows first
|
|
464
|
+
for row_name in special_rows:
|
|
465
|
+
row_values = [str(product_features[p].get(row_name, "")) for p in sorted_products]
|
|
466
|
+
f.write("| " + " | ".join([row_name] + row_values) + " |\n")
|
|
467
|
+
|
|
468
|
+
# Write the feature rows
|
|
469
|
+
for feature in sorted_features:
|
|
470
|
+
# Use backticks for feature names that are just numbers (from the list)
|
|
471
|
+
display_feature = f"`{feature}`"
|
|
472
|
+
feature_row = [display_feature]
|
|
473
|
+
for product in sorted_products:
|
|
474
|
+
# Use .get() to place an 'X' or an empty string
|
|
475
|
+
feature_row.append(product_features[product].get(feature, ""))
|
|
476
|
+
f.write("| " + " | ".join(feature_row) + " |\n")
|
|
477
|
+
|
|
478
|
+
write_markdown_table(product_features_map, all_feature_names)
|
|
479
|
+
click.echo("Done.")
|
|
480
|
+
|
|
481
|
+
|
|
328
482
|
cli.add_command(login)
|
|
329
483
|
cli.add_command(discover)
|
|
330
484
|
cli.add_command(list_devices)
|
|
@@ -334,6 +488,8 @@ cli.add_command(status)
|
|
|
334
488
|
cli.add_command(command)
|
|
335
489
|
cli.add_command(parser)
|
|
336
490
|
cli.add_command(session)
|
|
491
|
+
cli.add_command(get_device_info)
|
|
492
|
+
cli.add_command(update_docs)
|
|
337
493
|
|
|
338
494
|
|
|
339
495
|
def main():
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from collections import namedtuple
|
|
5
|
-
from enum import Enum, IntEnum
|
|
5
|
+
from enum import Enum, IntEnum, StrEnum
|
|
6
6
|
|
|
7
7
|
_LOGGER = logging.getLogger(__name__)
|
|
8
8
|
completed_warnings = set()
|
|
@@ -51,6 +51,26 @@ class RoborockEnum(IntEnum):
|
|
|
51
51
|
return cls.as_dict().items()
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
class RoborockModeEnum(StrEnum):
|
|
55
|
+
"""A custom StrEnum that also stores an integer code for each member."""
|
|
56
|
+
|
|
57
|
+
code: int
|
|
58
|
+
|
|
59
|
+
def __new__(cls, value: str, code: int) -> RoborockModeEnum:
|
|
60
|
+
"""Creates a new enum member."""
|
|
61
|
+
member = str.__new__(cls, value)
|
|
62
|
+
member._value_ = value
|
|
63
|
+
member.code = code
|
|
64
|
+
return member
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_code(cls, code: int):
|
|
68
|
+
for member in cls:
|
|
69
|
+
if member.code == code:
|
|
70
|
+
return member
|
|
71
|
+
raise ValueError(f"{code} is not a valid code for {cls.__name__}")
|
|
72
|
+
|
|
73
|
+
|
|
54
74
|
ProductInfo = namedtuple("ProductInfo", ["nickname", "short_models"])
|
|
55
75
|
|
|
56
76
|
|
|
@@ -33,6 +33,7 @@ from .code_mappings import (
|
|
|
33
33
|
RoborockFanSpeedSaros10R,
|
|
34
34
|
RoborockFinishReason,
|
|
35
35
|
RoborockInCleaning,
|
|
36
|
+
RoborockModeEnum,
|
|
36
37
|
RoborockMopIntensityCode,
|
|
37
38
|
RoborockMopIntensityP10,
|
|
38
39
|
RoborockMopIntensityQ7Max,
|
|
@@ -120,6 +121,8 @@ class RoborockBase:
|
|
|
120
121
|
return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
|
|
121
122
|
if issubclass(class_type, RoborockBase):
|
|
122
123
|
return class_type.from_dict(value)
|
|
124
|
+
if issubclass(class_type, RoborockModeEnum):
|
|
125
|
+
return class_type.from_code(value)
|
|
123
126
|
if class_type is Any:
|
|
124
127
|
return value
|
|
125
128
|
return class_type(value) # type: ignore[call-arg]
|
|
@@ -162,8 +162,8 @@ async def create_device_manager(
|
|
|
162
162
|
case _:
|
|
163
163
|
raise NotImplementedError(f"Device {device.name} has unsupported category {product.category}")
|
|
164
164
|
case DeviceVersion.B01:
|
|
165
|
-
|
|
166
|
-
traits.append(B01PropsApi(
|
|
165
|
+
channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
|
|
166
|
+
traits.append(B01PropsApi(channel))
|
|
167
167
|
case _:
|
|
168
168
|
raise NotImplementedError(f"Device {device.name} has unsupported version {device.pv}")
|
|
169
169
|
return RoborockDevice(device, channel, traits)
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
|
|
8
|
+
from roborock.callbacks import CallbackList, decoder_callback
|
|
8
9
|
from roborock.exceptions import RoborockConnectionException, RoborockException
|
|
9
10
|
from roborock.protocol import Decoder, Encoder, create_local_decoder, create_local_encoder
|
|
10
11
|
from roborock.roborock_message import RoborockMessage
|
|
@@ -42,11 +43,13 @@ class LocalChannel(Channel):
|
|
|
42
43
|
self._host = host
|
|
43
44
|
self._transport: asyncio.Transport | None = None
|
|
44
45
|
self._protocol: _LocalProtocol | None = None
|
|
45
|
-
self._subscribers:
|
|
46
|
+
self._subscribers: CallbackList[RoborockMessage] = CallbackList(_LOGGER)
|
|
46
47
|
self._is_connected = False
|
|
47
48
|
|
|
48
49
|
self._decoder: Decoder = create_local_decoder(local_key)
|
|
49
50
|
self._encoder: Encoder = create_local_encoder(local_key)
|
|
51
|
+
# Callback to decode messages and dispatch to subscribers
|
|
52
|
+
self._data_received: Callable[[bytes], None] = decoder_callback(self._decoder, self._subscribers, _LOGGER)
|
|
50
53
|
|
|
51
54
|
@property
|
|
52
55
|
def is_connected(self) -> bool:
|
|
@@ -76,19 +79,6 @@ class LocalChannel(Channel):
|
|
|
76
79
|
self._transport = None
|
|
77
80
|
self._is_connected = False
|
|
78
81
|
|
|
79
|
-
def _data_received(self, data: bytes) -> None:
|
|
80
|
-
"""Handle incoming data from the transport."""
|
|
81
|
-
if not (messages := self._decoder(data)):
|
|
82
|
-
_LOGGER.warning("Failed to decode local message: %s", data)
|
|
83
|
-
return
|
|
84
|
-
for message in messages:
|
|
85
|
-
_LOGGER.debug("Received message: %s", message)
|
|
86
|
-
for callback in self._subscribers:
|
|
87
|
-
try:
|
|
88
|
-
callback(message)
|
|
89
|
-
except Exception as e:
|
|
90
|
-
_LOGGER.exception("Uncaught error in message handler callback: %s", e)
|
|
91
|
-
|
|
92
82
|
def _connection_lost(self, exc: Exception | None) -> None:
|
|
93
83
|
"""Handle connection loss."""
|
|
94
84
|
_LOGGER.warning("Connection lost to %s", self._host, exc_info=exc)
|
|
@@ -97,12 +87,7 @@ class LocalChannel(Channel):
|
|
|
97
87
|
|
|
98
88
|
async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
|
|
99
89
|
"""Subscribe to all messages from the device."""
|
|
100
|
-
self._subscribers.
|
|
101
|
-
|
|
102
|
-
def unsubscribe() -> None:
|
|
103
|
-
self._subscribers.remove(callback)
|
|
104
|
-
|
|
105
|
-
return unsubscribe
|
|
90
|
+
return self._subscribers.add_callback(callback)
|
|
106
91
|
|
|
107
92
|
async def publish(self, message: RoborockMessage) -> None:
|
|
108
93
|
"""Send a command message.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
|
|
6
|
+
from roborock.callbacks import decoder_callback
|
|
6
7
|
from roborock.containers import HomeDataDevice, RRiot, UserData
|
|
7
8
|
from roborock.exceptions import RoborockException
|
|
8
9
|
from roborock.mqtt.session import MqttParams, MqttSession, MqttSessionException
|
|
@@ -56,19 +57,8 @@ class MqttChannel(Channel):
|
|
|
56
57
|
|
|
57
58
|
Returns a callable that can be used to unsubscribe from the topic.
|
|
58
59
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if not (messages := self._decoder(payload)):
|
|
62
|
-
_LOGGER.warning("Failed to decode MQTT message: %s", payload)
|
|
63
|
-
return
|
|
64
|
-
for message in messages:
|
|
65
|
-
_LOGGER.debug("Received message: %s", message)
|
|
66
|
-
try:
|
|
67
|
-
callback(message)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
_LOGGER.exception("Uncaught error in message handler callback: %s", e)
|
|
70
|
-
|
|
71
|
-
return await self._mqtt_session.subscribe(self._subscribe_topic, message_handler)
|
|
60
|
+
dispatch = decoder_callback(self._decoder, callback, _LOGGER)
|
|
61
|
+
return await self._mqtt_session.subscribe(self._subscribe_topic, dispatch)
|
|
72
62
|
|
|
73
63
|
async def publish(self, message: RoborockMessage) -> None:
|
|
74
64
|
"""Publish a command message.
|
|
@@ -27,4 +27,6 @@ class B01PropsApi(Trait):
|
|
|
27
27
|
|
|
28
28
|
async def query_values(self, props: list[RoborockB01Props]) -> None:
|
|
29
29
|
"""Query the device for the values of the given Dyad protocols."""
|
|
30
|
-
await send_decoded_command(
|
|
30
|
+
await send_decoded_command(
|
|
31
|
+
self._channel, dps=10000, command=RoborockB01Methods.GET_PROP, params={"property": props}
|
|
32
|
+
)
|
|
@@ -17,6 +17,8 @@ from contextlib import asynccontextmanager
|
|
|
17
17
|
import aiomqtt
|
|
18
18
|
from aiomqtt import MqttError, TLSParameters
|
|
19
19
|
|
|
20
|
+
from roborock.callbacks import CallbackMap
|
|
21
|
+
|
|
20
22
|
from .session import MqttParams, MqttSession, MqttSessionException
|
|
21
23
|
|
|
22
24
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -53,7 +55,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
53
55
|
self._backoff = MIN_BACKOFF_INTERVAL
|
|
54
56
|
self._client: aiomqtt.Client | None = None
|
|
55
57
|
self._client_lock = asyncio.Lock()
|
|
56
|
-
self._listeners:
|
|
58
|
+
self._listeners: CallbackMap[str, bytes] = CallbackMap(_LOGGER)
|
|
57
59
|
|
|
58
60
|
@property
|
|
59
61
|
def connected(self) -> bool:
|
|
@@ -164,7 +166,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
164
166
|
# Re-establish any existing subscriptions
|
|
165
167
|
async with self._client_lock:
|
|
166
168
|
self._client = client
|
|
167
|
-
for topic in self._listeners:
|
|
169
|
+
for topic in self._listeners.keys():
|
|
168
170
|
_LOGGER.debug("Re-establishing subscription to topic %s", topic)
|
|
169
171
|
# TODO: If this fails it will break the whole connection. Make
|
|
170
172
|
# this retry again in the background with backoff.
|
|
@@ -179,13 +181,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
179
181
|
_LOGGER.debug("Processing MQTT messages")
|
|
180
182
|
async for message in client.messages:
|
|
181
183
|
_LOGGER.debug("Received message: %s", message)
|
|
182
|
-
|
|
183
|
-
try:
|
|
184
|
-
listener(message.payload)
|
|
185
|
-
except asyncio.CancelledError:
|
|
186
|
-
raise
|
|
187
|
-
except Exception as e:
|
|
188
|
-
_LOGGER.exception("Uncaught exception in subscriber callback: %s", e)
|
|
184
|
+
self._listeners(message.topic.value, message.payload)
|
|
189
185
|
|
|
190
186
|
async def subscribe(self, topic: str, callback: Callable[[bytes], None]) -> Callable[[], None]:
|
|
191
187
|
"""Subscribe to messages on the specified topic and invoke the callback for new messages.
|
|
@@ -196,9 +192,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
196
192
|
The returned callable unsubscribes from the topic when called.
|
|
197
193
|
"""
|
|
198
194
|
_LOGGER.debug("Subscribing to topic %s", topic)
|
|
199
|
-
|
|
200
|
-
self._listeners[topic] = []
|
|
201
|
-
self._listeners[topic].append(callback)
|
|
195
|
+
unsub = self._listeners.add_callback(topic, callback)
|
|
202
196
|
|
|
203
197
|
async with self._client_lock:
|
|
204
198
|
if self._client:
|
|
@@ -210,7 +204,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
210
204
|
else:
|
|
211
205
|
_LOGGER.debug("Client not connected, will establish subscription later")
|
|
212
206
|
|
|
213
|
-
return
|
|
207
|
+
return unsub
|
|
214
208
|
|
|
215
209
|
async def publish(self, topic: str, message: bytes) -> None:
|
|
216
210
|
"""Publish a message on the topic."""
|
|
@@ -13,6 +13,7 @@ from roborock.roborock_message import (
|
|
|
13
13
|
RoborockMessage,
|
|
14
14
|
RoborockMessageProtocol,
|
|
15
15
|
)
|
|
16
|
+
from roborock.util import get_next_int
|
|
16
17
|
|
|
17
18
|
_LOGGER = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -23,7 +24,15 @@ ParamsType = list | dict | int | None
|
|
|
23
24
|
|
|
24
25
|
def encode_mqtt_payload(dps: int, command: CommandType, params: ParamsType) -> RoborockMessage:
|
|
25
26
|
"""Encode payload for B01 commands over MQTT."""
|
|
26
|
-
dps_data = {
|
|
27
|
+
dps_data = {
|
|
28
|
+
"dps": {
|
|
29
|
+
dps: {
|
|
30
|
+
"method": str(command),
|
|
31
|
+
"msgId": str(get_next_int(100000000000, 999999999999)),
|
|
32
|
+
"params": params or [],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
payload = pad(json.dumps(dps_data).encode("utf-8"), AES.block_size)
|
|
28
37
|
return RoborockMessage(
|
|
29
38
|
protocol=RoborockMessageProtocol.RPC_REQUEST,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.37.0 → python_roborock-2.39.0}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|