python-roborock 2.35.0__tar.gz → 2.36.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 (54) hide show
  1. {python_roborock-2.35.0 → python_roborock-2.36.0}/PKG-INFO +1 -1
  2. {python_roborock-2.35.0 → python_roborock-2.36.0}/pyproject.toml +1 -1
  3. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/api.py +1 -6
  4. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/containers.py +23 -0
  5. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/local_channel.py +6 -17
  6. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/mqtt_channel.py +6 -16
  7. python_roborock-2.36.0/roborock/devices/pending.py +45 -0
  8. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/protocol.py +20 -0
  9. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/protocols/v1_protocol.py +39 -0
  10. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/version_1_apis/roborock_client_v1.py +19 -17
  11. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/version_1_apis/roborock_local_client_v1.py +1 -1
  12. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +4 -9
  13. {python_roborock-2.35.0 → python_roborock-2.36.0}/LICENSE +0 -0
  14. {python_roborock-2.35.0 → python_roborock-2.36.0}/README.md +0 -0
  15. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/__init__.py +0 -0
  16. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/clean_modes.py +0 -0
  17. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/cli.py +0 -0
  18. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/cloud_api.py +0 -0
  19. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/code_mappings.py +0 -0
  20. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/command_cache.py +0 -0
  21. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/const.py +0 -0
  22. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/device_features.py +0 -0
  23. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/README.md +0 -0
  24. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/__init__.py +0 -0
  25. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/a01_channel.py +0 -0
  26. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/b01_channel.py +0 -0
  27. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/cache.py +0 -0
  28. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/channel.py +0 -0
  29. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/device.py +0 -0
  30. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/device_manager.py +0 -0
  31. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/traits/b01/__init__.py +0 -0
  32. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/traits/b01/props.py +0 -0
  33. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/traits/dyad.py +0 -0
  34. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/traits/status.py +0 -0
  35. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/traits/trait.py +0 -0
  36. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/traits/zeo.py +0 -0
  37. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/v1_channel.py +0 -0
  38. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/devices/v1_rpc_channel.py +0 -0
  39. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/exceptions.py +0 -0
  40. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/mqtt/__init__.py +0 -0
  41. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/mqtt/roborock_session.py +0 -0
  42. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/mqtt/session.py +0 -0
  43. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/protocols/a01_protocol.py +0 -0
  44. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/protocols/b01_protocol.py +0 -0
  45. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/py.typed +0 -0
  46. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/roborock_future.py +0 -0
  47. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/roborock_message.py +0 -0
  48. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/roborock_typing.py +0 -0
  49. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/util.py +0 -0
  50. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/version_1_apis/__init__.py +0 -0
  51. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/version_a01_apis/__init__.py +0 -0
  52. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  53. {python_roborock-2.35.0 → python_roborock-2.36.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  54. {python_roborock-2.35.0 → python_roborock-2.36.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.35.0
3
+ Version: 2.36.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.35.0"
3
+ version = "2.36.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"
@@ -3,9 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import base64
7
6
  import logging
8
- import secrets
9
7
  import time
10
8
  from abc import ABC, abstractmethod
11
9
  from typing import Any
@@ -37,14 +35,11 @@ class RoborockClient(ABC):
37
35
  def __init__(self, device_info: DeviceData) -> None:
38
36
  """Initialize RoborockClient."""
39
37
  self.device_info = device_info
40
- self._nonce = secrets.token_bytes(16)
41
38
  self._waiting_queue: dict[int, RoborockFuture] = {}
42
39
  self._last_device_msg_in = time.monotonic()
43
40
  self._last_disconnection = time.monotonic()
44
41
  self.keep_alive = KEEPALIVE
45
- self._diagnostic_data: dict[str, dict[str, Any]] = {
46
- "misc_info": {"Nonce": base64.b64encode(self._nonce).decode("utf-8")}
47
- }
42
+ self._diagnostic_data: dict[str, dict[str, Any]] = {}
48
43
  self.is_available: bool = True
49
44
 
50
45
  async def async_release(self) -> None:
@@ -725,6 +725,29 @@ class NetworkInfo(RoborockBase):
725
725
  rssi: int | None = None
726
726
 
727
727
 
728
+ @dataclass
729
+ class AppInitStatusLocalInfo(RoborockBase):
730
+ location: str
731
+ bom: str | None = None
732
+ featureset: int | None = None
733
+ language: str | None = None
734
+ logserver: str | None = None
735
+ wifiplan: str | None = None
736
+ timezone: str | None = None
737
+ name: str | None = None
738
+
739
+
740
+ @dataclass
741
+ class AppInitStatus(RoborockBase):
742
+ local_info: AppInitStatusLocalInfo
743
+ feature_info: list[int]
744
+ new_feature_info: int
745
+ new_feature_info_str: str
746
+ new_feature_info_2: int | None = None
747
+ carriage_type: int | None = None
748
+ dsp_version: int | None = None
749
+
750
+
728
751
  @dataclass
729
752
  class DeviceData(RoborockBase):
730
753
  device: HomeDataDevice
@@ -11,6 +11,7 @@ from roborock.protocol import Decoder, Encoder, create_local_decoder, create_loc
11
11
  from roborock.roborock_message import RoborockMessage
12
12
 
13
13
  from .channel import Channel
14
+ from .pending import PendingRpcs
14
15
 
15
16
  _LOGGER = logging.getLogger(__name__)
16
17
  _PORT = 58867
@@ -47,10 +48,9 @@ class LocalChannel(Channel):
47
48
  self._is_connected = False
48
49
 
49
50
  # RPC support
50
- self._waiting_queue: dict[int, asyncio.Future[RoborockMessage]] = {}
51
+ self._pending_rpcs: PendingRpcs[int, RoborockMessage] = PendingRpcs()
51
52
  self._decoder: Decoder = create_local_decoder(local_key)
52
53
  self._encoder: Encoder = create_local_encoder(local_key)
53
- self._queue_lock = asyncio.Lock()
54
54
 
55
55
  @property
56
56
  def is_connected(self) -> bool:
@@ -114,11 +114,7 @@ class LocalChannel(Channel):
114
114
  if (request_id := message.get_request_id()) is None:
115
115
  _LOGGER.debug("Received message with no request_id")
116
116
  return
117
- async with self._queue_lock:
118
- if (future := self._waiting_queue.pop(request_id, None)) is not None:
119
- future.set_result(message)
120
- else:
121
- _LOGGER.debug("Received message with no waiting handler: request_id=%s", request_id)
117
+ await self._pending_rpcs.resolve(request_id, message)
122
118
 
123
119
  async def send_message(self, message: RoborockMessage, timeout: float = 10.0) -> RoborockMessage:
124
120
  """Send a command message and wait for the response message."""
@@ -132,24 +128,17 @@ class LocalChannel(Channel):
132
128
  _LOGGER.exception("Error getting request_id from message: %s", err)
133
129
  raise RoborockException(f"Invalid message format, Message must have a request_id: {err}") from err
134
130
 
135
- future: asyncio.Future[RoborockMessage] = asyncio.Future()
136
- async with self._queue_lock:
137
- if request_id in self._waiting_queue:
138
- raise RoborockException(f"Request ID {request_id} already pending, cannot send command")
139
- self._waiting_queue[request_id] = future
140
-
131
+ future: asyncio.Future[RoborockMessage] = await self._pending_rpcs.start(request_id)
141
132
  try:
142
133
  encoded_msg = self._encoder(message)
143
134
  self._transport.write(encoded_msg)
144
135
  return await asyncio.wait_for(future, timeout=timeout)
145
136
  except asyncio.TimeoutError as ex:
146
- async with self._queue_lock:
147
- self._waiting_queue.pop(request_id, None)
137
+ await self._pending_rpcs.pop(request_id)
148
138
  raise RoborockException(f"Command timed out after {timeout}s") from ex
149
139
  except Exception:
150
140
  logging.exception("Uncaught error sending command")
151
- async with self._queue_lock:
152
- self._waiting_queue.pop(request_id, None)
141
+ await self._pending_rpcs.pop(request_id)
153
142
  raise
154
143
 
155
144
 
@@ -12,6 +12,7 @@ from roborock.protocol import create_mqtt_decoder, create_mqtt_encoder
12
12
  from roborock.roborock_message import RoborockMessage
13
13
 
14
14
  from .channel import Channel
15
+ from .pending import PendingRpcs
15
16
 
16
17
  _LOGGER = logging.getLogger(__name__)
17
18
 
@@ -31,10 +32,9 @@ class MqttChannel(Channel):
31
32
  self._mqtt_params = mqtt_params
32
33
 
33
34
  # RPC support
34
- self._waiting_queue: dict[int, asyncio.Future[RoborockMessage]] = {}
35
+ self._pending_rpcs: PendingRpcs[int, RoborockMessage] = PendingRpcs()
35
36
  self._decoder = create_mqtt_decoder(local_key)
36
37
  self._encoder = create_mqtt_encoder(local_key)
37
- self._queue_lock = asyncio.Lock()
38
38
  self._mqtt_unsub: Callable[[], None] | None = None
39
39
 
40
40
  @property
@@ -89,11 +89,7 @@ class MqttChannel(Channel):
89
89
  if (request_id := message.get_request_id()) is None:
90
90
  _LOGGER.debug("Received message with no request_id")
91
91
  return
92
- async with self._queue_lock:
93
- if (future := self._waiting_queue.pop(request_id, None)) is not None:
94
- future.set_result(message)
95
- else:
96
- _LOGGER.debug("Received message with no waiting handler: request_id=%s", request_id)
92
+ await self._pending_rpcs.resolve(request_id, message)
97
93
 
98
94
  async def send_message(self, message: RoborockMessage, timeout: float = 10.0) -> RoborockMessage:
99
95
  """Send a command message and wait for the response message.
@@ -107,11 +103,7 @@ class MqttChannel(Channel):
107
103
  _LOGGER.exception("Error getting request_id from message: %s", err)
108
104
  raise RoborockException(f"Invalid message format, Message must have a request_id: {err}") from err
109
105
 
110
- future: asyncio.Future[RoborockMessage] = asyncio.Future()
111
- async with self._queue_lock:
112
- if request_id in self._waiting_queue:
113
- raise RoborockException(f"Request ID {request_id} already pending, cannot send command")
114
- self._waiting_queue[request_id] = future
106
+ future: asyncio.Future[RoborockMessage] = await self._pending_rpcs.start(request_id)
115
107
 
116
108
  try:
117
109
  encoded_msg = self._encoder(message)
@@ -120,13 +112,11 @@ class MqttChannel(Channel):
120
112
  return await asyncio.wait_for(future, timeout=timeout)
121
113
 
122
114
  except asyncio.TimeoutError as ex:
123
- async with self._queue_lock:
124
- self._waiting_queue.pop(request_id, None)
115
+ await self._pending_rpcs.pop(request_id)
125
116
  raise RoborockException(f"Command timed out after {timeout}s") from ex
126
117
  except Exception:
127
118
  logging.exception("Uncaught error sending command")
128
- async with self._queue_lock:
129
- self._waiting_queue.pop(request_id, None)
119
+ await self._pending_rpcs.pop(request_id)
130
120
  raise
131
121
 
132
122
 
@@ -0,0 +1,45 @@
1
+ """Module for managing pending RPCs."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Generic, TypeVar
6
+
7
+ from roborock.exceptions import RoborockException
8
+
9
+ _LOGGER = logging.getLogger(__name__)
10
+
11
+
12
+ K = TypeVar("K")
13
+ V = TypeVar("V")
14
+
15
+
16
+ class PendingRpcs(Generic[K, V]):
17
+ """Manage pending RPCs."""
18
+
19
+ def __init__(self) -> None:
20
+ """Initialize the pending RPCs."""
21
+ self._queue_lock = asyncio.Lock()
22
+ self._waiting_queue: dict[K, asyncio.Future[V]] = {}
23
+
24
+ async def start(self, key: K) -> asyncio.Future[V]:
25
+ """Start the pending RPCs."""
26
+ future: asyncio.Future[V] = asyncio.Future()
27
+ async with self._queue_lock:
28
+ if key in self._waiting_queue:
29
+ raise RoborockException(f"Request ID {key} already pending, cannot send command")
30
+ self._waiting_queue[key] = future
31
+ return future
32
+
33
+ async def pop(self, key: K) -> None:
34
+ """Pop a pending RPC."""
35
+ async with self._queue_lock:
36
+ if (future := self._waiting_queue.pop(key, None)) is not None:
37
+ future.cancel()
38
+
39
+ async def resolve(self, key: K, value: V) -> None:
40
+ """Resolve waiting future with proper locking."""
41
+ async with self._queue_lock:
42
+ if (future := self._waiting_queue.pop(key, None)) is not None:
43
+ future.set_result(value)
44
+ else:
45
+ _LOGGER.debug("Received unsolicited message: %s", key)
@@ -147,6 +147,26 @@ class Utils:
147
147
  return unpad(decipher.decrypt(ciphertext), AES.block_size)
148
148
  return ciphertext
149
149
 
150
+ @staticmethod
151
+ def encrypt_cbc(plaintext: bytes, token: bytes) -> bytes:
152
+ """Encrypt plaintext with a given token using cbc mode.
153
+
154
+ This is currently used for testing purposes only.
155
+
156
+ :param bytes plaintext: Plaintext (json) to encrypt
157
+ :param bytes token: Token to use
158
+ :return: Encrypted bytes
159
+ """
160
+ if not isinstance(plaintext, bytes):
161
+ raise TypeError("plaintext requires bytes")
162
+ Utils.verify_token(token)
163
+ iv = bytes(AES.block_size)
164
+ cipher = AES.new(token, AES.MODE_CBC, iv)
165
+ if plaintext:
166
+ plaintext = pad(plaintext, AES.block_size)
167
+ return cipher.encrypt(plaintext)
168
+ return plaintext
169
+
150
170
  @staticmethod
151
171
  def decrypt_cbc(ciphertext: bytes, token: bytes) -> bytes:
152
172
  """Decrypt ciphertext with a given token using cbc mode.
@@ -7,6 +7,7 @@ import json
7
7
  import logging
8
8
  import math
9
9
  import secrets
10
+ import struct
10
11
  import time
11
12
  from collections.abc import Callable
12
13
  from dataclasses import dataclass, field
@@ -44,6 +45,10 @@ class SecurityData:
44
45
  """Convert security data to a dictionary for sending in the payload."""
45
46
  return {"security": {"endpoint": self.endpoint, "nonce": self.nonce.hex().lower()}}
46
47
 
48
+ def to_diagnostic_data(self) -> dict[str, Any]:
49
+ """Convert security data to a dictionary for debugging purposes."""
50
+ return {"nonce": self.nonce.hex().lower()}
51
+
47
52
 
48
53
  def create_security_data(rriot: RRiot) -> SecurityData:
49
54
  """Create a SecurityData instance for the given endpoint and nonce."""
@@ -142,3 +147,37 @@ def decode_rpc_response(message: RoborockMessage) -> dict[str, Any]:
142
147
  if not isinstance(result, dict):
143
148
  raise RoborockException(f"Invalid V1 message format: 'result' should be a dictionary for {message.payload!r}")
144
149
  return result
150
+
151
+
152
+ @dataclass
153
+ class MapResponse:
154
+ """Data structure for the V1 Map response."""
155
+
156
+ request_id: int
157
+ """The request ID of the map response."""
158
+
159
+ data: bytes
160
+ """The map data, decrypted and decompressed."""
161
+
162
+
163
+ def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse]:
164
+ """Create a decoder for V1 map response messages."""
165
+
166
+ def _decode_map_response(message: RoborockMessage) -> MapResponse:
167
+ """Decode a V1 map response message."""
168
+ if not message.payload or len(message.payload) < 24:
169
+ raise RoborockException("Invalid V1 map response format: missing payload")
170
+ header, body = message.payload[:24], message.payload[24:]
171
+ [endpoint, _, request_id, _] = struct.unpack("<8s8sH6s", header)
172
+ if not endpoint.decode().startswith(security_data.endpoint):
173
+ raise RoborockException(
174
+ f"Invalid V1 map response endpoint: {endpoint!r}, expected {security_data.endpoint!r}"
175
+ )
176
+ try:
177
+ decrypted = Utils.decrypt_cbc(body, security_data.nonce)
178
+ except ValueError as err:
179
+ raise RoborockException("Failed to decode map message payload") from err
180
+ decompressed = Utils.decompress(decrypted)
181
+ return MapResponse(request_id=request_id, data=decompressed)
182
+
183
+ return _decode_map_response
@@ -1,13 +1,13 @@
1
1
  import asyncio
2
2
  import dataclasses
3
3
  import json
4
- import struct
5
4
  import time
6
5
  from abc import ABC, abstractmethod
7
6
  from collections.abc import Callable, Coroutine
8
7
  from typing import Any, TypeVar, final
9
8
 
10
9
  from roborock import (
10
+ AppInitStatus,
11
11
  DeviceProp,
12
12
  DockSummary,
13
13
  RoborockCommand,
@@ -45,7 +45,7 @@ from roborock.containers import (
45
45
  ValleyElectricityTimer,
46
46
  WashTowelMode,
47
47
  )
48
- from roborock.protocol import Utils
48
+ from roborock.protocols.v1_protocol import MapResponse, SecurityData, create_map_response_decoder
49
49
  from roborock.roborock_message import (
50
50
  ROBOROCK_DATA_CONSUMABLE_PROTOCOL,
51
51
  ROBOROCK_DATA_STATUS_PROTOCOL,
@@ -150,10 +150,15 @@ class RoborockClientV1(RoborockClient, ABC):
150
150
  """Roborock client base class for version 1 devices."""
151
151
 
152
152
  _listeners: dict[str, ListenerModel] = {}
153
+ _map_response_decoder: Callable[[RoborockMessage], MapResponse] | None = None
153
154
 
154
- def __init__(self, device_info: DeviceData, endpoint: str):
155
+ def __init__(self, device_info: DeviceData, security_data: SecurityData | None) -> None:
155
156
  """Initializes the Roborock client."""
156
157
  super().__init__(device_info)
158
+ if security_data is not None:
159
+ self._diagnostic_data.update({"misc_info": security_data.to_diagnostic_data()})
160
+ self._map_response_decoder = create_map_response_decoder(security_data)
161
+
157
162
  self._status_type: type[Status] = ModelStatus.get(device_info.model, S7MaxVStatus)
158
163
  self.cache: dict[CacheableAttribute, AttributeCache] = {
159
164
  cacheable_attribute: AttributeCache(attr, self._send_command)
@@ -162,7 +167,6 @@ class RoborockClientV1(RoborockClient, ABC):
162
167
  if device_info.device.duid not in self._listeners:
163
168
  self._listeners[device_info.device.duid] = ListenerModel({}, self.cache)
164
169
  self.listener_model = self._listeners[device_info.device.duid]
165
- self._endpoint = endpoint
166
170
 
167
171
  async def async_release(self) -> None:
168
172
  await super().async_release()
@@ -339,6 +343,10 @@ class RoborockClientV1(RoborockClient, ABC):
339
343
  """Load the map into the vacuum's memory."""
340
344
  await self.send_command(RoborockCommand.LOAD_MULTI_MAP, [map_flag])
341
345
 
346
+ async def get_app_init_status(self) -> AppInitStatus:
347
+ """Gets the app init status (needed for determining vacuum capabilities)."""
348
+ return await self.send_command(RoborockCommand.APP_GET_INIT_STATUS, return_type=AppInitStatus)
349
+
342
350
  @abstractmethod
343
351
  async def _send_command(
344
352
  self,
@@ -429,21 +437,15 @@ class RoborockClientV1(RoborockClient, ABC):
429
437
  dps = {data_point_number: data_point}
430
438
  self._logger.debug(f"Got unknown data point {dps}")
431
439
  elif data.payload and protocol == RoborockMessageProtocol.MAP_RESPONSE:
432
- payload = data.payload[0:24]
433
- [endpoint, _, request_id, _] = struct.unpack("<8s8sH6s", payload)
434
- if endpoint.decode().startswith(self._endpoint):
435
- try:
436
- decrypted = Utils.decrypt_cbc(data.payload[24:], self._nonce)
437
- except ValueError as err:
438
- raise RoborockException(f"Failed to decode {data.payload!r} for {data.protocol}") from err
439
- decompressed = Utils.decompress(decrypted)
440
- queue = self._waiting_queue.get(request_id)
440
+ if self._map_response_decoder is not None:
441
+ map_response = self._map_response_decoder(data)
442
+ queue = self._waiting_queue.get(map_response.request_id)
441
443
  if queue:
442
- if isinstance(decompressed, list):
443
- decompressed = decompressed[0]
444
- queue.set_result(decompressed)
444
+ queue.set_result(map_response.data)
445
445
  else:
446
- self._logger.debug("Received response for unknown request id %s", request_id)
446
+ self._logger.debug(
447
+ "Received unsolicited map response for request_id %s", map_response.request_id
448
+ )
447
449
  else:
448
450
  queue = self._waiting_queue.get(data.seq)
449
451
  if queue:
@@ -60,7 +60,7 @@ class RoborockLocalClientV1(RoborockClientV1, RoborockClient):
60
60
  self.transport: Transport | None = None
61
61
  self._mutex = Lock()
62
62
  self.keep_alive_task: TimerHandle | None = None
63
- RoborockClientV1.__init__(self, device_data, "abc")
63
+ RoborockClientV1.__init__(self, device_data, security_data=None)
64
64
  RoborockClient.__init__(self, device_data)
65
65
  self._local_protocol = _LocalProtocol(self._data_received, self._connection_lost)
66
66
  self._encoder: Encoder = create_local_encoder(device_data.device.local_key)
@@ -1,4 +1,3 @@
1
- import base64
2
1
  import logging
3
2
 
4
3
  from vacuum_map_parser_base.config.color import ColorsPalette
@@ -10,8 +9,7 @@ from roborock.cloud_api import RoborockMqttClient
10
9
 
11
10
  from ..containers import DeviceData, UserData
12
11
  from ..exceptions import CommandVacuumError, RoborockException, VacuumError
13
- from ..protocol import Utils
14
- from ..protocols.v1_protocol import SecurityData, create_mqtt_payload_encoder
12
+ from ..protocols.v1_protocol import create_mqtt_payload_encoder, create_security_data
15
13
  from ..roborock_message import (
16
14
  RoborockMessageProtocol,
17
15
  )
@@ -30,15 +28,12 @@ class RoborockMqttClientV1(RoborockMqttClient, RoborockClientV1):
30
28
  rriot = user_data.rriot
31
29
  if rriot is None:
32
30
  raise RoborockException("Got no rriot data from user_data")
33
- endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode()
34
-
31
+ security_data = create_security_data(rriot)
35
32
  RoborockMqttClient.__init__(self, user_data, device_info)
36
- RoborockClientV1.__init__(self, device_info, endpoint)
33
+ RoborockClientV1.__init__(self, device_info, security_data=security_data)
37
34
  self.queue_timeout = queue_timeout
38
35
  self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
39
- self._payload_encoder = create_mqtt_payload_encoder(
40
- SecurityData(endpoint=self._endpoint, nonce=self._nonce),
41
- )
36
+ self._payload_encoder = create_mqtt_payload_encoder(security_data)
42
37
 
43
38
  async def _send_command(
44
39
  self,