python-roborock 0.41.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 (31) hide show
  1. {python_roborock-0.41.0 → python_roborock-2.0.0}/PKG-INFO +1 -1
  2. {python_roborock-0.41.0 → python_roborock-2.0.0}/pyproject.toml +13 -1
  3. python_roborock-2.0.0/roborock/api.py +127 -0
  4. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/cloud_api.py +6 -3
  5. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/code_mappings.py +218 -2
  6. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/containers.py +25 -0
  7. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/local_api.py +1 -38
  8. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/protocol.py +21 -5
  9. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/roborock_message.py +44 -2
  10. python_roborock-2.0.0/roborock/version_1_apis/__init__.py +3 -0
  11. python_roborock-0.41.0/roborock/api.py → python_roborock-2.0.0/roborock/version_1_apis/roborock_client_v1.py +239 -136
  12. python_roborock-2.0.0/roborock/version_1_apis/roborock_local_client_v1.py +72 -0
  13. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +5 -2
  14. python_roborock-2.0.0/roborock/version_a01_apis/__init__.py +2 -0
  15. python_roborock-2.0.0/roborock/version_a01_apis/roborock_client_a01.py +142 -0
  16. python_roborock-2.0.0/roborock/version_a01_apis/roborock_mqtt_client_a01.py +68 -0
  17. python_roborock-0.41.0/roborock/version_1_apis/__init__.py +0 -0
  18. python_roborock-0.41.0/roborock/version_1_apis/roborock_client_v1.py +0 -227
  19. python_roborock-0.41.0/roborock/version_1_apis/roborock_local_client_v1.py +0 -32
  20. {python_roborock-0.41.0 → python_roborock-2.0.0}/LICENSE +0 -0
  21. {python_roborock-0.41.0 → python_roborock-2.0.0}/README.md +0 -0
  22. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/__init__.py +0 -0
  23. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/cli.py +0 -0
  24. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/command_cache.py +0 -0
  25. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/const.py +0 -0
  26. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/exceptions.py +0 -0
  27. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/py.typed +0 -0
  28. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/roborock_future.py +0 -0
  29. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/roborock_typing.py +0 -0
  30. {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/util.py +0 -0
  31. {python_roborock-0.41.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: 0.41.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 = "0.41.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"
@@ -49,8 +49,20 @@ pyshark = "^0.6"
49
49
  branch = "main"
50
50
  version_toml = "pyproject.toml:tool.poetry.version"
51
51
  build_command = "pip install poetry && poetry build"
52
+ [tool.semantic_release.commit_parser_options]
53
+ allowed_tags = [
54
+ "chore",
55
+ "docs",
56
+ "feat",
57
+ "fix",
58
+ "refactor"
59
+ ]
60
+ major_tags= ["refactor"]
52
61
 
53
62
  [tool.ruff]
54
63
  ignore = ["F403", "E741"]
55
64
  line-length = 120
56
65
  select=["E", "F", "UP", "I"]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ "*/__init__.py" = ["F401"]
@@ -0,0 +1,127 @@
1
+ """The Roborock api."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import base64
7
+ import logging
8
+ import secrets
9
+ import time
10
+ from collections.abc import Callable, Coroutine
11
+ from typing import Any
12
+
13
+ from .containers import (
14
+ DeviceData,
15
+ )
16
+ from .exceptions import (
17
+ RoborockTimeout,
18
+ UnknownMethodError,
19
+ VacuumError,
20
+ )
21
+ from .roborock_future import RoborockFuture
22
+ from .roborock_message import (
23
+ RoborockMessage,
24
+ )
25
+ from .roborock_typing import RoborockCommand
26
+ from .util import RoborockLoggerAdapter, get_running_loop_or_create_one
27
+
28
+ _LOGGER = logging.getLogger(__name__)
29
+ KEEPALIVE = 60
30
+
31
+
32
+ class RoborockClient:
33
+ def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = 4) -> None:
34
+ self.event_loop = get_running_loop_or_create_one()
35
+ self.device_info = device_info
36
+ self._endpoint = endpoint
37
+ self._nonce = secrets.token_bytes(16)
38
+ self._waiting_queue: dict[int, RoborockFuture] = {}
39
+ self._last_device_msg_in = self.time_func()
40
+ self._last_disconnection = self.time_func()
41
+ self.keep_alive = KEEPALIVE
42
+ self._diagnostic_data: dict[str, dict[str, Any]] = {
43
+ "misc_info": {"Nonce": base64.b64encode(self._nonce).decode("utf-8")}
44
+ }
45
+ self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
46
+ self.is_available: bool = True
47
+ self.queue_timeout = queue_timeout
48
+
49
+ def __del__(self) -> None:
50
+ self.release()
51
+
52
+ def release(self):
53
+ self.sync_disconnect()
54
+
55
+ async def async_release(self):
56
+ await self.async_disconnect()
57
+
58
+ @property
59
+ def diagnostic_data(self) -> dict:
60
+ return self._diagnostic_data
61
+
62
+ @property
63
+ def time_func(self) -> Callable[[], float]:
64
+ try:
65
+ # Use monotonic clock if available
66
+ time_func = time.monotonic
67
+ except AttributeError:
68
+ time_func = time.time
69
+ return time_func
70
+
71
+ async def async_connect(self):
72
+ raise NotImplementedError
73
+
74
+ def sync_disconnect(self) -> Any:
75
+ raise NotImplementedError
76
+
77
+ async def async_disconnect(self) -> Any:
78
+ raise NotImplementedError
79
+
80
+ def on_message_received(self, messages: list[RoborockMessage]) -> None:
81
+ raise NotImplementedError
82
+
83
+ def on_connection_lost(self, exc: Exception | None) -> None:
84
+ self._last_disconnection = self.time_func()
85
+ self._logger.info("Roborock client disconnected")
86
+ if exc is not None:
87
+ self._logger.warning(exc)
88
+
89
+ def should_keepalive(self) -> bool:
90
+ now = self.time_func()
91
+ # noinspection PyUnresolvedReferences
92
+ if now - self._last_disconnection > self.keep_alive**2 and now - self._last_device_msg_in > self.keep_alive:
93
+ return False
94
+ return True
95
+
96
+ async def validate_connection(self) -> None:
97
+ if not self.should_keepalive():
98
+ await self.async_disconnect()
99
+ await self.async_connect()
100
+
101
+ async def _wait_response(self, request_id: int, queue: RoborockFuture) -> tuple[Any, VacuumError | None]:
102
+ try:
103
+ (response, err) = await queue.async_get(self.queue_timeout)
104
+ if response == "unknown_method":
105
+ raise UnknownMethodError("Unknown method")
106
+ return response, err
107
+ except (asyncio.TimeoutError, asyncio.CancelledError):
108
+ raise RoborockTimeout(f"id={request_id} Timeout after {self.queue_timeout} seconds") from None
109
+ finally:
110
+ self._waiting_queue.pop(request_id, None)
111
+
112
+ def _async_response(
113
+ self, request_id: int, protocol_id: int = 0
114
+ ) -> Coroutine[Any, Any, tuple[Any, VacuumError | None]]:
115
+ queue = RoborockFuture(protocol_id)
116
+ self._waiting_queue[request_id] = queue
117
+ return self._wait_response(request_id, queue)
118
+
119
+ async def send_message(self, roborock_message: RoborockMessage):
120
+ raise NotImplementedError
121
+
122
+ async def _send_command(
123
+ self,
124
+ method: RoborockCommand | str,
125
+ params: list | dict | int | None = None,
126
+ ):
127
+ raise NotImplementedError
@@ -4,6 +4,7 @@ import asyncio
4
4
  import base64
5
5
  import logging
6
6
  import threading
7
+ import typing
7
8
  import uuid
8
9
  from asyncio import Lock, Task
9
10
  from typing import Any
@@ -11,15 +12,17 @@ from urllib.parse import urlparse
11
12
 
12
13
  import paho.mqtt.client as mqtt
13
14
 
14
- from .api import KEEPALIVE, RoborockClient, md5hex
15
+ from .api import KEEPALIVE, RoborockClient
15
16
  from .containers import DeviceData, UserData
16
17
  from .exceptions import RoborockException, VacuumError
17
- from .protocol import MessageParser, Utils
18
+ from .protocol import MessageParser, Utils, md5hex
18
19
  from .roborock_future import RoborockFuture
19
20
  from .roborock_message import RoborockMessage
20
21
  from .roborock_typing import RoborockCommand
21
22
  from .util import RoborockLoggerAdapter
22
23
 
24
+ if typing.TYPE_CHECKING:
25
+ pass
23
26
  _LOGGER = logging.getLogger(__name__)
24
27
  CONNECT_REQUEST_ID = 0
25
28
  DISCONNECT_REQUEST_ID = 1
@@ -78,7 +81,7 @@ class RoborockMqttClient(RoborockClient, mqtt.Client):
78
81
  connection_queue.resolve((True, None))
79
82
 
80
83
  def on_message(self, *args, **kwargs):
81
- _, __, msg = args
84
+ client, __, msg = args
82
85
  try:
83
86
  messages, _ = MessageParser.parse(msg.payload, local_key=self.device_info.device.local_key)
84
87
  super().on_message_received(messages)
@@ -98,8 +98,8 @@ class RoborockDyadStateCode(RoborockEnum):
98
98
  self_clean_deep_cleaning = 6
99
99
  self_clean_rinsing = 7
100
100
  self_clean_dehydrating = 8
101
- drying = 10
102
- ventilating = 11 # drying
101
+ drying = 9
102
+ ventilating = 10 # drying
103
103
  reserving = 12
104
104
  mop_washing_paused = 13
105
105
  dusting_mode = 14
@@ -369,3 +369,219 @@ class RoborockCategory(Enum):
369
369
  def __missing__(self, key):
370
370
  _LOGGER.warning("Missing key %s from category", key)
371
371
  return RoborockCategory.UNKNOWN
372
+
373
+
374
+ class DyadSelfCleanMode(RoborockEnum):
375
+ self_clean = 1
376
+ self_clean_and_dry = 2
377
+ dry = 3
378
+ ventilation = 4
379
+
380
+
381
+ class DyadSelfCleanLevel(RoborockEnum):
382
+ normal = 1
383
+ deep = 2
384
+
385
+
386
+ class DyadWarmLevel(RoborockEnum):
387
+ normal = 1
388
+ deep = 2
389
+
390
+
391
+ class DyadMode(RoborockEnum):
392
+ wash = 1
393
+ wash_and_dry = 2
394
+ dry = 3
395
+
396
+
397
+ class DyadCleanMode(RoborockEnum):
398
+ auto = 1
399
+ max = 2
400
+ dehydration = 3
401
+ power_saving = 4
402
+
403
+
404
+ class DyadSuction(RoborockEnum):
405
+ l1 = 1
406
+ l2 = 2
407
+ l3 = 3
408
+ l4 = 4
409
+ l5 = 5
410
+ l6 = 6
411
+
412
+
413
+ class DyadWaterLevel(RoborockEnum):
414
+ l1 = 1
415
+ l2 = 2
416
+ l3 = 3
417
+ l4 = 4
418
+
419
+
420
+ class DyadBrushSpeed(RoborockEnum):
421
+ l1 = 1
422
+ l2 = 2
423
+
424
+
425
+ class DyadCleanser(RoborockEnum):
426
+ none = 0
427
+ normal = 1
428
+ deep = 2
429
+ max = 3
430
+
431
+
432
+ class DyadError(RoborockEnum):
433
+ none = 0
434
+ dirty_tank_full = 20000 # Dirty tank full. Empty it
435
+ water_level_sensor_stuck = 20001 # Water level sensor is stuck. Clean it.
436
+ clean_tank_empty = 20002 # Clean tank empty. Refill now
437
+ clean_head_entangled = 20003 # Check if the cleaning head is entangled with foreign objects.
438
+ clean_head_too_hot = 20004 # Cleaning head temperature protection. Wait for the temperature to return to normal.
439
+ fan_protection_e5 = 10005 # Fan protection (E5). Restart the vacuum cleaner.
440
+ cleaning_head_blocked = 20005 # Remove blockages from the cleaning head and pipes.
441
+ temperature_protection = 20006 # Temperature protection. Wait for the temperature to return to normal
442
+ fan_protection_e4 = 10004 # Fan protection (E4). Restart the vacuum cleaner.
443
+ fan_protection_e9 = 10009 # Fan protection (E9). Restart the vacuum cleaner.
444
+ battery_temperature_protection_e0 = 10000
445
+ battery_temperature_protection = (
446
+ 20007 # Battery temperature protection. Wait for the temperature to return to a normal range.
447
+ )
448
+ battery_temperature_protection_2 = 20008
449
+ power_adapter_error = 20009 # Check if the power adapter is working properly.
450
+ dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts.
451
+ low_battery = 20017 # Low battery level. Charge before starting self-cleaning.
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
@@ -829,3 +829,28 @@ class RoborockCategoryDetail(RoborockBase):
829
829
  @dataclass
830
830
  class ProductResponse(RoborockBase):
831
831
  category_detail_list: list[RoborockCategoryDetail]
832
+
833
+
834
+ @dataclass
835
+ class DyadProductInfo(RoborockBase):
836
+ sn: str
837
+ ssid: str
838
+ timezone: str
839
+ posix_timezone: str
840
+ ip: str
841
+ mac: str
842
+ oba: dict
843
+
844
+
845
+ @dataclass
846
+ class DyadSndState(RoborockBase):
847
+ sid_in_use: int
848
+ sid_version: int
849
+ location: str
850
+ bom: str
851
+ language: str
852
+
853
+
854
+ @dataclass
855
+ class DyadOtaNfo(RoborockBase):
856
+ mqttOtaData: dict
@@ -8,7 +8,7 @@ import async_timeout
8
8
 
9
9
  from . import DeviceData
10
10
  from .api import RoborockClient
11
- from .exceptions import CommandVacuumError, RoborockConnectionException, RoborockException
11
+ from .exceptions import RoborockConnectionException, RoborockException
12
12
  from .protocol import MessageParser
13
13
  from .roborock_message import RoborockMessage, RoborockMessageProtocol
14
14
  from .roborock_typing import RoborockCommand
@@ -121,40 +121,3 @@ class RoborockLocalClient(RoborockClient, asyncio.Protocol):
121
121
  self.transport.write(data)
122
122
  except Exception as e:
123
123
  raise RoborockException(e) from e
124
-
125
- async def send_message(self, roborock_message: RoborockMessage):
126
- await self.validate_connection()
127
- method = roborock_message.get_method()
128
- params = roborock_message.get_params()
129
- request_id: int | None
130
- if not method or not method.startswith("get"):
131
- request_id = roborock_message.seq
132
- response_protocol = request_id + 1
133
- else:
134
- request_id = roborock_message.get_request_id()
135
- response_protocol = RoborockMessageProtocol.GENERAL_REQUEST
136
- if request_id is None:
137
- raise RoborockException(f"Failed build message {roborock_message}")
138
- local_key = self.device_info.device.local_key
139
- msg = MessageParser.build(roborock_message, local_key=local_key)
140
- if method:
141
- self._logger.debug(f"id={request_id} Requesting method {method} with {params}")
142
- # Send the command to the Roborock device
143
- async_response = asyncio.ensure_future(self._async_response(request_id, response_protocol))
144
- self._send_msg_raw(msg)
145
- (response, err) = await async_response
146
- self._diagnostic_data[method if method is not None else "unknown"] = {
147
- "params": roborock_message.get_params(),
148
- "response": response,
149
- "error": err,
150
- }
151
- if err:
152
- raise CommandVacuumError(method, err) from err
153
- if roborock_message.protocol == RoborockMessageProtocol.GENERAL_REQUEST:
154
- self._logger.debug(f"id={request_id} Response from method {roborock_message.get_method()}: {response}")
155
- if response == "retry":
156
- retry_id = roborock_message.get_retry_id()
157
- return self.send_command(
158
- RoborockCommand.RETRY_REQUEST, {"retry_id": retry_id, "retry_count": 8, "method": method}
159
- )
160
- return response
@@ -13,7 +13,6 @@ from construct import ( # type: ignore
13
13
  Bytes,
14
14
  Checksum,
15
15
  ChecksumError,
16
- Const,
17
16
  Construct,
18
17
  Container,
19
18
  GreedyBytes,
@@ -36,11 +35,18 @@ from roborock.roborock_message import RoborockMessage
36
35
 
37
36
  _LOGGER = logging.getLogger(__name__)
38
37
  SALT = b"TXdfu$jyZ#TZHsg4"
38
+ A01_HASH = "726f626f726f636b2d67a6d6da"
39
39
  BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe"
40
40
  AP_CONFIG = 1
41
41
  SOCK_DISCOVERY = 2
42
42
 
43
43
 
44
+ def md5hex(message: str) -> str:
45
+ md5 = hashlib.md5()
46
+ md5.update(message.encode())
47
+ return md5.hexdigest()
48
+
49
+
44
50
  class RoborockProtocol(asyncio.DatagramProtocol):
45
51
  def __init__(self, timeout: int = 5):
46
52
  self.timeout = timeout
@@ -199,12 +205,22 @@ class EncryptionAdapter(Construct):
199
205
 
200
206
  :param obj: JSON object to encrypt
201
207
  """
208
+ if context.version == b"A01":
209
+ iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
210
+ decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
211
+ f = decipher.encrypt(obj)
212
+ return f
202
213
  token = self.token_func(context)
203
214
  encrypted = Utils.encrypt_ecb(obj, token)
204
215
  return encrypted
205
216
 
206
217
  def _decode(self, obj, context, _):
207
218
  """Decrypts the given payload with the token stored in the context."""
219
+ if context.version == b"A01":
220
+ iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
221
+ decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
222
+ f = decipher.decrypt(obj)
223
+ return f
208
224
  token = self.token_func(context)
209
225
  decrypted = Utils.decrypt_ecb(obj, token)
210
226
  return decrypted
@@ -227,9 +243,9 @@ class OptionalChecksum(Checksum):
227
243
 
228
244
  class PrefixedStruct(Struct):
229
245
  def _parse(self, stream, context, path):
230
- subcon1 = Peek(Optional(Const(b"1.0")))
246
+ subcon1 = Peek(Optional(Bytes(3)))
231
247
  peek_version = subcon1.parse_stream(stream, **context)
232
- if peek_version is None:
248
+ if peek_version not in (b"1.0", b"A01"):
233
249
  subcon2 = Bytes(4)
234
250
  subcon2.parse_stream(stream, **context)
235
251
  return super()._parse(stream, context, path)
@@ -251,7 +267,7 @@ class PrefixedStruct(Struct):
251
267
 
252
268
  _Message = RawCopy(
253
269
  Struct(
254
- "version" / Const(b"1.0"),
270
+ "version" / Bytes(3),
255
271
  "seq" / Int32ub,
256
272
  "random" / Int32ub,
257
273
  "timestamp" / Int32ub,
@@ -280,7 +296,7 @@ _BroadcastMessage = Struct(
280
296
  "message"
281
297
  / RawCopy(
282
298
  Struct(
283
- "version" / Const(b"1.0"),
299
+ "version" / Bytes(3),
284
300
  "seq" / Int32ub,
285
301
  "protocol" / Int16ub,
286
302
  "payload" / EncryptionAdapter(lambda ctx: BROADCAST_TOKEN),
@@ -57,7 +57,7 @@ class RoborockDyadDataProtocol(RoborockEnum):
57
57
  COUNTDOWN_TIME = 210
58
58
  AUTO_SELF_CLEAN_SET = 212
59
59
  AUTO_DRY = 213
60
- MESH_LEF = 214
60
+ MESH_LEFT = 214
61
61
  BRUSH_LEFT = 215
62
62
  ERROR = 216
63
63
  MESH_RESET = 218
@@ -70,7 +70,7 @@ class RoborockDyadDataProtocol(RoborockEnum):
70
70
  SILENT_MODE = 226
71
71
  SILENT_MODE_START_TIME = 227
72
72
  SILENT_MODE_END_TIME = 228
73
- RECENT_RUN_TIMe = 229
73
+ RECENT_RUN_TIME = 229
74
74
  TOTAL_RUN_TIME = 230
75
75
  FEATURE_INFO = 235
76
76
  RECOVER_SETTINGS = 236
@@ -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,3 @@
1
+ from .roborock_client_v1 import AttributeCache, RoborockClientV1
2
+ from .roborock_local_client_v1 import RoborockLocalClientV1
3
+ from .roborock_mqtt_client_v1 import RoborockMqttClientV1