python-roborock 2.24.0__tar.gz → 2.25.1__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 (38) hide show
  1. {python_roborock-2.24.0 → python_roborock-2.25.1}/PKG-INFO +1 -1
  2. {python_roborock-2.24.0 → python_roborock-2.25.1}/pyproject.toml +2 -1
  3. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/code_mappings.py +31 -0
  4. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/const.py +1 -0
  5. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/containers.py +12 -0
  6. python_roborock-2.25.1/roborock/protocols/v1_protocol.py +91 -0
  7. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/version_1_apis/roborock_client_v1.py +3 -33
  8. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/version_1_apis/roborock_local_client_v1.py +8 -17
  9. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/version_1_apis/roborock_mqtt_client_v1.py +7 -4
  10. {python_roborock-2.24.0 → python_roborock-2.25.1}/LICENSE +0 -0
  11. {python_roborock-2.24.0 → python_roborock-2.25.1}/README.md +0 -0
  12. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/__init__.py +0 -0
  13. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/api.py +0 -0
  14. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/cli.py +0 -0
  15. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/cloud_api.py +0 -0
  16. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/command_cache.py +0 -0
  17. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/devices/README.md +0 -0
  18. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/devices/__init__.py +0 -0
  19. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/devices/device.py +0 -0
  20. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/devices/device_manager.py +0 -0
  21. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/devices/local_channel.py +0 -0
  22. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/devices/mqtt_channel.py +0 -0
  23. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/exceptions.py +0 -0
  24. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/local_api.py +0 -0
  25. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/mqtt/__init__.py +0 -0
  26. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/mqtt/roborock_session.py +0 -0
  27. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/mqtt/session.py +0 -0
  28. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/protocol.py +0 -0
  29. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/py.typed +0 -0
  30. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/roborock_future.py +0 -0
  31. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/roborock_message.py +0 -0
  32. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/roborock_typing.py +0 -0
  33. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/util.py +0 -0
  34. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/version_1_apis/__init__.py +0 -0
  35. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/version_a01_apis/__init__.py +0 -0
  36. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  37. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  38. {python_roborock-2.24.0 → python_roborock-2.25.1}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.24.0
3
+ Version: 2.25.1
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.24.0"
3
+ version = "2.25.1"
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"
@@ -76,3 +76,4 @@ select=["E", "F", "UP", "I"]
76
76
  asyncio_mode = "auto"
77
77
  asyncio_default_fixture_loop_scope = "function"
78
78
  timeout = 30
79
+ log_format = "%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s"
@@ -300,6 +300,17 @@ class RoborockFanSpeedS8MaxVUltra(RoborockFanPowerCode):
300
300
  smart_mode = 110
301
301
 
302
302
 
303
+ class RoborockFanSpeedSaros10R(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
+
303
314
  class RoborockMopModeCode(RoborockEnum):
304
315
  """Describes the mop mode of the vacuum cleaner."""
305
316
 
@@ -341,6 +352,15 @@ class RoborockMopModeS8MaxVUltra(RoborockMopModeCode):
341
352
  smart_mode = 306
342
353
 
343
354
 
355
+ class RoborockMopModeSaros10R(RoborockMopModeCode):
356
+ standard = 300
357
+ deep = 301
358
+ custom = 302
359
+ deep_plus = 303
360
+ fast = 304
361
+ smart_mode = 306
362
+
363
+
344
364
  class RoborockMopModeQRevoMaster(RoborockMopModeCode):
345
365
  standard = 300
346
366
  deep = 301
@@ -438,6 +458,17 @@ class RoborockMopIntensityS8MaxVUltra(RoborockMopIntensityCode):
438
458
  custom_water_flow = 207
439
459
 
440
460
 
461
+ class RoborockMopIntensitySaros10R(RoborockMopIntensityCode):
462
+ off = 200
463
+ low = 201
464
+ medium = 202
465
+ high = 203
466
+ custom = 204
467
+ extreme = 250
468
+ vac_followed_by_mop = 235
469
+ smart_mode = 209
470
+
471
+
441
472
  class RoborockMopIntensityS5Max(RoborockMopIntensityCode):
442
473
  """Describes the mop intensity of the vacuum cleaner."""
443
474
 
@@ -50,6 +50,7 @@ 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"
53
54
 
54
55
  ROBOROCK_DYAD_AIR = "roborock.wetdryvac.a107"
55
56
  ROBOROCK_DYAD_PRO_COMBO = "roborock.wetdryvac.a83"
@@ -28,6 +28,7 @@ from .code_mappings import (
28
28
  RoborockFanSpeedS7,
29
29
  RoborockFanSpeedS7MaxV,
30
30
  RoborockFanSpeedS8MaxVUltra,
31
+ RoborockFanSpeedSaros10R,
31
32
  RoborockFinishReason,
32
33
  RoborockInCleaning,
33
34
  RoborockMopIntensityCode,
@@ -40,6 +41,7 @@ from .code_mappings import (
40
41
  RoborockMopIntensityS6MaxV,
41
42
  RoborockMopIntensityS7,
42
43
  RoborockMopIntensityS8MaxVUltra,
44
+ RoborockMopIntensitySaros10R,
43
45
  RoborockMopModeCode,
44
46
  RoborockMopModeQRevoCurv,
45
47
  RoborockMopModeQRevoMaster,
@@ -47,6 +49,7 @@ from .code_mappings import (
47
49
  RoborockMopModeS7,
48
50
  RoborockMopModeS8MaxVUltra,
49
51
  RoborockMopModeS8ProUltra,
52
+ RoborockMopModeSaros10R,
50
53
  RoborockStartType,
51
54
  RoborockStateCode,
52
55
  )
@@ -74,6 +77,7 @@ from .const import (
74
77
  ROBOROCK_S8,
75
78
  ROBOROCK_S8_MAXV_ULTRA,
76
79
  ROBOROCK_S8_PRO_ULTRA,
80
+ ROBOROCK_SAROS_10R,
77
81
  SENSOR_DIRTY_REPLACE_TIME,
78
82
  SIDE_BRUSH_REPLACE_TIME,
79
83
  STRAINER_REPLACE_TIME,
@@ -678,6 +682,13 @@ class S8MaxvUltraStatus(Status):
678
682
  mop_mode: RoborockMopModeS8MaxVUltra | None = None
679
683
 
680
684
 
685
+ @dataclass
686
+ class Saros10RStatus(Status):
687
+ fan_power: RoborockFanSpeedSaros10R | None = None
688
+ water_box_mode: RoborockMopIntensitySaros10R | None = None
689
+ mop_mode: RoborockMopModeSaros10R | None = None
690
+
691
+
681
692
  ModelStatus: dict[str, type[Status]] = {
682
693
  ROBOROCK_S4_MAX: S4MaxStatus,
683
694
  ROBOROCK_S5_MAX: S5MaxStatus,
@@ -701,6 +712,7 @@ ModelStatus: dict[str, type[Status]] = {
701
712
  ROBOROCK_QREVO_MAXV: QRevoMaxVStatus,
702
713
  ROBOROCK_QREVO_PRO: P10Status,
703
714
  ROBOROCK_S8_MAXV_ULTRA: S8MaxvUltraStatus,
715
+ ROBOROCK_SAROS_10R: Saros10RStatus,
704
716
  }
705
717
 
706
718
 
@@ -0,0 +1,91 @@
1
+ """Roborock V1 Protocol Encoder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import time
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ from roborock.roborock_message import MessageRetry, RoborockMessage, RoborockMessageProtocol
13
+ from roborock.roborock_typing import RoborockCommand
14
+ from roborock.util import get_next_int
15
+
16
+ CommandType = RoborockCommand | str
17
+ ParamsType = list | dict | int | None
18
+
19
+
20
+ @dataclass(frozen=True, kw_only=True)
21
+ class SecurityData:
22
+ """Security data included in the request for some V1 commands."""
23
+
24
+ endpoint: str
25
+ nonce: bytes
26
+
27
+ def to_dict(self) -> dict[str, Any]:
28
+ """Convert security data to a dictionary for sending in the payload."""
29
+ return {"security": {"endpoint": self.endpoint, "nonce": self.nonce.hex().lower()}}
30
+
31
+
32
+ @dataclass
33
+ class RequestMessage:
34
+ """Data structure for v1 RoborockMessage payloads."""
35
+
36
+ method: RoborockCommand | str
37
+ params: ParamsType
38
+ timestamp: int = field(default_factory=lambda: math.floor(time.time()))
39
+ request_id: int = field(default_factory=lambda: get_next_int(10000, 32767))
40
+
41
+ def as_payload(self, security_data: SecurityData | None) -> bytes:
42
+ """Convert the request arguments to a dictionary."""
43
+ inner = {
44
+ "id": self.request_id,
45
+ "method": self.method,
46
+ "params": self.params or [],
47
+ **(security_data.to_dict() if security_data else {}),
48
+ }
49
+ return bytes(
50
+ json.dumps(
51
+ {
52
+ "dps": {"101": json.dumps(inner, separators=(",", ":"))},
53
+ "t": self.timestamp,
54
+ },
55
+ separators=(",", ":"),
56
+ ).encode()
57
+ )
58
+
59
+
60
+ def create_mqtt_payload_encoder(security_data: SecurityData) -> Callable[[CommandType, ParamsType], RoborockMessage]:
61
+ """Create a payload encoder for V1 commands over MQTT."""
62
+
63
+ def _get_payload(method: CommandType, params: ParamsType) -> RoborockMessage:
64
+ """Build the payload for a V1 command."""
65
+ request = RequestMessage(method=method, params=params)
66
+ payload = request.as_payload(security_data) # always secure
67
+ return RoborockMessage(
68
+ timestamp=request.timestamp,
69
+ protocol=RoborockMessageProtocol.RPC_REQUEST,
70
+ payload=payload,
71
+ )
72
+
73
+ return _get_payload
74
+
75
+
76
+ def encode_local_payload(method: CommandType, params: ParamsType) -> RoborockMessage:
77
+ """Encode payload for V1 commands over local connection."""
78
+
79
+ request = RequestMessage(method=method, params=params)
80
+ payload = request.as_payload(security_data=None)
81
+
82
+ message_retry: MessageRetry | None = None
83
+ if method == RoborockCommand.RETRY_REQUEST and isinstance(params, dict):
84
+ message_retry = MessageRetry(method=method, retry_id=params["retry_id"])
85
+
86
+ return RoborockMessage(
87
+ timestamp=request.timestamp,
88
+ protocol=RoborockMessageProtocol.GENERAL_REQUEST,
89
+ payload=payload,
90
+ message_retry=message_retry,
91
+ )
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import dataclasses
3
3
  import json
4
- import math
5
4
  import struct
6
5
  import time
7
6
  from abc import ABC, abstractmethod
@@ -54,15 +53,15 @@ from roborock.roborock_message import (
54
53
  RoborockMessage,
55
54
  RoborockMessageProtocol,
56
55
  )
57
- from roborock.util import RepeatableTask, get_next_int, unpack_list
56
+ from roborock.util import RepeatableTask, unpack_list
57
+
58
+ CUSTOM_COMMANDS = {RoborockCommand.GET_MAP_CALIBRATION}
58
59
 
59
60
  COMMANDS_SECURED = {
60
61
  RoborockCommand.GET_MAP_V1,
61
62
  RoborockCommand.GET_MULTI_MAP,
62
63
  }
63
64
 
64
- CUSTOM_COMMANDS = {RoborockCommand.GET_MAP_CALIBRATION}
65
-
66
65
  CLOUD_REQUIRED = COMMANDS_SECURED.union(CUSTOM_COMMANDS)
67
66
 
68
67
  WASH_N_FILL_DOCK = [
@@ -340,35 +339,6 @@ class RoborockClientV1(RoborockClient, ABC):
340
339
  """Load the map into the vacuum's memory."""
341
340
  await self.send_command(RoborockCommand.LOAD_MULTI_MAP, [map_flag])
342
341
 
343
- def _get_payload(
344
- self,
345
- method: RoborockCommand | str,
346
- params: list | dict | int | None = None,
347
- secured=False,
348
- ):
349
- timestamp = math.floor(time.time())
350
- request_id = get_next_int(10000, 32767)
351
- inner = {
352
- "id": request_id,
353
- "method": method,
354
- "params": params or [],
355
- }
356
- if secured:
357
- inner["security"] = {
358
- "endpoint": self._endpoint,
359
- "nonce": self._nonce.hex().lower(),
360
- }
361
- payload = bytes(
362
- json.dumps(
363
- {
364
- "dps": {"101": json.dumps(inner, separators=(",", ":"))},
365
- "t": timestamp,
366
- },
367
- separators=(",", ":"),
368
- ).encode()
369
- )
370
- return request_id, timestamp, payload
371
-
372
342
  @abstractmethod
373
343
  async def _send_command(
374
344
  self,
@@ -4,9 +4,10 @@ from roborock.local_api import RoborockLocalClient
4
4
 
5
5
  from .. import CommandVacuumError, DeviceData, RoborockCommand, RoborockException
6
6
  from ..exceptions import VacuumError
7
- from ..roborock_message import MessageRetry, RoborockMessage, RoborockMessageProtocol
7
+ from ..protocols.v1_protocol import encode_local_payload
8
+ from ..roborock_message import RoborockMessage, RoborockMessageProtocol
8
9
  from ..util import RoborockLoggerAdapter
9
- from .roborock_client_v1 import COMMANDS_SECURED, RoborockClientV1
10
+ from .roborock_client_v1 import CLOUD_REQUIRED, RoborockClientV1
10
11
 
11
12
  _LOGGER = logging.getLogger(__name__)
12
13
 
@@ -21,26 +22,16 @@ class RoborockLocalClientV1(RoborockLocalClient, RoborockClientV1):
21
22
  self.queue_timeout = queue_timeout
22
23
  self._logger = RoborockLoggerAdapter(device_data.device.name, _LOGGER)
23
24
 
24
- def build_roborock_message(
25
- self, method: RoborockCommand | str, params: list | dict | int | None = None
26
- ) -> RoborockMessage:
27
- secured = True if method in COMMANDS_SECURED else False
28
- request_id, timestamp, payload = self._get_payload(method, params, secured)
29
- self._logger.debug("Building message id %s for method %s", request_id, method)
30
- request_protocol = RoborockMessageProtocol.GENERAL_REQUEST
31
- message_retry: MessageRetry | None = None
32
- if method == RoborockCommand.RETRY_REQUEST and isinstance(params, dict):
33
- message_retry = MessageRetry(method=params["method"], retry_id=params["retry_id"])
34
- return RoborockMessage(
35
- timestamp=timestamp, protocol=request_protocol, payload=payload, message_retry=message_retry
36
- )
37
-
38
25
  async def _send_command(
39
26
  self,
40
27
  method: RoborockCommand | str,
41
28
  params: list | dict | int | None = None,
42
29
  ):
43
- roborock_message = self.build_roborock_message(method, params)
30
+ if method in CLOUD_REQUIRED:
31
+ raise RoborockException(f"Method {method} is not supported over local connection")
32
+
33
+ roborock_message = encode_local_payload(method, params)
34
+ self._logger.debug("Building message id %s for method %s", roborock_message.get_request_id(), method)
44
35
  return await self.send_message(roborock_message)
45
36
 
46
37
  async def send_message(self, roborock_message: RoborockMessage):
@@ -11,6 +11,7 @@ from roborock.cloud_api import RoborockMqttClient
11
11
  from ..containers import DeviceData, UserData
12
12
  from ..exceptions import CommandVacuumError, RoborockException, VacuumError
13
13
  from ..protocol import Utils
14
+ from ..protocols.v1_protocol import SecurityData, create_mqtt_payload_encoder
14
15
  from ..roborock_message import (
15
16
  RoborockMessage,
16
17
  RoborockMessageProtocol,
@@ -36,6 +37,9 @@ class RoborockMqttClientV1(RoborockMqttClient, RoborockClientV1):
36
37
  RoborockClientV1.__init__(self, device_info, endpoint)
37
38
  self.queue_timeout = queue_timeout
38
39
  self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
40
+ self._payload_encoder = create_mqtt_payload_encoder(
41
+ SecurityData(endpoint=self._endpoint, nonce=self._nonce),
42
+ )
39
43
 
40
44
  async def send_message(self, roborock_message: RoborockMessage):
41
45
  await self.validate_connection()
@@ -78,10 +82,9 @@ class RoborockMqttClientV1(RoborockMqttClient, RoborockClientV1):
78
82
  if method in CUSTOM_COMMANDS:
79
83
  # When we have more custom commands do something more complicated here
80
84
  return await self._get_calibration_points()
81
- request_id, timestamp, payload = self._get_payload(method, params, True)
82
- self._logger.debug("Building message id %s for method %s", request_id, method)
83
- request_protocol = RoborockMessageProtocol.RPC_REQUEST
84
- roborock_message = RoborockMessage(timestamp=timestamp, protocol=request_protocol, payload=payload)
85
+
86
+ roborock_message = self._payload_encoder(method, params)
87
+ self._logger.debug("Building message id %s for method %s", roborock_message.get_request_id, method)
85
88
  return await self.send_message(roborock_message)
86
89
 
87
90
  async def _get_calibration_points(self):