python-roborock 1.0.0__tar.gz → 2.1.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.
Files changed (29) hide show
  1. {python_roborock-1.0.0 → python_roborock-2.1.0}/PKG-INFO +1 -1
  2. {python_roborock-1.0.0 → python_roborock-2.1.0}/pyproject.toml +1 -1
  3. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/api.py +0 -9
  4. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/code_mappings.py +172 -0
  5. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/const.py +1 -0
  6. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/containers.py +12 -0
  7. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/protocol.py +2 -3
  8. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/roborock_message.py +42 -0
  9. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/roborock_typing.py +29 -0
  10. python_roborock-2.1.0/roborock/version_a01_apis/roborock_client_a01.py +142 -0
  11. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +21 -8
  12. python_roborock-1.0.0/roborock/version_a01_apis/roborock_client_a01.py +0 -100
  13. {python_roborock-1.0.0 → python_roborock-2.1.0}/LICENSE +0 -0
  14. {python_roborock-1.0.0 → python_roborock-2.1.0}/README.md +0 -0
  15. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/__init__.py +0 -0
  16. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/cli.py +0 -0
  17. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/cloud_api.py +0 -0
  18. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/command_cache.py +0 -0
  19. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/exceptions.py +0 -0
  20. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/local_api.py +0 -0
  21. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/py.typed +0 -0
  22. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/roborock_future.py +0 -0
  23. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/util.py +0 -0
  24. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/version_1_apis/__init__.py +0 -0
  25. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  26. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  27. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  28. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/version_a01_apis/__init__.py +0 -0
  29. {python_roborock-1.0.0 → python_roborock-2.1.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 1.0.0
3
+ Version: 2.1.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Home-page: https://github.com/humbertogontijo/python-roborock
6
6
  License: GPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "1.0.0"
3
+ version = "2.1.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
6
6
  license = "GPL-3.0-only"
@@ -12,9 +12,6 @@ from typing import Any
12
12
 
13
13
  from .containers import (
14
14
  DeviceData,
15
- ModelStatus,
16
- S7MaxVStatus,
17
- Status,
18
15
  )
19
16
  from .exceptions import (
20
17
  RoborockTimeout,
@@ -48,16 +45,10 @@ class RoborockClient:
48
45
  self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
49
46
  self.is_available: bool = True
50
47
  self.queue_timeout = queue_timeout
51
- self._status_type: type[Status] = ModelStatus.get(self.device_info.model, S7MaxVStatus)
52
48
 
53
49
  def __del__(self) -> None:
54
50
  self.release()
55
51
 
56
- @property
57
- def status_type(self) -> type[Status]:
58
- """Gets the status type for this device"""
59
- return self._status_type
60
-
61
52
  def release(self):
62
53
  self.sync_disconnect()
63
54
 
@@ -65,10 +65,12 @@ class RoborockStateCode(RoborockEnum):
65
65
  segment_cleaning = 18
66
66
  emptying_the_bin = 22 # on s7+
67
67
  washing_the_mop = 23 # on a46
68
+ washing_the_mop_2 = 25
68
69
  going_to_wash_the_mop = 26 # on a46
69
70
  in_call = 28
70
71
  mapping = 29
71
72
  egg_attack = 30
73
+ patrol = 32
72
74
  charging_complete = 100
73
75
  device_offline = 101
74
76
  locked = 103
@@ -126,6 +128,7 @@ class RoborockErrorCode(RoborockEnum):
126
128
  side_brush_error = 17
127
129
  fan_error = 18
128
130
  dock = 19 # Dock not connected to power
131
+ optical_flow_sensor_dirt = 20
129
132
  vertical_bumper_pressed = 21
130
133
  dock_locator_error = 22
131
134
  return_to_dock_fail = 23
@@ -150,7 +153,14 @@ class RoborockErrorCode(RoborockEnum):
150
153
  clear_brush_exception_2 = 43 # Positioning button error
151
154
  filter_screen_exception = 44 # Clean the dock water filter
152
155
  mopping_roller_2 = 45 # Wash roller may be jammed
156
+ up_water_exception = 48
157
+ drain_water_exception = 49
153
158
  temperature_protection = 51 # Unit temperature protection
159
+ clean_carousel_exception = 52
160
+ clean_carousel_water_full = 53
161
+ water_carriage_drop = 54
162
+ check_clean_carouse = 55
163
+ audio_error = 56
154
164
 
155
165
 
156
166
  class RoborockFanPowerCode(RoborockEnum):
@@ -236,6 +246,14 @@ class RoborockFanSpeedP10(RoborockFanPowerCode):
236
246
  max_plus = 108
237
247
 
238
248
 
249
+ class RoborockFanSpeedS8MaxVUltra(RoborockFanPowerCode):
250
+ off = 105
251
+ balanced = 102
252
+ custom = 106
253
+ max_plus = 108
254
+ smart_mode = 110
255
+
256
+
239
257
  class RoborockMopModeCode(RoborockEnum):
240
258
  """Describes the mop mode of the vacuum cleaner."""
241
259
 
@@ -257,6 +275,15 @@ class RoborockMopModeS8ProUltra(RoborockMopModeCode):
257
275
  custom = 302
258
276
 
259
277
 
278
+ class RoborockMopModeS8MaxVUltra(RoborockMopModeCode):
279
+ standard = 300
280
+ deep = 301
281
+ deep_plus = 303
282
+ fast = 304
283
+ deep_plus_pearl = 305
284
+ smart_mode = 306
285
+
286
+
260
287
  class RoborockMopIntensityCode(RoborockEnum):
261
288
  """Describes the mop intensity of the vacuum cleaner."""
262
289
 
@@ -292,6 +319,14 @@ class RoborockMopIntensityP10(RoborockMopIntensityCode):
292
319
  custom_water_flow = 207
293
320
 
294
321
 
322
+ class RoborockMopIntensityS8MaxVUltra(RoborockMopIntensityCode):
323
+ off = 200
324
+ low = 201
325
+ medium = 202
326
+ smart_mode = 209
327
+ custom_water_flow = 207
328
+
329
+
295
330
  class RoborockMopIntensityS5Max(RoborockMopIntensityCode):
296
331
  """Describes the mop intensity of the vacuum cleaner."""
297
332
 
@@ -321,6 +356,7 @@ class RoborockDockErrorCode(RoborockEnum):
321
356
  duct_blockage = 34
322
357
  water_empty = 38
323
358
  waste_water_tank_full = 39
359
+ maintenance_brush_jammed = 42
324
360
  dirty_tank_latch_open = 44
325
361
  no_dustbin = 46
326
362
  cleaning_tank_full_or_blocked = 53
@@ -335,6 +371,7 @@ class RoborockDockTypeCode(RoborockEnum):
335
371
  s7_max_ultra_dock = 6
336
372
  s8_dock = 7
337
373
  p10_dock = 8
374
+ s8_maxv_ultra_dock = 10
338
375
 
339
376
 
340
377
  class RoborockDockDustCollectionModeCode(RoborockEnum):
@@ -450,3 +487,138 @@ class DyadError(RoborockEnum):
450
487
  dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts.
451
488
  low_battery = 20017 # Low battery level. Charge before starting self-cleaning.
452
489
  battery_under_10 = 20018 # Charge until the battery level exceeds 10% before manually starting self-cleaning.
490
+
491
+
492
+ class ZeoMode(RoborockEnum):
493
+ wash = 1
494
+ wash_and_dry = 2
495
+ dry = 3
496
+
497
+
498
+ class ZeoState(RoborockEnum):
499
+ standby = 1
500
+ weighing = 2
501
+ soaking = 3
502
+ washing = 4
503
+ rinsing = 5
504
+ spinning = 6
505
+ drying = 7
506
+ cooling = 8
507
+ under_delay_start = 9
508
+ done = 10
509
+
510
+
511
+ class ZeoProgram(RoborockEnum):
512
+ standard = 1
513
+ quick = 2
514
+ sanitize = 3
515
+ wool = 4
516
+ air_refresh = 5
517
+ custom = 6
518
+ bedding = 7
519
+ down = 8
520
+ silk = 9
521
+ rinse_and_spin = 10
522
+ spin = 11
523
+ down_clean = 12
524
+ baby_care = 13
525
+ anti_allergen = 14
526
+ sportswear = 15
527
+ night = 16
528
+ new_clothes = 17
529
+ shirts = 18
530
+ synthetics = 19
531
+ underwear = 20
532
+ gentle = 21
533
+ intensive = 22
534
+ cotton_linen = 23
535
+ season = 24
536
+ warming = 25
537
+ bra = 26
538
+ panties = 27
539
+ boiling_wash = 28
540
+ socks = 30
541
+ towels = 31
542
+ anti_mite = 32
543
+ exo_40_60 = 33
544
+ twenty_c = 34
545
+ t_shirts = 35
546
+ stain_removal = 36
547
+
548
+
549
+ class ZeoSoak(RoborockEnum):
550
+ normal = 0
551
+ low = 1
552
+ medium = 2
553
+ high = 3
554
+ max = 4
555
+
556
+
557
+ class ZeoTemperature(RoborockEnum):
558
+ normal = 1
559
+ low = 2
560
+ medium = 3
561
+ high = 4
562
+ max = 5
563
+ twenty_c = 6
564
+
565
+
566
+ class ZeoRinse(RoborockEnum):
567
+ none = 0
568
+ min = 1
569
+ low = 2
570
+ mid = 3
571
+ high = 4
572
+ max = 5
573
+
574
+
575
+ class ZeoSpin(RoborockEnum):
576
+ none = 1
577
+ very_low = 2
578
+ low = 3
579
+ mid = 4
580
+ high = 5
581
+ very_high = 6
582
+ max = 7
583
+
584
+
585
+ class ZeoDryingMode(RoborockEnum):
586
+ none = 0
587
+ quick = 1
588
+ iron = 2
589
+ store = 3
590
+
591
+
592
+ class ZeoDetergentType(RoborockEnum):
593
+ empty = 0
594
+ low = 1
595
+ medium = 2
596
+ high = 3
597
+
598
+
599
+ class ZeoSoftenerType(RoborockEnum):
600
+ empty = 0
601
+ low = 1
602
+ medium = 2
603
+ high = 3
604
+
605
+
606
+ class ZeoError(RoborockEnum):
607
+ none = 0
608
+ refill_error = 1
609
+ drain_error = 2
610
+ door_lock_error = 3
611
+ water_level_error = 4
612
+ inverter_error = 5
613
+ heating_error = 6
614
+ temperature_error = 7
615
+ communication_error = 10
616
+ drying_error = 11
617
+ drying_error_e_12 = 12
618
+ drying_error_e_13 = 13
619
+ drying_error_e_14 = 14
620
+ drying_error_e_15 = 15
621
+ drying_error_e_16 = 16
622
+ drying_error_water_flow = 17 # Check for normal water flow
623
+ drying_error_restart = 18 # Restart the washer and try again
624
+ spin_error = 19 # re-arrange clothes
@@ -43,6 +43,7 @@ ROBOROCK_C1 = "roborock.vacuum.c1"
43
43
  ROBOROCK_S8_PRO_ULTRA = "roborock.vacuum.a70"
44
44
  ROBOROCK_S8 = "roborock.vacuum.a51"
45
45
  ROBOROCK_P10 = "roborock.vacuum.a75"
46
+ ROBOROCK_S8_MAXV_ULTRA = "roborock.vacuum.a97"
46
47
 
47
48
  ROBOROCK_DYAD_AIR = "roborock.wetdryvac.a107"
48
49
  ROBOROCK_DYAD_PRO_COMBO = "roborock.wetdryvac.a83"
@@ -24,14 +24,17 @@ from .code_mappings import (
24
24
  RoborockFanSpeedS6Pure,
25
25
  RoborockFanSpeedS7,
26
26
  RoborockFanSpeedS7MaxV,
27
+ RoborockFanSpeedS8MaxVUltra,
27
28
  RoborockMopIntensityCode,
28
29
  RoborockMopIntensityP10,
29
30
  RoborockMopIntensityS5Max,
30
31
  RoborockMopIntensityS6MaxV,
31
32
  RoborockMopIntensityS7,
33
+ RoborockMopIntensityS8MaxVUltra,
32
34
  RoborockMopIntensityV2,
33
35
  RoborockMopModeCode,
34
36
  RoborockMopModeS7,
37
+ RoborockMopModeS8MaxVUltra,
35
38
  RoborockMopModeS8ProUltra,
36
39
  RoborockStateCode,
37
40
  )
@@ -52,6 +55,7 @@ from .const import (
52
55
  ROBOROCK_S7,
53
56
  ROBOROCK_S7_MAXV,
54
57
  ROBOROCK_S8,
58
+ ROBOROCK_S8_MAXV_ULTRA,
55
59
  ROBOROCK_S8_PRO_ULTRA,
56
60
  SENSOR_DIRTY_REPLACE_TIME,
57
61
  SIDE_BRUSH_REPLACE_TIME,
@@ -550,6 +554,13 @@ class P10Status(Status):
550
554
  mop_mode: RoborockMopModeS8ProUltra | None = None
551
555
 
552
556
 
557
+ @dataclass
558
+ class S8MaxvUltraStatus(Status):
559
+ fan_power: RoborockFanSpeedS8MaxVUltra | None = None
560
+ water_box_mode: RoborockMopIntensityS8MaxVUltra | None = None
561
+ mop_mode: RoborockMopModeS8MaxVUltra | None = None
562
+
563
+
553
564
  ModelStatus: dict[str, type[Status]] = {
554
565
  ROBOROCK_S4_MAX: S4MaxStatus,
555
566
  ROBOROCK_S5_MAX: S5MaxStatus,
@@ -563,6 +574,7 @@ ModelStatus: dict[str, type[Status]] = {
563
574
  ROBOROCK_S8_PRO_ULTRA: S8ProUltraStatus,
564
575
  ROBOROCK_G10S_PRO: S7MaxVStatus,
565
576
  ROBOROCK_P10: P10Status,
577
+ ROBOROCK_S8_MAXV_ULTRA: S8MaxvUltraStatus,
566
578
  }
567
579
 
568
580
 
@@ -36,7 +36,6 @@ from roborock.roborock_message import RoborockMessage
36
36
  _LOGGER = logging.getLogger(__name__)
37
37
  SALT = b"TXdfu$jyZ#TZHsg4"
38
38
  A01_HASH = "726f626f726f636b2d67a6d6da"
39
- A01_AES_DECIPHER = "ELSYN0wTI4AUm7C4"
40
39
  BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe"
41
40
  AP_CONFIG = 1
42
41
  SOCK_DISCOVERY = 2
@@ -208,7 +207,7 @@ class EncryptionAdapter(Construct):
208
207
  """
209
208
  if context.version == b"A01":
210
209
  iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
211
- decipher = AES.new(bytes(A01_AES_DECIPHER, "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
210
+ decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
212
211
  f = decipher.encrypt(obj)
213
212
  return f
214
213
  token = self.token_func(context)
@@ -219,7 +218,7 @@ class EncryptionAdapter(Construct):
219
218
  """Decrypts the given payload with the token stored in the context."""
220
219
  if context.version == b"A01":
221
220
  iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
222
- decipher = AES.new(bytes(A01_AES_DECIPHER, "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
221
+ decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
223
222
  f = decipher.decrypt(obj)
224
223
  return f
225
224
  token = self.token_func(context)
@@ -87,6 +87,48 @@ class RoborockDyadDataProtocol(RoborockEnum):
87
87
  RPC_RESPONSE = 10102
88
88
 
89
89
 
90
+ class RoborockZeoProtocol(RoborockEnum):
91
+ START = 200 # rw
92
+ PAUSE = 201 # rw
93
+ SHUTDOWN = 202 # rw
94
+ STATE = 203 # ro
95
+ MODE = 204 # rw
96
+ PROGRAM = 205 # rw
97
+ CHILD_LOCK = 206 # rw
98
+ TEMP = 207 # rw
99
+ RINSE_TIMES = 208 # rw
100
+ SPIN_LEVEL = 209 # rw
101
+ DRYING_MODE = 210 # rw
102
+ DETERGENT_SET = 211 # rw
103
+ SOFTENER_SET = 212 # rw
104
+ DETERGENT_TYPE = 213 # rw
105
+ SOFTENER_TYPE = 214 # rw
106
+ COUNTDOWN = 217 # rw
107
+ WASHING_LEFT = 218 # ro
108
+ DOORLOCK_STATE = 219 # ro
109
+ ERROR = 220 # ro
110
+ CUSTOM_PARAM_SAVE = 221 # rw
111
+ CUSTOM_PARAM_GET = 222 # ro
112
+ SOUND_SET = 223 # rw
113
+ TIMES_AFTER_CLEAN = 224 # ro
114
+ DEFAULT_SETTING = 225 # rw
115
+ DETERGENT_EMPTY = 226 # ro
116
+ SOFTENER_EMPTY = 227 # ro
117
+ LIGHT_SETTING = 229 # rw
118
+ DETERGENT_VOLUME = 230 # rw
119
+ SOFTENER_VOLUME = 231 # rw
120
+ APP_AUTHORIZATION = 232 # rw
121
+ ID_QUERY = 10000
122
+ F_C = 10001
123
+ SND_STATE = 10004
124
+ PRODUCT_INFO = 10005
125
+ PRIVACY_INFO = 10006
126
+ OTA_NFO = 10007
127
+ WASHING_LOG = 10008
128
+ RPC_REQ = 10101
129
+ RPC_RESp = 10102
130
+
131
+
90
132
  ROBOROCK_DATA_STATUS_PROTOCOL = [
91
133
  RoborockDataProtocol.ERROR_CODE,
92
134
  RoborockDataProtocol.STATE,
@@ -35,10 +35,13 @@ class RoborockCommand(str, Enum):
35
35
  APP_RC_START = "app_rc_start"
36
36
  APP_RC_STOP = "app_rc_stop"
37
37
  APP_RESUME_BUILD_MAP = "app_resume_build_map"
38
+ APP_RESUME_PATROL = "app_resume_patrol"
38
39
  APP_SEGMENT_CLEAN = "app_segment_clean"
39
40
  APP_SET_AMETHYST_STATUS = "app_set_amethyst_status"
40
41
  APP_SET_CARPET_DEEP_CLEAN_STATUS = "app_set_carpet_deep_clean_status"
42
+ APP_SET_CROSS_CARPET_CLEANING_STATUS = "app_set_cross_carpet_cleaning_status"
41
43
  APP_SET_DOOR_SILL_BLOCKS = "app_set_door_sill_blocks"
44
+ APP_SET_DIRTY_REPLENISH_CLEAN_STATUS = "app_set_dirty_replenish_clean_status"
42
45
  APP_SET_DRYER_SETTING = "app_set_dryer_setting"
43
46
  APP_SET_DRYER_STATUS = "app_set_dryer_status"
44
47
  APP_SET_DYNAMIC_CONFIG = "app_set_dynamic_config"
@@ -50,6 +53,8 @@ class RoborockCommand(str, Enum):
50
53
  APP_START_BUILD_MAP = "app_start_build_map"
51
54
  APP_START_COLLECT_DUST = "app_start_collect_dust"
52
55
  APP_START_EASTER_EGG = "app_start_easter_egg"
56
+ APP_START_PATROL = "app_start_patrol"
57
+ APP_START_PET_PATROL = "app_start_pet_patrol"
53
58
  APP_START_WASH = "app_start_wash"
54
59
  APP_STAT = "app_stat"
55
60
  APP_STOP = "app_stop"
@@ -177,6 +182,7 @@ class RoborockCommand(str, Enum):
177
182
  SET_CLEAN_FOLLOW_GROUND_MATERIAL_STATUS = "set_clean_follow_ground_material_status"
178
183
  SET_CLEAN_MOTOR_MODE = "set_clean_motor_mode"
179
184
  SET_CLEAN_SEQUENCE = "set_clean_sequence"
185
+ SET_CLEAN_REPEAT_TIMES = "set_clean_repeat_times"
180
186
  SET_COLLISION_AVOID_STATUS = "set_collision_avoid_status"
181
187
  SET_CUSTOM_MODE = "set_custom_mode"
182
188
  SET_CUSTOMIZE_CLEAN_MODE = "set_customize_clean_mode"
@@ -239,6 +245,29 @@ class RoborockCommand(str, Enum):
239
245
  USE_NEW_MAP = "use_new_map"
240
246
  USE_OLD_MAP = "use_old_map"
241
247
  USER_UPLOAD_LOG = "user_upload_log"
248
+ SET_STRETCH_TAG_STATUS = "set_stretch_tag_status"
249
+ GET_STRETCH_TAG_STATUS = "get_stretch_tag_status"
250
+ SET_RIGHT_BRUSH_STRETCH_STATUS = "set_right_brush_stretch_status"
251
+ GET_RIGHT_BRUSH_STRETCH_STATUS = "get_right_brush_stretch_status"
252
+ SET_DIRTY_OBJECT_DETECT_STATUS = "set_dirty_object_detect_status"
253
+ GET_DIRTY_OBJECT_DETECT_STATUS = "get_dirty_object_detect_status"
254
+ SET_WASH_WATER_TEMPERATURE = "set_wash_water_temperature"
255
+ GET_WASH_WATER_TEMPERATURE = "get_wash_water_temperature"
256
+ APP_EMPTY_RINSE_TANK_WATER = "app_empty_rinse_tank_water"
257
+ SET_PET_SUPPLIES_DEEP_CLEAN_STATUS = "set_pet_supplies_deep_clean_status"
258
+ GET_PET_SUPPLIES_DEEP_CLEAN_STATUS = "get_pet_supplies_deep_clean_status"
259
+ SET_AP_MIC_LED_STATUS = "set_ap_mic_led_status"
260
+ GET_AP_MIC_LED_STATUS = "get_ap_mic_led_status"
261
+ SET_HANDLE_LEAK_WATER_STATUS = "set_handle_leak_water_status"
262
+ GET_HANDLE_LEAK_WATER_STATUS = "get_handle_leak_water_status"
263
+ APP_IGNORE_DIRTY_OBJECTS = "app_ignore_dirty_objects"
264
+ MATTER_GET_STATUS = "matter.get_status"
265
+ MATTER_DNLD_KEY = "matter.dnld_key"
266
+ MATTER_RESET = "matter.reset"
267
+ SET_GAP_DEEP_CLEAN_STATUS = "set_gap_deep_clean_status"
268
+ GET_GAP_DEEP_CLEAN_STATUS = "get_gap_deep_clean_status"
269
+ APP_SET_ROBOT_SETTING = "app_set_robot_setting"
270
+ APP_GET_ROBOT_SETTING = "app_get_robot_setting"
242
271
 
243
272
 
244
273
  @dataclass
@@ -0,0 +1,142 @@
1
+ import dataclasses
2
+ import json
3
+ import typing
4
+ from collections.abc import Callable
5
+ from datetime import time
6
+
7
+ from Crypto.Cipher import AES
8
+ from Crypto.Util.Padding import unpad
9
+
10
+ from roborock import DeviceData
11
+ from roborock.api import RoborockClient
12
+ from roborock.code_mappings import (
13
+ DyadBrushSpeed,
14
+ DyadCleanMode,
15
+ DyadError,
16
+ DyadSelfCleanLevel,
17
+ DyadSelfCleanMode,
18
+ DyadSuction,
19
+ DyadWarmLevel,
20
+ DyadWaterLevel,
21
+ RoborockDyadStateCode,
22
+ ZeoDetergentType,
23
+ ZeoDryingMode,
24
+ ZeoError,
25
+ ZeoMode,
26
+ ZeoProgram,
27
+ ZeoRinse,
28
+ ZeoSoftenerType,
29
+ ZeoSpin,
30
+ ZeoState,
31
+ ZeoTemperature,
32
+ )
33
+ from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory
34
+ from roborock.roborock_message import (
35
+ RoborockDyadDataProtocol,
36
+ RoborockMessage,
37
+ RoborockMessageProtocol,
38
+ RoborockZeoProtocol,
39
+ )
40
+
41
+
42
+ @dataclasses.dataclass
43
+ class A01ProtocolCacheEntry:
44
+ post_process_fn: Callable
45
+ value: typing.Any | None = None
46
+
47
+
48
+ # Right now this cache is not active, it was too much complexity for the initial addition of dyad.
49
+ protocol_entries = {
50
+ RoborockDyadDataProtocol.STATUS: A01ProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
51
+ RoborockDyadDataProtocol.SELF_CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
52
+ RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: A01ProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
53
+ RoborockDyadDataProtocol.WARM_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
54
+ RoborockDyadDataProtocol.CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
55
+ RoborockDyadDataProtocol.SUCTION: A01ProtocolCacheEntry(lambda val: DyadSuction(val).name),
56
+ RoborockDyadDataProtocol.WATER_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
57
+ RoborockDyadDataProtocol.BRUSH_SPEED: A01ProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
58
+ RoborockDyadDataProtocol.POWER: A01ProtocolCacheEntry(lambda val: int(val)),
59
+ RoborockDyadDataProtocol.AUTO_DRY: A01ProtocolCacheEntry(lambda val: bool(val)),
60
+ RoborockDyadDataProtocol.MESH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
61
+ RoborockDyadDataProtocol.BRUSH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
62
+ RoborockDyadDataProtocol.ERROR: A01ProtocolCacheEntry(lambda val: DyadError(val).name),
63
+ RoborockDyadDataProtocol.VOLUME_SET: A01ProtocolCacheEntry(lambda val: int(val)),
64
+ RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: A01ProtocolCacheEntry(lambda val: bool(val)),
65
+ RoborockDyadDataProtocol.AUTO_DRY_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
66
+ RoborockDyadDataProtocol.SILENT_DRY_DURATION: A01ProtocolCacheEntry(lambda val: int(val)), # in minutes
67
+ RoborockDyadDataProtocol.SILENT_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
68
+ RoborockDyadDataProtocol.SILENT_MODE_START_TIME: A01ProtocolCacheEntry(
69
+ lambda val: time(hour=int(val / 60), minute=val % 60)
70
+ ), # in minutes since 00:00
71
+ RoborockDyadDataProtocol.SILENT_MODE_END_TIME: A01ProtocolCacheEntry(
72
+ lambda val: time(hour=int(val / 60), minute=val % 60)
73
+ ), # in minutes since 00:00
74
+ RoborockDyadDataProtocol.RECENT_RUN_TIME: A01ProtocolCacheEntry(
75
+ lambda val: [int(v) for v in val.split(",")]
76
+ ), # minutes of cleaning in past few days.
77
+ RoborockDyadDataProtocol.TOTAL_RUN_TIME: A01ProtocolCacheEntry(lambda val: int(val)),
78
+ RoborockDyadDataProtocol.SND_STATE: A01ProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
79
+ RoborockDyadDataProtocol.PRODUCT_INFO: A01ProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
80
+ }
81
+
82
+ zeo_data_protocol_entries = {
83
+ # ro
84
+ RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name),
85
+ RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)),
86
+ RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: int(val)),
87
+ RoborockZeoProtocol.ERROR: A01ProtocolCacheEntry(lambda val: ZeoError(val).name),
88
+ RoborockZeoProtocol.TIMES_AFTER_CLEAN: A01ProtocolCacheEntry(lambda val: int(val)),
89
+ RoborockZeoProtocol.DETERGENT_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
90
+ RoborockZeoProtocol.SOFTENER_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
91
+ # rw
92
+ RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name),
93
+ RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val).name),
94
+ RoborockZeoProtocol.TEMP: A01ProtocolCacheEntry(lambda val: ZeoTemperature(val).name),
95
+ RoborockZeoProtocol.RINSE_TIMES: A01ProtocolCacheEntry(lambda val: ZeoRinse(val).name),
96
+ RoborockZeoProtocol.SPIN_LEVEL: A01ProtocolCacheEntry(lambda val: ZeoSpin(val).name),
97
+ RoborockZeoProtocol.DRYING_MODE: A01ProtocolCacheEntry(lambda val: ZeoDryingMode(val).name),
98
+ RoborockZeoProtocol.DETERGENT_TYPE: A01ProtocolCacheEntry(lambda val: ZeoDetergentType(val).name),
99
+ RoborockZeoProtocol.SOFTENER_TYPE: A01ProtocolCacheEntry(lambda val: ZeoSoftenerType(val).name),
100
+ RoborockZeoProtocol.SOUND_SET: A01ProtocolCacheEntry(lambda val: bool(val)),
101
+ }
102
+
103
+
104
+ class RoborockClientA01(RoborockClient):
105
+ def __init__(self, endpoint: str, device_info: DeviceData, category: RoborockCategory, queue_timeout: int = 4):
106
+ super().__init__(endpoint, device_info, queue_timeout)
107
+ self.category = category
108
+
109
+ def on_message_received(self, messages: list[RoborockMessage]) -> None:
110
+ for message in messages:
111
+ protocol = message.protocol
112
+ if message.payload and protocol in [
113
+ RoborockMessageProtocol.RPC_RESPONSE,
114
+ RoborockMessageProtocol.GENERAL_REQUEST,
115
+ ]:
116
+ payload = message.payload
117
+ try:
118
+ payload = unpad(payload, AES.block_size)
119
+ except Exception:
120
+ continue
121
+ payload_json = json.loads(payload.decode())
122
+ for data_point_number, data_point in payload_json.get("dps").items():
123
+ data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol
124
+ entries: dict
125
+ if self.category == RoborockCategory.WET_DRY_VAC:
126
+ data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
127
+ entries = protocol_entries
128
+ elif self.category == RoborockCategory.WASHING_MACHINE:
129
+ data_point_protocol = RoborockZeoProtocol(int(data_point_number))
130
+ entries = zeo_data_protocol_entries
131
+ else:
132
+ continue
133
+ if data_point_protocol in entries:
134
+ # Auto convert into data struct we want.
135
+ converted_response = entries[data_point_protocol].post_process_fn(data_point)
136
+ queue = self._waiting_queue.get(int(data_point_number))
137
+ if queue and queue.protocol == protocol:
138
+ queue.resolve((converted_response, None))
139
+
140
+ async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]):
141
+ """This should handle updating for each given protocol."""
142
+ raise NotImplementedError
@@ -1,28 +1,36 @@
1
1
  import asyncio
2
2
  import base64
3
3
  import json
4
+ import typing
4
5
 
5
6
  from Crypto.Cipher import AES
6
7
  from Crypto.Util.Padding import pad, unpad
7
8
 
8
9
  from roborock.cloud_api import RoborockMqttClient
9
- from roborock.containers import DeviceData, UserData
10
+ from roborock.containers import DeviceData, RoborockCategory, UserData
10
11
  from roborock.exceptions import RoborockException
11
12
  from roborock.protocol import MessageParser, Utils
12
- from roborock.roborock_message import RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol
13
+ from roborock.roborock_message import (
14
+ RoborockDyadDataProtocol,
15
+ RoborockMessage,
16
+ RoborockMessageProtocol,
17
+ RoborockZeoProtocol,
18
+ )
13
19
 
14
20
  from .roborock_client_a01 import RoborockClientA01
15
21
 
16
22
 
17
23
  class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
18
- def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None:
24
+ def __init__(
25
+ self, user_data: UserData, device_info: DeviceData, category: RoborockCategory, queue_timeout: int = 10
26
+ ) -> None:
19
27
  rriot = user_data.rriot
20
28
  if rriot is None:
21
29
  raise RoborockException("Got no rriot data from user_data")
22
30
  endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode()
23
31
 
24
32
  RoborockMqttClient.__init__(self, user_data, device_info, queue_timeout)
25
- RoborockClientA01.__init__(self, endpoint, device_info)
33
+ RoborockClientA01.__init__(self, endpoint, device_info, category, queue_timeout)
26
34
 
27
35
  async def send_message(self, roborock_message: RoborockMessage):
28
36
  await self.validate_connection()
@@ -37,14 +45,19 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
37
45
  for dps in json.loads(payload["dps"]["10000"]):
38
46
  futures.append(asyncio.ensure_future(self._async_response(dps, response_protocol)))
39
47
  self._send_msg_raw(m)
40
- responses = await asyncio.gather(*futures)
41
- dps_responses = {}
48
+ responses = await asyncio.gather(*futures, return_exceptions=True)
49
+ dps_responses: dict[int, typing.Any] = {}
42
50
  if "10000" in payload["dps"]:
43
51
  for i, dps in enumerate(json.loads(payload["dps"]["10000"])):
44
- dps_responses[dps] = responses[i][0]
52
+ response = responses[i]
53
+ if isinstance(response, BaseException):
54
+ self._logger.warning("Timed out get req for %s after %s s", dps, self.queue_timeout)
55
+ dps_responses[dps] = None
56
+ else:
57
+ dps_responses[dps] = response[0]
45
58
  return dps_responses
46
59
 
47
- async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]):
60
+ async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]):
48
61
  payload = {"dps": {RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}}
49
62
  return await self.send_message(
50
63
  RoborockMessage(
@@ -1,100 +0,0 @@
1
- import dataclasses
2
- import json
3
- import typing
4
- from collections.abc import Callable
5
- from datetime import time
6
-
7
- from Crypto.Cipher import AES
8
- from Crypto.Util.Padding import unpad
9
-
10
- from roborock import DeviceData
11
- from roborock.api import RoborockClient
12
- from roborock.code_mappings import (
13
- DyadBrushSpeed,
14
- DyadCleanMode,
15
- DyadError,
16
- DyadSelfCleanLevel,
17
- DyadSelfCleanMode,
18
- DyadSuction,
19
- DyadWarmLevel,
20
- DyadWaterLevel,
21
- RoborockDyadStateCode,
22
- )
23
- from roborock.containers import DyadProductInfo, DyadSndState
24
- from roborock.roborock_message import (
25
- RoborockDyadDataProtocol,
26
- RoborockMessage,
27
- RoborockMessageProtocol,
28
- )
29
-
30
-
31
- @dataclasses.dataclass
32
- class DyadProtocolCacheEntry:
33
- post_process_fn: Callable
34
- value: typing.Any | None = None
35
-
36
-
37
- # Right now this cache is not active, it was too much complexity for the initial addition of dyad.
38
- protocol_entries = {
39
- RoborockDyadDataProtocol.STATUS: DyadProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
40
- RoborockDyadDataProtocol.SELF_CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
41
- RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: DyadProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
42
- RoborockDyadDataProtocol.WARM_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
43
- RoborockDyadDataProtocol.CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
44
- RoborockDyadDataProtocol.SUCTION: DyadProtocolCacheEntry(lambda val: DyadSuction(val).name),
45
- RoborockDyadDataProtocol.WATER_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
46
- RoborockDyadDataProtocol.BRUSH_SPEED: DyadProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
47
- RoborockDyadDataProtocol.POWER: DyadProtocolCacheEntry(lambda val: int(val)),
48
- RoborockDyadDataProtocol.AUTO_DRY: DyadProtocolCacheEntry(lambda val: bool(val)),
49
- RoborockDyadDataProtocol.MESH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
50
- RoborockDyadDataProtocol.BRUSH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
51
- RoborockDyadDataProtocol.ERROR: DyadProtocolCacheEntry(lambda val: DyadError(val).name),
52
- RoborockDyadDataProtocol.VOLUME_SET: DyadProtocolCacheEntry(lambda val: int(val)),
53
- RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: DyadProtocolCacheEntry(lambda val: bool(val)),
54
- RoborockDyadDataProtocol.AUTO_DRY_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
55
- RoborockDyadDataProtocol.SILENT_DRY_DURATION: DyadProtocolCacheEntry(lambda val: int(val)), # in minutes
56
- RoborockDyadDataProtocol.SILENT_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
57
- RoborockDyadDataProtocol.SILENT_MODE_START_TIME: DyadProtocolCacheEntry(
58
- lambda val: time(hour=int(val / 60), minute=val % 60)
59
- ), # in minutes since 00:00
60
- RoborockDyadDataProtocol.SILENT_MODE_END_TIME: DyadProtocolCacheEntry(
61
- lambda val: time(hour=int(val / 60), minute=val % 60)
62
- ), # in minutes since 00:00
63
- RoborockDyadDataProtocol.RECENT_RUN_TIME: DyadProtocolCacheEntry(
64
- lambda val: [int(v) for v in val.split(",")]
65
- ), # minutes of cleaning in past few days.
66
- RoborockDyadDataProtocol.TOTAL_RUN_TIME: DyadProtocolCacheEntry(lambda val: int(val)),
67
- RoborockDyadDataProtocol.SND_STATE: DyadProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
68
- RoborockDyadDataProtocol.PRODUCT_INFO: DyadProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
69
- }
70
-
71
-
72
- class RoborockClientA01(RoborockClient):
73
- def __init__(self, endpoint: str, device_info: DeviceData):
74
- super().__init__(endpoint, device_info)
75
-
76
- def on_message_received(self, messages: list[RoborockMessage]) -> None:
77
- for message in messages:
78
- protocol = message.protocol
79
- if message.payload and protocol in [
80
- RoborockMessageProtocol.RPC_RESPONSE,
81
- RoborockMessageProtocol.GENERAL_REQUEST,
82
- ]:
83
- payload = message.payload
84
- try:
85
- payload = unpad(payload, AES.block_size)
86
- except Exception:
87
- continue
88
- payload_json = json.loads(payload.decode())
89
- for data_point_number, data_point in payload_json.get("dps").items():
90
- data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
91
- if data_point_protocol in protocol_entries:
92
- # Auto convert into data struct we want.
93
- converted_response = protocol_entries[data_point_protocol].post_process_fn(data_point)
94
- queue = self._waiting_queue.get(int(data_point_number))
95
- if queue and queue.protocol == protocol:
96
- queue.resolve((converted_response, None))
97
-
98
- async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]):
99
- """This should handle updating for each given protocol."""
100
- raise NotImplementedError
File without changes