python-roborock 2.24.0__tar.gz → 2.25.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 (38) hide show
  1. {python_roborock-2.24.0 → python_roborock-2.25.0}/PKG-INFO +1 -1
  2. {python_roborock-2.24.0 → python_roborock-2.25.0}/pyproject.toml +2 -1
  3. python_roborock-2.25.0/roborock/protocols/v1_protocol.py +91 -0
  4. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/version_1_apis/roborock_client_v1.py +3 -33
  5. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/version_1_apis/roborock_local_client_v1.py +8 -17
  6. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +7 -4
  7. {python_roborock-2.24.0 → python_roborock-2.25.0}/LICENSE +0 -0
  8. {python_roborock-2.24.0 → python_roborock-2.25.0}/README.md +0 -0
  9. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/__init__.py +0 -0
  10. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/api.py +0 -0
  11. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/cli.py +0 -0
  12. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/cloud_api.py +0 -0
  13. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/code_mappings.py +0 -0
  14. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/command_cache.py +0 -0
  15. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/const.py +0 -0
  16. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/containers.py +0 -0
  17. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/devices/README.md +0 -0
  18. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/devices/__init__.py +0 -0
  19. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/devices/device.py +0 -0
  20. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/devices/device_manager.py +0 -0
  21. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/devices/local_channel.py +0 -0
  22. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/devices/mqtt_channel.py +0 -0
  23. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/exceptions.py +0 -0
  24. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/local_api.py +0 -0
  25. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/mqtt/__init__.py +0 -0
  26. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/mqtt/roborock_session.py +0 -0
  27. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/mqtt/session.py +0 -0
  28. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/protocol.py +0 -0
  29. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/py.typed +0 -0
  30. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/roborock_future.py +0 -0
  31. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/roborock_message.py +0 -0
  32. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/roborock_typing.py +0 -0
  33. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/util.py +0 -0
  34. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/version_1_apis/__init__.py +0 -0
  35. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/version_a01_apis/__init__.py +0 -0
  36. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  37. {python_roborock-2.24.0 → python_roborock-2.25.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  38. {python_roborock-2.24.0 → python_roborock-2.25.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.24.0
3
+ Version: 2.25.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.24.0"
3
+ version = "2.25.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"
@@ -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"
@@ -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):