python-roborock 1.0.0__tar.gz → 2.0.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.0.0}/PKG-INFO +1 -1
  2. {python_roborock-1.0.0 → python_roborock-2.0.0}/pyproject.toml +1 -1
  3. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/api.py +0 -9
  4. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/code_mappings.py +135 -0
  5. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/protocol.py +2 -3
  6. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/roborock_message.py +42 -0
  7. python_roborock-2.0.0/roborock/version_a01_apis/roborock_client_a01.py +142 -0
  8. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +21 -8
  9. python_roborock-1.0.0/roborock/version_a01_apis/roborock_client_a01.py +0 -100
  10. {python_roborock-1.0.0 → python_roborock-2.0.0}/LICENSE +0 -0
  11. {python_roborock-1.0.0 → python_roborock-2.0.0}/README.md +0 -0
  12. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/__init__.py +0 -0
  13. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/cli.py +0 -0
  14. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/cloud_api.py +0 -0
  15. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/command_cache.py +0 -0
  16. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/const.py +0 -0
  17. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/containers.py +0 -0
  18. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/exceptions.py +0 -0
  19. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/local_api.py +0 -0
  20. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/py.typed +0 -0
  21. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/roborock_future.py +0 -0
  22. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/roborock_typing.py +0 -0
  23. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/util.py +0 -0
  24. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/version_1_apis/__init__.py +0 -0
  25. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  26. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  27. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  28. {python_roborock-1.0.0 → python_roborock-2.0.0}/roborock/version_a01_apis/__init__.py +0 -0
  29. {python_roborock-1.0.0 → python_roborock-2.0.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.0.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.0.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
 
@@ -450,3 +450,138 @@ class DyadError(RoborockEnum):
450
450
  dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts.
451
451
  low_battery = 20017 # Low battery level. Charge before starting self-cleaning.
452
452
  battery_under_10 = 20018 # Charge until the battery level exceeds 10% before manually starting self-cleaning.
453
+
454
+
455
+ class ZeoMode(RoborockEnum):
456
+ wash = 1
457
+ wash_and_dry = 2
458
+ dry = 3
459
+
460
+
461
+ class ZeoState(RoborockEnum):
462
+ standby = 1
463
+ weighing = 2
464
+ soaking = 3
465
+ washing = 4
466
+ rinsing = 5
467
+ spinning = 6
468
+ drying = 7
469
+ cooling = 8
470
+ under_delay_start = 9
471
+ done = 10
472
+
473
+
474
+ class ZeoProgram(RoborockEnum):
475
+ standard = 1
476
+ quick = 2
477
+ sanitize = 3
478
+ wool = 4
479
+ air_refresh = 5
480
+ custom = 6
481
+ bedding = 7
482
+ down = 8
483
+ silk = 9
484
+ rinse_and_spin = 10
485
+ spin = 11
486
+ down_clean = 12
487
+ baby_care = 13
488
+ anti_allergen = 14
489
+ sportswear = 15
490
+ night = 16
491
+ new_clothes = 17
492
+ shirts = 18
493
+ synthetics = 19
494
+ underwear = 20
495
+ gentle = 21
496
+ intensive = 22
497
+ cotton_linen = 23
498
+ season = 24
499
+ warming = 25
500
+ bra = 26
501
+ panties = 27
502
+ boiling_wash = 28
503
+ socks = 30
504
+ towels = 31
505
+ anti_mite = 32
506
+ exo_40_60 = 33
507
+ twenty_c = 34
508
+ t_shirts = 35
509
+ stain_removal = 36
510
+
511
+
512
+ class ZeoSoak(RoborockEnum):
513
+ normal = 0
514
+ low = 1
515
+ medium = 2
516
+ high = 3
517
+ max = 4
518
+
519
+
520
+ class ZeoTemperature(RoborockEnum):
521
+ normal = 1
522
+ low = 2
523
+ medium = 3
524
+ high = 4
525
+ max = 5
526
+ twenty_c = 6
527
+
528
+
529
+ class ZeoRinse(RoborockEnum):
530
+ none = 0
531
+ min = 1
532
+ low = 2
533
+ mid = 3
534
+ high = 4
535
+ max = 5
536
+
537
+
538
+ class ZeoSpin(RoborockEnum):
539
+ none = 1
540
+ very_low = 2
541
+ low = 3
542
+ mid = 4
543
+ high = 5
544
+ very_high = 6
545
+ max = 7
546
+
547
+
548
+ class ZeoDryingMode(RoborockEnum):
549
+ none = 0
550
+ quick = 1
551
+ iron = 2
552
+ store = 3
553
+
554
+
555
+ class ZeoDetergentType(RoborockEnum):
556
+ empty = 0
557
+ low = 1
558
+ medium = 2
559
+ high = 3
560
+
561
+
562
+ class ZeoSoftenerType(RoborockEnum):
563
+ empty = 0
564
+ low = 1
565
+ medium = 2
566
+ high = 3
567
+
568
+
569
+ class ZeoError(RoborockEnum):
570
+ none = 0
571
+ refill_error = 1
572
+ drain_error = 2
573
+ door_lock_error = 3
574
+ water_level_error = 4
575
+ inverter_error = 5
576
+ heating_error = 6
577
+ temperature_error = 7
578
+ communication_error = 10
579
+ drying_error = 11
580
+ drying_error_e_12 = 12
581
+ drying_error_e_13 = 13
582
+ drying_error_e_14 = 14
583
+ drying_error_e_15 = 15
584
+ drying_error_e_16 = 16
585
+ drying_error_water_flow = 17 # Check for normal water flow
586
+ drying_error_restart = 18 # Restart the washer and try again
587
+ spin_error = 19 # re-arrange clothes
@@ -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,
@@ -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