python-roborock 2.9.2__tar.gz → 2.9.4__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 (28) hide show
  1. {python_roborock-2.9.2 → python_roborock-2.9.4}/PKG-INFO +2 -2
  2. {python_roborock-2.9.2 → python_roborock-2.9.4}/pyproject.toml +6 -2
  3. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/api.py +0 -9
  4. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/cloud_api.py +5 -13
  5. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/roborock_message.py +4 -4
  6. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/version_1_apis/roborock_client_v1.py +21 -9
  7. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/version_a01_apis/roborock_client_a01.py +3 -1
  8. {python_roborock-2.9.2 → python_roborock-2.9.4}/LICENSE +0 -0
  9. {python_roborock-2.9.2 → python_roborock-2.9.4}/README.md +0 -0
  10. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/__init__.py +0 -0
  11. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/cli.py +0 -0
  12. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/code_mappings.py +0 -0
  13. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/command_cache.py +0 -0
  14. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/const.py +0 -0
  15. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/containers.py +0 -0
  16. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/exceptions.py +0 -0
  17. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/local_api.py +0 -0
  18. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/protocol.py +0 -0
  19. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/py.typed +0 -0
  20. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/roborock_future.py +0 -0
  21. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/roborock_typing.py +0 -0
  22. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/util.py +0 -0
  23. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/version_1_apis/__init__.py +0 -0
  24. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  25. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  26. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/version_a01_apis/__init__.py +0 -0
  27. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  28. {python_roborock-2.9.2 → python_roborock-2.9.4}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-roborock
3
- Version: 2.9.2
3
+ Version: 2.9.4
4
4
  Summary: A package to control Roborock vacuums.
5
5
  License: GPL-3.0-only
6
6
  Keywords: roborock,vacuum,homeassistant
@@ -21,7 +21,7 @@ Requires-Dist: aiohttp (>=3.8.2,<4.0.0)
21
21
  Requires-Dist: async-timeout
22
22
  Requires-Dist: click (>=8)
23
23
  Requires-Dist: construct (>=2.10.57,<3.0.0)
24
- Requires-Dist: paho-mqtt (>=1.6.1,<2.0.0)
24
+ Requires-Dist: paho-mqtt (>=1.6.1,<3.0.0)
25
25
  Requires-Dist: pycryptodome (>=3.18,<4.0)
26
26
  Requires-Dist: pycryptodomex (>=3.18,<4.0) ; sys_platform == "darwin"
27
27
  Requires-Dist: vacuum-map-parser-roborock
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.9.2"
3
+ version = "2.9.4"
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"
@@ -27,7 +27,7 @@ aiohttp = "^3.8.2"
27
27
  async-timeout = "*"
28
28
  pycryptodome = "^3.18"
29
29
  pycryptodomex = {version = "^3.18", markers = "sys_platform == 'darwin'"}
30
- paho-mqtt = "^1.6.1"
30
+ paho-mqtt = ">=1.6.1,<3.0.0"
31
31
  construct = "^2.10.57"
32
32
  vacuum-map-parser-roborock = "*"
33
33
 
@@ -45,6 +45,8 @@ ruff = "*"
45
45
  codespell = "*"
46
46
  pyshark = "^0.6"
47
47
  aioresponses = "^0.7.7"
48
+ freezegun = "^1.5.1"
49
+ pytest-timeout = "^2.3.1"
48
50
 
49
51
  [tool.semantic_release]
50
52
  branch = "main"
@@ -70,3 +72,5 @@ select=["E", "F", "UP", "I"]
70
72
 
71
73
  [tool.pytest.ini_options]
72
74
  asyncio_mode = "auto"
75
+ asyncio_default_fixture_loop_scope = "function"
76
+ timeout = 20
@@ -21,7 +21,6 @@ from .roborock_future import RoborockFuture
21
21
  from .roborock_message import (
22
22
  RoborockMessage,
23
23
  )
24
- from .roborock_typing import RoborockCommand
25
24
  from .util import get_next_int, get_running_loop_or_create_one
26
25
 
27
26
  _LOGGER = logging.getLogger(__name__)
@@ -124,11 +123,3 @@ class RoborockClient(ABC):
124
123
  @abstractmethod
125
124
  async def send_message(self, roborock_message: RoborockMessage):
126
125
  """Send a message to the Roborock device."""
127
-
128
- @abstractmethod
129
- async def _send_command(
130
- self,
131
- method: RoborockCommand | str,
132
- params: list | dict | int | None = None,
133
- ):
134
- """Send a command to the Roborock device."""
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import threading
5
- import uuid
6
5
  from abc import ABC
7
6
  from asyncio import Lock
8
7
  from typing import Any
@@ -29,16 +28,10 @@ class _Mqtt(mqtt.Client):
29
28
  """
30
29
 
31
30
  _thread: threading.Thread
32
- _client_id: str
33
31
 
34
32
  def __init__(self) -> None:
35
33
  """Initialize the MQTT client."""
36
34
  super().__init__(protocol=mqtt.MQTTv5)
37
- self.reset_client_id()
38
-
39
- def reset_client_id(self):
40
- """Generate a new client id to make a new session when reconnecting."""
41
- self._client_id = mqtt.base62(uuid.uuid4().int, padding=22)
42
35
 
43
36
  def maybe_restart_loop(self) -> None:
44
37
  """Ensure that the MQTT loop is running in case it previously exited."""
@@ -89,6 +82,8 @@ class RoborockMqttClient(RoborockClient, ABC):
89
82
  self._logger.error(message)
90
83
  if connection_queue:
91
84
  connection_queue.set_exception(VacuumError(message))
85
+ else:
86
+ self._logger.debug("Failed to notify connect future, not in queue")
92
87
  return
93
88
  self._logger.info(f"Connected to mqtt {self._mqtt_host}:{self._mqtt_port}")
94
89
  topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}"
@@ -116,8 +111,6 @@ class RoborockMqttClient(RoborockClient, ABC):
116
111
  try:
117
112
  exc = RoborockException(mqtt.error_string(rc)) if rc != mqtt.MQTT_ERR_SUCCESS else None
118
113
  super().on_connection_lost(exc)
119
- if rc == mqtt.MQTT_ERR_PROTOCOL:
120
- self._mqtt_client.reset_client_id()
121
114
  connection_queue = self._waiting_queue.get(DISCONNECT_REQUEST_ID)
122
115
  if connection_queue:
123
116
  connection_queue.set_result(True)
@@ -163,10 +156,9 @@ class RoborockMqttClient(RoborockClient, ABC):
163
156
  async def async_disconnect(self) -> None:
164
157
  async with self._mutex:
165
158
  if disconnected_future := self.sync_disconnect():
166
- try:
167
- await disconnected_future
168
- except VacuumError as err:
169
- raise RoborockException(err) from err
159
+ # There are no errors set on this future
160
+ await disconnected_future
161
+ await self.event_loop.run_in_executor(None, self._mqtt_client.loop_stop)
170
162
 
171
163
  async def async_connect(self) -> None:
172
164
  async with self._mutex:
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import math
5
5
  import time
6
- from dataclasses import dataclass
6
+ from dataclasses import dataclass, field
7
7
 
8
8
  from roborock import RoborockEnum
9
9
  from roborock.util import get_next_int
@@ -155,10 +155,10 @@ class MessageRetry:
155
155
  class RoborockMessage:
156
156
  protocol: RoborockMessageProtocol
157
157
  payload: bytes | None = None
158
- seq: int = get_next_int(100000, 999999)
158
+ seq: int = field(default_factory=lambda: get_next_int(100000, 999999))
159
159
  version: bytes = b"1.0"
160
- random: int = get_next_int(10000, 99999)
161
- timestamp: int = math.floor(time.time())
160
+ random: int = field(default_factory=lambda: get_next_int(10000, 99999))
161
+ timestamp: int = field(default_factory=lambda: math.floor(time.time()))
162
162
  message_retry: MessageRetry | None = None
163
163
 
164
164
  def get_request_id(self) -> int | None:
@@ -4,7 +4,7 @@ import json
4
4
  import math
5
5
  import struct
6
6
  import time
7
- from abc import ABC
7
+ from abc import ABC, abstractmethod
8
8
  from collections.abc import Callable, Coroutine
9
9
  from typing import Any, TypeVar, final
10
10
 
@@ -78,12 +78,15 @@ RT = TypeVar("RT", bound=RoborockBase)
78
78
  EVICT_TIME = 60
79
79
 
80
80
 
81
+ _SendCommandT = Callable[[RoborockCommand | str, list | dict | int | None], Any]
82
+
83
+
81
84
  class AttributeCache:
82
- def __init__(self, attribute: RoborockAttribute, api: RoborockClient):
85
+ def __init__(self, attribute: RoborockAttribute, loop: asyncio.AbstractEventLoop, send_command: _SendCommandT):
83
86
  self.attribute = attribute
84
- self.api = api
87
+ self._send_command = send_command
85
88
  self.attribute = attribute
86
- self.task = RepeatableTask(self.api.event_loop, self._async_value, EVICT_TIME)
89
+ self.task = RepeatableTask(loop, self._async_value, EVICT_TIME)
87
90
  self._value: Any = None
88
91
  self._mutex = asyncio.Lock()
89
92
  self.unsupported: bool = False
@@ -96,7 +99,7 @@ class AttributeCache:
96
99
  if self.unsupported:
97
100
  return None
98
101
  try:
99
- self._value = await self.api._send_command(self.attribute.get_command)
102
+ self._value = await self._send_command(self.attribute.get_command, None)
100
103
  except UnknownMethodError as err:
101
104
  # Limit the amount of times we call unsupported methods
102
105
  self.unsupported = True
@@ -115,21 +118,21 @@ class AttributeCache:
115
118
  async def update_value(self, params) -> None:
116
119
  if self.attribute.set_command is None:
117
120
  raise RoborockException(f"{self.attribute.attribute} have no set command")
118
- response = await self.api._send_command(self.attribute.set_command, params)
121
+ response = await self._send_command(self.attribute.set_command, params)
119
122
  await self._async_value()
120
123
  return response
121
124
 
122
125
  async def add_value(self, params):
123
126
  if self.attribute.add_command is None:
124
127
  raise RoborockException(f"{self.attribute.attribute} have no add command")
125
- response = await self.api._send_command(self.attribute.add_command, params)
128
+ response = await self._send_command(self.attribute.add_command, params)
126
129
  await self._async_value()
127
130
  return response
128
131
 
129
132
  async def close_value(self, params=None) -> None:
130
133
  if self.attribute.close_command is None:
131
134
  raise RoborockException(f"{self.attribute.attribute} have no close command")
132
- response = await self.api._send_command(self.attribute.close_command, params)
135
+ response = await self._send_command(self.attribute.close_command, params)
133
136
  await self._async_value()
134
137
  return response
135
138
 
@@ -153,7 +156,8 @@ class RoborockClientV1(RoborockClient, ABC):
153
156
  super().__init__(device_info)
154
157
  self._status_type: type[Status] = ModelStatus.get(device_info.model, S7MaxVStatus)
155
158
  self.cache: dict[CacheableAttribute, AttributeCache] = {
156
- cacheable_attribute: AttributeCache(attr, self) for cacheable_attribute, attr in get_cache_map().items()
159
+ cacheable_attribute: AttributeCache(attr, self.event_loop, self._send_command)
160
+ for cacheable_attribute, attr in get_cache_map().items()
157
161
  }
158
162
  if device_info.device.duid not in self._listeners:
159
163
  self._listeners[device_info.device.duid] = ListenerModel({}, self.cache)
@@ -364,6 +368,14 @@ class RoborockClientV1(RoborockClient, ABC):
364
368
  )
365
369
  return request_id, timestamp, payload
366
370
 
371
+ @abstractmethod
372
+ async def _send_command(
373
+ self,
374
+ method: RoborockCommand | str,
375
+ params: list | dict | int | None = None,
376
+ ) -> Any:
377
+ """Send a command to the Roborock device."""
378
+
367
379
  def on_message_received(self, messages: list[RoborockMessage]) -> None:
368
380
  try:
369
381
  self._last_device_msg_in = time.monotonic()
@@ -123,11 +123,13 @@ class RoborockClientA01(RoborockClient, ABC):
123
123
  payload = message.payload
124
124
  try:
125
125
  payload = unpad(payload, AES.block_size)
126
- except Exception:
126
+ except Exception as err:
127
+ self._logger.debug("Failed to unpad payload: %s", err)
127
128
  continue
128
129
  payload_json = json.loads(payload.decode())
129
130
  for data_point_number, data_point in payload_json.get("dps").items():
130
131
  data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol
132
+ self._logger.debug("received msg with dps, protocol: %s, %s", data_point_number, protocol)
131
133
  entries: dict
132
134
  if self.category == RoborockCategory.WET_DRY_VAC:
133
135
  data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
File without changes