python-roborock 2.25.0__tar.gz → 2.26.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 (40) hide show
  1. {python_roborock-2.25.0 → python_roborock-2.26.0}/PKG-INFO +1 -1
  2. {python_roborock-2.25.0 → python_roborock-2.26.0}/pyproject.toml +1 -1
  3. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/code_mappings.py +61 -0
  4. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/const.py +2 -0
  5. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/containers.py +24 -0
  6. python_roborock-2.26.0/roborock/protocols/a01_protocol.py +54 -0
  7. python_roborock-2.26.0/roborock/version_a01_apis/roborock_client_a01.py +159 -0
  8. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +7 -18
  9. python_roborock-2.25.0/roborock/version_a01_apis/roborock_client_a01.py +0 -153
  10. {python_roborock-2.25.0 → python_roborock-2.26.0}/LICENSE +0 -0
  11. {python_roborock-2.25.0 → python_roborock-2.26.0}/README.md +0 -0
  12. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/__init__.py +0 -0
  13. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/api.py +0 -0
  14. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/cli.py +0 -0
  15. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/cloud_api.py +0 -0
  16. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/command_cache.py +0 -0
  17. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/README.md +0 -0
  18. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/__init__.py +0 -0
  19. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/device.py +0 -0
  20. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/device_manager.py +0 -0
  21. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/local_channel.py +0 -0
  22. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/mqtt_channel.py +0 -0
  23. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/exceptions.py +0 -0
  24. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/local_api.py +0 -0
  25. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/mqtt/__init__.py +0 -0
  26. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/mqtt/roborock_session.py +0 -0
  27. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/mqtt/session.py +0 -0
  28. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/protocol.py +0 -0
  29. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/protocols/v1_protocol.py +0 -0
  30. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/py.typed +0 -0
  31. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/roborock_future.py +0 -0
  32. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/roborock_message.py +0 -0
  33. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/roborock_typing.py +0 -0
  34. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/util.py +0 -0
  35. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/__init__.py +0 -0
  36. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  37. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  38. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  39. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_a01_apis/__init__.py +0 -0
  40. {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.25.0
3
+ Version: 2.26.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 = "2.25.0"
3
+ version = "2.26.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"
@@ -300,6 +300,28 @@ class RoborockFanSpeedS8MaxVUltra(RoborockFanPowerCode):
300
300
  smart_mode = 110
301
301
 
302
302
 
303
+ class RoborockFanSpeedSaros10(RoborockFanPowerCode):
304
+ off = 105
305
+ quiet = 101
306
+ balanced = 102
307
+ turbo = 103
308
+ max = 104
309
+ custom = 106
310
+ max_plus = 108
311
+ smart_mode = 110
312
+
313
+
314
+ class RoborockFanSpeedSaros10R(RoborockFanPowerCode):
315
+ off = 105
316
+ quiet = 101
317
+ balanced = 102
318
+ turbo = 103
319
+ max = 104
320
+ custom = 106
321
+ max_plus = 108
322
+ smart_mode = 110
323
+
324
+
303
325
  class RoborockMopModeCode(RoborockEnum):
304
326
  """Describes the mop mode of the vacuum cleaner."""
305
327
 
@@ -341,6 +363,15 @@ class RoborockMopModeS8MaxVUltra(RoborockMopModeCode):
341
363
  smart_mode = 306
342
364
 
343
365
 
366
+ class RoborockMopModeSaros10R(RoborockMopModeCode):
367
+ standard = 300
368
+ deep = 301
369
+ custom = 302
370
+ deep_plus = 303
371
+ fast = 304
372
+ smart_mode = 306
373
+
374
+
344
375
  class RoborockMopModeQRevoMaster(RoborockMopModeCode):
345
376
  standard = 300
346
377
  deep = 301
@@ -359,6 +390,15 @@ class RoborockMopModeQRevoMaxV(RoborockMopModeCode):
359
390
  smart_mode = 306
360
391
 
361
392
 
393
+ class RoborockMopModeSaros10(RoborockMopModeCode):
394
+ standard = 300
395
+ deep = 301
396
+ custom = 302
397
+ deep_plus = 303
398
+ fast = 304
399
+ smart_mode = 306
400
+
401
+
362
402
  class RoborockMopIntensityCode(RoborockEnum):
363
403
  """Describes the mop intensity of the vacuum cleaner."""
364
404
 
@@ -438,6 +478,27 @@ class RoborockMopIntensityS8MaxVUltra(RoborockMopIntensityCode):
438
478
  custom_water_flow = 207
439
479
 
440
480
 
481
+ class RoborockMopIntensitySaros10(RoborockMopIntensityCode):
482
+ off = 200
483
+ mild = 201
484
+ standard = 202
485
+ intense = 203
486
+ extreme = 208
487
+ custom = 204
488
+ smart_mode = 209
489
+
490
+
491
+ class RoborockMopIntensitySaros10R(RoborockMopIntensityCode):
492
+ off = 200
493
+ low = 201
494
+ medium = 202
495
+ high = 203
496
+ custom = 204
497
+ extreme = 250
498
+ vac_followed_by_mop = 235
499
+ smart_mode = 209
500
+
501
+
441
502
  class RoborockMopIntensityS5Max(RoborockMopIntensityCode):
442
503
  """Describes the mop intensity of the vacuum cleaner."""
443
504
 
@@ -50,6 +50,8 @@ ROBOROCK_S8_MAXV_ULTRA = "roborock.vacuum.a97"
50
50
  ROBOROCK_QREVO_S = "roborock.vacuum.a104"
51
51
  ROBOROCK_QREVO_PRO = "roborock.vacuum.a101"
52
52
  ROBOROCK_QREVO_MAXV = "roborock.vacuum.a87"
53
+ ROBOROCK_SAROS_10R = "roborock.vacuum.a144"
54
+ ROBOROCK_SAROS_10 = "roborock.vacuum.a147"
53
55
 
54
56
  ROBOROCK_DYAD_AIR = "roborock.wetdryvac.a107"
55
57
  ROBOROCK_DYAD_PRO_COMBO = "roborock.wetdryvac.a83"
@@ -28,6 +28,8 @@ from .code_mappings import (
28
28
  RoborockFanSpeedS7,
29
29
  RoborockFanSpeedS7MaxV,
30
30
  RoborockFanSpeedS8MaxVUltra,
31
+ RoborockFanSpeedSaros10,
32
+ RoborockFanSpeedSaros10R,
31
33
  RoborockFinishReason,
32
34
  RoborockInCleaning,
33
35
  RoborockMopIntensityCode,
@@ -40,6 +42,8 @@ from .code_mappings import (
40
42
  RoborockMopIntensityS6MaxV,
41
43
  RoborockMopIntensityS7,
42
44
  RoborockMopIntensityS8MaxVUltra,
45
+ RoborockMopIntensitySaros10,
46
+ RoborockMopIntensitySaros10R,
43
47
  RoborockMopModeCode,
44
48
  RoborockMopModeQRevoCurv,
45
49
  RoborockMopModeQRevoMaster,
@@ -47,6 +51,8 @@ from .code_mappings import (
47
51
  RoborockMopModeS7,
48
52
  RoborockMopModeS8MaxVUltra,
49
53
  RoborockMopModeS8ProUltra,
54
+ RoborockMopModeSaros10,
55
+ RoborockMopModeSaros10R,
50
56
  RoborockStartType,
51
57
  RoborockStateCode,
52
58
  )
@@ -74,6 +80,8 @@ from .const import (
74
80
  ROBOROCK_S8,
75
81
  ROBOROCK_S8_MAXV_ULTRA,
76
82
  ROBOROCK_S8_PRO_ULTRA,
83
+ ROBOROCK_SAROS_10,
84
+ ROBOROCK_SAROS_10R,
77
85
  SENSOR_DIRTY_REPLACE_TIME,
78
86
  SIDE_BRUSH_REPLACE_TIME,
79
87
  STRAINER_REPLACE_TIME,
@@ -678,6 +686,20 @@ class S8MaxvUltraStatus(Status):
678
686
  mop_mode: RoborockMopModeS8MaxVUltra | None = None
679
687
 
680
688
 
689
+ @dataclass
690
+ class Saros10RStatus(Status):
691
+ fan_power: RoborockFanSpeedSaros10R | None = None
692
+ water_box_mode: RoborockMopIntensitySaros10R | None = None
693
+ mop_mode: RoborockMopModeSaros10R | None = None
694
+
695
+
696
+ @dataclass
697
+ class Saros10Status(Status):
698
+ fan_power: RoborockFanSpeedSaros10 | None = None
699
+ water_box_mode: RoborockMopIntensitySaros10 | None = None
700
+ mop_mode: RoborockMopModeSaros10 | None = None
701
+
702
+
681
703
  ModelStatus: dict[str, type[Status]] = {
682
704
  ROBOROCK_S4_MAX: S4MaxStatus,
683
705
  ROBOROCK_S5_MAX: S5MaxStatus,
@@ -701,6 +723,8 @@ ModelStatus: dict[str, type[Status]] = {
701
723
  ROBOROCK_QREVO_MAXV: QRevoMaxVStatus,
702
724
  ROBOROCK_QREVO_PRO: P10Status,
703
725
  ROBOROCK_S8_MAXV_ULTRA: S8MaxvUltraStatus,
726
+ ROBOROCK_SAROS_10R: Saros10RStatus,
727
+ ROBOROCK_SAROS_10: Saros10Status,
704
728
  }
705
729
 
706
730
 
@@ -0,0 +1,54 @@
1
+ """Roborock A01 Protocol encoding and decoding."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any
6
+
7
+ from Crypto.Cipher import AES
8
+ from Crypto.Util.Padding import pad, unpad
9
+
10
+ from roborock.exceptions import RoborockException
11
+ from roborock.roborock_message import (
12
+ RoborockDyadDataProtocol,
13
+ RoborockMessage,
14
+ RoborockMessageProtocol,
15
+ RoborockZeoProtocol,
16
+ )
17
+
18
+ _LOGGER = logging.getLogger(__name__)
19
+
20
+ A01_VERSION = b"A01"
21
+
22
+
23
+ def encode_mqtt_payload(data: dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any]) -> RoborockMessage:
24
+ """Encode payload for A01 commands over MQTT."""
25
+ dps_data = {"dps": data}
26
+ payload = pad(json.dumps(dps_data).encode("utf-8"), AES.block_size)
27
+ return RoborockMessage(
28
+ protocol=RoborockMessageProtocol.RPC_REQUEST,
29
+ version=A01_VERSION,
30
+ payload=payload,
31
+ )
32
+
33
+
34
+ def decode_rpc_response(message: RoborockMessage) -> dict[int, Any]:
35
+ """Decode a V1 RPC_RESPONSE message."""
36
+ if not message.payload:
37
+ raise RoborockException("Invalid A01 message format: missing payload")
38
+ try:
39
+ unpadded = unpad(message.payload, AES.block_size)
40
+ except ValueError as err:
41
+ raise RoborockException(f"Unable to unpad A01 payload: {err}")
42
+
43
+ try:
44
+ payload = json.loads(unpadded.decode())
45
+ except (json.JSONDecodeError, TypeError) as e:
46
+ raise RoborockException(f"Invalid A01 message payload: {e} for {message.payload!r}") from e
47
+
48
+ datapoints = payload.get("dps", {})
49
+ if not isinstance(datapoints, dict):
50
+ raise RoborockException(f"Invalid A01 message format: 'dps' should be a dictionary for {message.payload!r}")
51
+ try:
52
+ return {int(key): value for key, value in datapoints.items()}
53
+ except ValueError:
54
+ raise RoborockException(f"Invalid A01 message format: 'dps' key should be an integer for {message.payload!r}")
@@ -0,0 +1,159 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from collections.abc import Callable
4
+ from datetime import time
5
+ from typing import Any
6
+
7
+ from roborock import DeviceData
8
+ from roborock.api import RoborockClient
9
+ from roborock.code_mappings import (
10
+ DyadBrushSpeed,
11
+ DyadCleanMode,
12
+ DyadError,
13
+ DyadSelfCleanLevel,
14
+ DyadSelfCleanMode,
15
+ DyadSuction,
16
+ DyadWarmLevel,
17
+ DyadWaterLevel,
18
+ RoborockDyadStateCode,
19
+ ZeoDetergentType,
20
+ ZeoDryingMode,
21
+ ZeoError,
22
+ ZeoMode,
23
+ ZeoProgram,
24
+ ZeoRinse,
25
+ ZeoSoftenerType,
26
+ ZeoSpin,
27
+ ZeoState,
28
+ ZeoTemperature,
29
+ )
30
+ from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory
31
+ from roborock.exceptions import RoborockException
32
+ from roborock.protocols.a01_protocol import decode_rpc_response
33
+ from roborock.roborock_message import (
34
+ RoborockDyadDataProtocol,
35
+ RoborockMessage,
36
+ RoborockMessageProtocol,
37
+ RoborockZeoProtocol,
38
+ )
39
+
40
+ _LOGGER = logging.getLogger(__name__)
41
+
42
+
43
+ DYAD_PROTOCOL_ENTRIES: dict[RoborockDyadDataProtocol, Callable] = {
44
+ RoborockDyadDataProtocol.STATUS: lambda val: RoborockDyadStateCode(val).name,
45
+ RoborockDyadDataProtocol.SELF_CLEAN_MODE: lambda val: DyadSelfCleanMode(val).name,
46
+ RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: lambda val: DyadSelfCleanLevel(val).name,
47
+ RoborockDyadDataProtocol.WARM_LEVEL: lambda val: DyadWarmLevel(val).name,
48
+ RoborockDyadDataProtocol.CLEAN_MODE: lambda val: DyadCleanMode(val).name,
49
+ RoborockDyadDataProtocol.SUCTION: lambda val: DyadSuction(val).name,
50
+ RoborockDyadDataProtocol.WATER_LEVEL: lambda val: DyadWaterLevel(val).name,
51
+ RoborockDyadDataProtocol.BRUSH_SPEED: lambda val: DyadBrushSpeed(val).name,
52
+ RoborockDyadDataProtocol.POWER: lambda val: int(val),
53
+ RoborockDyadDataProtocol.AUTO_DRY: lambda val: bool(val),
54
+ RoborockDyadDataProtocol.MESH_LEFT: lambda val: int(360000 - val * 60),
55
+ RoborockDyadDataProtocol.BRUSH_LEFT: lambda val: int(360000 - val * 60),
56
+ RoborockDyadDataProtocol.ERROR: lambda val: DyadError(val).name,
57
+ RoborockDyadDataProtocol.VOLUME_SET: lambda val: int(val),
58
+ RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: lambda val: bool(val),
59
+ RoborockDyadDataProtocol.AUTO_DRY_MODE: lambda val: bool(val),
60
+ RoborockDyadDataProtocol.SILENT_DRY_DURATION: lambda val: int(val), # in minutes
61
+ RoborockDyadDataProtocol.SILENT_MODE: lambda val: bool(val),
62
+ RoborockDyadDataProtocol.SILENT_MODE_START_TIME: lambda val: time(
63
+ hour=int(val / 60), minute=val % 60
64
+ ), # in minutes since 00:00
65
+ RoborockDyadDataProtocol.SILENT_MODE_END_TIME: lambda val: time(
66
+ hour=int(val / 60), minute=val % 60
67
+ ), # in minutes since 00:00
68
+ RoborockDyadDataProtocol.RECENT_RUN_TIME: lambda val: [
69
+ int(v) for v in val.split(",")
70
+ ], # minutes of cleaning in past few days.
71
+ RoborockDyadDataProtocol.TOTAL_RUN_TIME: lambda val: int(val),
72
+ RoborockDyadDataProtocol.SND_STATE: lambda val: DyadSndState.from_dict(val),
73
+ RoborockDyadDataProtocol.PRODUCT_INFO: lambda val: DyadProductInfo.from_dict(val),
74
+ }
75
+
76
+ ZEO_PROTOCOL_ENTRIES: dict[RoborockZeoProtocol, Callable] = {
77
+ # ro
78
+ RoborockZeoProtocol.STATE: lambda val: ZeoState(val).name,
79
+ RoborockZeoProtocol.COUNTDOWN: lambda val: int(val),
80
+ RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val),
81
+ RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name,
82
+ RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val),
83
+ RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val),
84
+ RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val),
85
+ # rw
86
+ RoborockZeoProtocol.MODE: lambda val: ZeoMode(val).name,
87
+ RoborockZeoProtocol.PROGRAM: lambda val: ZeoProgram(val).name,
88
+ RoborockZeoProtocol.TEMP: lambda val: ZeoTemperature(val).name,
89
+ RoborockZeoProtocol.RINSE_TIMES: lambda val: ZeoRinse(val).name,
90
+ RoborockZeoProtocol.SPIN_LEVEL: lambda val: ZeoSpin(val).name,
91
+ RoborockZeoProtocol.DRYING_MODE: lambda val: ZeoDryingMode(val).name,
92
+ RoborockZeoProtocol.DETERGENT_TYPE: lambda val: ZeoDetergentType(val).name,
93
+ RoborockZeoProtocol.SOFTENER_TYPE: lambda val: ZeoSoftenerType(val).name,
94
+ RoborockZeoProtocol.SOUND_SET: lambda val: bool(val),
95
+ }
96
+
97
+
98
+ def convert_dyad_value(protocol: int, value: Any) -> Any:
99
+ """Convert a dyad protocol value to its corresponding type."""
100
+ protocol_value = RoborockDyadDataProtocol(protocol)
101
+ if (converter := DYAD_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
102
+ return converter(value)
103
+ return None
104
+
105
+
106
+ def convert_zeo_value(protocol: int, value: Any) -> Any:
107
+ """Convert a zeo protocol value to its corresponding type."""
108
+ protocol_value = RoborockZeoProtocol(protocol)
109
+ if (converter := ZEO_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
110
+ return converter(value)
111
+ return None
112
+
113
+
114
+ class RoborockClientA01(RoborockClient, ABC):
115
+ """Roborock client base class for A01 devices."""
116
+
117
+ value_converter: Callable[[int, Any], Any] | None = None
118
+
119
+ def __init__(self, device_info: DeviceData, category: RoborockCategory):
120
+ """Initialize the Roborock client."""
121
+ super().__init__(device_info)
122
+ if category == RoborockCategory.WET_DRY_VAC:
123
+ self.value_converter = convert_dyad_value
124
+ elif category == RoborockCategory.WASHING_MACHINE:
125
+ self.value_converter = convert_zeo_value
126
+ else:
127
+ _LOGGER.debug("Device category %s is not (yet) supported", category)
128
+ self.value_converter = None
129
+
130
+ def on_message_received(self, messages: list[RoborockMessage]) -> None:
131
+ if self.value_converter is None:
132
+ return
133
+ for message in messages:
134
+ protocol = message.protocol
135
+ if message.payload and protocol in [
136
+ RoborockMessageProtocol.RPC_RESPONSE,
137
+ RoborockMessageProtocol.GENERAL_REQUEST,
138
+ ]:
139
+ try:
140
+ data_points = decode_rpc_response(message)
141
+ except RoborockException as err:
142
+ self._logger.debug("Failed to decode message: %s", err)
143
+ continue
144
+ for data_point_number, data_point in data_points.items():
145
+ self._logger.debug("received msg with dps, protocol: %s, %s", data_point_number, protocol)
146
+ if converted_response := self.value_converter(data_point_number, data_point):
147
+ queue = self._waiting_queue.get(int(data_point_number))
148
+ if queue and queue.protocol == protocol:
149
+ queue.set_result(converted_response)
150
+ else:
151
+ self._logger.debug(
152
+ "Received unknown data point %s for protocol %s, ignoring", data_point_number, protocol
153
+ )
154
+
155
+ @abstractmethod
156
+ async def update_values(
157
+ self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
158
+ ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any]:
159
+ """This should handle updating for each given protocol."""
@@ -4,11 +4,12 @@ import logging
4
4
  import typing
5
5
 
6
6
  from Crypto.Cipher import AES
7
- from Crypto.Util.Padding import pad, unpad
7
+ from Crypto.Util.Padding import unpad
8
8
 
9
9
  from roborock.cloud_api import RoborockMqttClient
10
10
  from roborock.containers import DeviceData, RoborockCategory, UserData
11
11
  from roborock.exceptions import RoborockException
12
+ from roborock.protocols.a01_protocol import encode_mqtt_payload
12
13
  from roborock.roborock_message import (
13
14
  RoborockDyadDataProtocol,
14
15
  RoborockMessage,
@@ -43,7 +44,6 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
43
44
  response_protocol = RoborockMessageProtocol.RPC_RESPONSE
44
45
 
45
46
  m = self._encoder(roborock_message)
46
- # self._logger.debug(f"id={request_id} Requesting method {method} with {params}")
47
47
  payload = json.loads(unpad(roborock_message.payload, AES.block_size))
48
48
  futures = []
49
49
  if "10000" in payload["dps"]:
@@ -56,7 +56,6 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
56
56
  for i, dps in enumerate(json.loads(payload["dps"]["10000"])):
57
57
  response = responses[i]
58
58
  if isinstance(response, BaseException):
59
- self._logger.warning("Timed out get req for %s after %s s", dps, self.queue_timeout)
60
59
  dps_responses[dps] = None
61
60
  else:
62
61
  dps_responses[dps] = response
@@ -65,24 +64,14 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
65
64
  async def update_values(
66
65
  self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
67
66
  ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, typing.Any]:
68
- payload = {"dps": {RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}}
69
- return await self.send_message(
70
- RoborockMessage(
71
- protocol=RoborockMessageProtocol.RPC_REQUEST,
72
- version=b"A01",
73
- payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size),
74
- )
67
+ message = encode_mqtt_payload(
68
+ {RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}
75
69
  )
70
+ return await self.send_message(message)
76
71
 
77
72
  async def set_value(
78
73
  self, protocol: RoborockDyadDataProtocol | RoborockZeoProtocol, value: typing.Any
79
74
  ) -> dict[int, typing.Any]:
80
75
  """Set a value for a specific protocol on the A01 device."""
81
- payload = {"dps": {int(protocol): value}}
82
- return await self.send_message(
83
- RoborockMessage(
84
- protocol=RoborockMessageProtocol.RPC_REQUEST,
85
- version=b"A01",
86
- payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size),
87
- )
88
- )
76
+ message = encode_mqtt_payload({protocol: value})
77
+ return await self.send_message(message)
@@ -1,153 +0,0 @@
1
- import dataclasses
2
- import json
3
- import logging
4
- import typing
5
- from abc import ABC, abstractmethod
6
- from collections.abc import Callable
7
- from datetime import time
8
-
9
- from Crypto.Cipher import AES
10
- from Crypto.Util.Padding import unpad
11
-
12
- from roborock import DeviceData
13
- from roborock.api import RoborockClient
14
- from roborock.code_mappings import (
15
- DyadBrushSpeed,
16
- DyadCleanMode,
17
- DyadError,
18
- DyadSelfCleanLevel,
19
- DyadSelfCleanMode,
20
- DyadSuction,
21
- DyadWarmLevel,
22
- DyadWaterLevel,
23
- RoborockDyadStateCode,
24
- ZeoDetergentType,
25
- ZeoDryingMode,
26
- ZeoError,
27
- ZeoMode,
28
- ZeoProgram,
29
- ZeoRinse,
30
- ZeoSoftenerType,
31
- ZeoSpin,
32
- ZeoState,
33
- ZeoTemperature,
34
- )
35
- from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory
36
- from roborock.roborock_message import (
37
- RoborockDyadDataProtocol,
38
- RoborockMessage,
39
- RoborockMessageProtocol,
40
- RoborockZeoProtocol,
41
- )
42
-
43
- _LOGGER = logging.getLogger(__name__)
44
-
45
-
46
- @dataclasses.dataclass
47
- class A01ProtocolCacheEntry:
48
- post_process_fn: Callable
49
- value: typing.Any | None = None
50
-
51
-
52
- # Right now this cache is not active, it was too much complexity for the initial addition of dyad.
53
- protocol_entries = {
54
- RoborockDyadDataProtocol.STATUS: A01ProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
55
- RoborockDyadDataProtocol.SELF_CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
56
- RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: A01ProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
57
- RoborockDyadDataProtocol.WARM_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
58
- RoborockDyadDataProtocol.CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
59
- RoborockDyadDataProtocol.SUCTION: A01ProtocolCacheEntry(lambda val: DyadSuction(val).name),
60
- RoborockDyadDataProtocol.WATER_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
61
- RoborockDyadDataProtocol.BRUSH_SPEED: A01ProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
62
- RoborockDyadDataProtocol.POWER: A01ProtocolCacheEntry(lambda val: int(val)),
63
- RoborockDyadDataProtocol.AUTO_DRY: A01ProtocolCacheEntry(lambda val: bool(val)),
64
- RoborockDyadDataProtocol.MESH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
65
- RoborockDyadDataProtocol.BRUSH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
66
- RoborockDyadDataProtocol.ERROR: A01ProtocolCacheEntry(lambda val: DyadError(val).name),
67
- RoborockDyadDataProtocol.VOLUME_SET: A01ProtocolCacheEntry(lambda val: int(val)),
68
- RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: A01ProtocolCacheEntry(lambda val: bool(val)),
69
- RoborockDyadDataProtocol.AUTO_DRY_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
70
- RoborockDyadDataProtocol.SILENT_DRY_DURATION: A01ProtocolCacheEntry(lambda val: int(val)), # in minutes
71
- RoborockDyadDataProtocol.SILENT_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
72
- RoborockDyadDataProtocol.SILENT_MODE_START_TIME: A01ProtocolCacheEntry(
73
- lambda val: time(hour=int(val / 60), minute=val % 60)
74
- ), # in minutes since 00:00
75
- RoborockDyadDataProtocol.SILENT_MODE_END_TIME: A01ProtocolCacheEntry(
76
- lambda val: time(hour=int(val / 60), minute=val % 60)
77
- ), # in minutes since 00:00
78
- RoborockDyadDataProtocol.RECENT_RUN_TIME: A01ProtocolCacheEntry(
79
- lambda val: [int(v) for v in val.split(",")]
80
- ), # minutes of cleaning in past few days.
81
- RoborockDyadDataProtocol.TOTAL_RUN_TIME: A01ProtocolCacheEntry(lambda val: int(val)),
82
- RoborockDyadDataProtocol.SND_STATE: A01ProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
83
- RoborockDyadDataProtocol.PRODUCT_INFO: A01ProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
84
- }
85
-
86
- zeo_data_protocol_entries = {
87
- # ro
88
- RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name),
89
- RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)),
90
- RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: int(val)),
91
- RoborockZeoProtocol.ERROR: A01ProtocolCacheEntry(lambda val: ZeoError(val).name),
92
- RoborockZeoProtocol.TIMES_AFTER_CLEAN: A01ProtocolCacheEntry(lambda val: int(val)),
93
- RoborockZeoProtocol.DETERGENT_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
94
- RoborockZeoProtocol.SOFTENER_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
95
- # rw
96
- RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name),
97
- RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val).name),
98
- RoborockZeoProtocol.TEMP: A01ProtocolCacheEntry(lambda val: ZeoTemperature(val).name),
99
- RoborockZeoProtocol.RINSE_TIMES: A01ProtocolCacheEntry(lambda val: ZeoRinse(val).name),
100
- RoborockZeoProtocol.SPIN_LEVEL: A01ProtocolCacheEntry(lambda val: ZeoSpin(val).name),
101
- RoborockZeoProtocol.DRYING_MODE: A01ProtocolCacheEntry(lambda val: ZeoDryingMode(val).name),
102
- RoborockZeoProtocol.DETERGENT_TYPE: A01ProtocolCacheEntry(lambda val: ZeoDetergentType(val).name),
103
- RoborockZeoProtocol.SOFTENER_TYPE: A01ProtocolCacheEntry(lambda val: ZeoSoftenerType(val).name),
104
- RoborockZeoProtocol.SOUND_SET: A01ProtocolCacheEntry(lambda val: bool(val)),
105
- }
106
-
107
-
108
- class RoborockClientA01(RoborockClient, ABC):
109
- """Roborock client base class for A01 devices."""
110
-
111
- def __init__(self, device_info: DeviceData, category: RoborockCategory):
112
- """Initialize the Roborock client."""
113
- super().__init__(device_info)
114
- self.category = category
115
-
116
- def on_message_received(self, messages: list[RoborockMessage]) -> None:
117
- for message in messages:
118
- protocol = message.protocol
119
- if message.payload and protocol in [
120
- RoborockMessageProtocol.RPC_RESPONSE,
121
- RoborockMessageProtocol.GENERAL_REQUEST,
122
- ]:
123
- payload = message.payload
124
- try:
125
- payload = unpad(payload, AES.block_size)
126
- except Exception as err:
127
- self._logger.debug("Failed to unpad payload: %s", err)
128
- continue
129
- payload_json = json.loads(payload.decode())
130
- for data_point_number, data_point in payload_json.get("dps").items():
131
- data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol
132
- self._logger.debug("received msg with dps, protocol: %s, %s", data_point_number, protocol)
133
- entries: dict
134
- if self.category == RoborockCategory.WET_DRY_VAC:
135
- data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
136
- entries = protocol_entries
137
- elif self.category == RoborockCategory.WASHING_MACHINE:
138
- data_point_protocol = RoborockZeoProtocol(int(data_point_number))
139
- entries = zeo_data_protocol_entries
140
- else:
141
- continue
142
- if data_point_protocol in entries:
143
- # Auto convert into data struct we want.
144
- converted_response = entries[data_point_protocol].post_process_fn(data_point)
145
- queue = self._waiting_queue.get(int(data_point_number))
146
- if queue and queue.protocol == protocol:
147
- queue.set_result(converted_response)
148
-
149
- @abstractmethod
150
- async def update_values(
151
- self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
152
- ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, typing.Any]:
153
- """This should handle updating for each given protocol."""