python-roborock 3.2.0__tar.gz → 3.3.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 (93) hide show
  1. {python_roborock-3.2.0 → python_roborock-3.3.0}/PKG-INFO +1 -1
  2. {python_roborock-3.2.0 → python_roborock-3.3.0}/pyproject.toml +1 -1
  3. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/v1/v1_containers.py +1 -1
  4. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/local_channel.py +111 -6
  5. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/mqtt/roborock_session.py +1 -0
  6. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/protocols/v1_protocol.py +8 -0
  7. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/version_1_apis/roborock_local_client_v1.py +1 -9
  8. {python_roborock-3.2.0 → python_roborock-3.3.0}/.gitignore +0 -0
  9. {python_roborock-3.2.0 → python_roborock-3.3.0}/LICENSE +0 -0
  10. {python_roborock-3.2.0 → python_roborock-3.3.0}/README.md +0 -0
  11. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/__init__.py +0 -0
  12. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/api.py +0 -0
  13. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/broadcast_protocol.py +0 -0
  14. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/callbacks.py +0 -0
  15. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/cli.py +0 -0
  16. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/cloud_api.py +0 -0
  17. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/command_cache.py +0 -0
  18. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/const.py +0 -0
  19. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/__init__.py +0 -0
  20. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/b01_q10/__init__.py +0 -0
  21. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  22. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  23. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/b01_q7/__init__.py +0 -0
  24. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  25. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  26. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/code_mappings.py +0 -0
  27. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/containers.py +0 -0
  28. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/dyad/__init__.py +0 -0
  29. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  30. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/dyad/dyad_containers.py +0 -0
  31. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/v1/__init__.py +0 -0
  32. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  33. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  34. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/zeo/__init__.py +0 -0
  35. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  36. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/data/zeo/zeo_containers.py +0 -0
  37. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/device_features.py +0 -0
  38. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/README.md +0 -0
  39. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/__init__.py +0 -0
  40. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/a01_channel.py +0 -0
  41. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/b01_channel.py +0 -0
  42. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/cache.py +0 -0
  43. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/channel.py +0 -0
  44. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/device.py +0 -0
  45. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/device_manager.py +0 -0
  46. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/mqtt_channel.py +0 -0
  47. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/__init__.py +0 -0
  48. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/a01/__init__.py +0 -0
  49. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/b01/__init__.py +0 -0
  50. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/traits_mixin.py +0 -0
  51. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/__init__.py +0 -0
  52. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  53. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  54. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/command.py +0 -0
  55. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/common.py +0 -0
  56. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  57. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/device_features.py +0 -0
  58. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  59. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  60. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  61. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/home.py +0 -0
  62. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/led_status.py +0 -0
  63. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/map_content.py +0 -0
  64. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/maps.py +0 -0
  65. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/network_info.py +0 -0
  66. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/rooms.py +0 -0
  67. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  68. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/status.py +0 -0
  69. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  70. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/volume.py +0 -0
  71. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  72. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/v1_channel.py +0 -0
  73. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/devices/v1_rpc_channel.py +0 -0
  74. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/exceptions.py +0 -0
  75. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/map/__init__.py +0 -0
  76. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/map/map_parser.py +0 -0
  77. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/mqtt/__init__.py +0 -0
  78. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/mqtt/session.py +0 -0
  79. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/protocol.py +0 -0
  80. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/protocols/a01_protocol.py +0 -0
  81. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/protocols/b01_protocol.py +0 -0
  82. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/py.typed +0 -0
  83. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/roborock_future.py +0 -0
  84. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/roborock_message.py +0 -0
  85. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/roborock_typing.py +0 -0
  86. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/util.py +0 -0
  87. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/version_1_apis/__init__.py +0 -0
  88. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  89. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  90. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/version_a01_apis/__init__.py +0 -0
  91. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  92. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  93. {python_roborock-3.2.0 → python_roborock-3.3.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
6
6
  Project-URL: Documentation, https://python-roborock.readthedocs.io/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "3.2.0"
3
+ version = "3.3.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
6
6
  requires-python = ">=3.11, <4"
@@ -584,7 +584,7 @@ class AppInitStatus(RoborockBase):
584
584
  new_feature_info_str: str
585
585
  new_feature_info_2: int | None = None
586
586
  carriage_type: int | None = None
587
- dsp_version: int | None = None
587
+ dsp_version: str | None = None
588
588
 
589
589
 
590
590
  @dataclass
@@ -7,13 +7,25 @@ from dataclasses import dataclass
7
7
 
8
8
  from roborock.callbacks import CallbackList, decoder_callback
9
9
  from roborock.exceptions import RoborockConnectionException, RoborockException
10
- from roborock.protocol import Decoder, Encoder, create_local_decoder, create_local_encoder
11
- from roborock.roborock_message import RoborockMessage
10
+ from roborock.protocol import create_local_decoder, create_local_encoder
11
+ from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
12
12
 
13
+ from ..protocols.v1_protocol import LocalProtocolVersion
14
+ from ..util import get_next_int
13
15
  from .channel import Channel
14
16
 
15
17
  _LOGGER = logging.getLogger(__name__)
16
18
  _PORT = 58867
19
+ _TIMEOUT = 5.0
20
+
21
+
22
+ @dataclass
23
+ class LocalChannelParams:
24
+ """Parameters for local channel encoder/decoder."""
25
+
26
+ local_key: str
27
+ connect_nonce: int
28
+ ack_nonce: int | None
17
29
 
18
30
 
19
31
  @dataclass
@@ -45,12 +57,74 @@ class LocalChannel(Channel):
45
57
  self._protocol: _LocalProtocol | None = None
46
58
  self._subscribers: CallbackList[RoborockMessage] = CallbackList(_LOGGER)
47
59
  self._is_connected = False
48
-
49
- self._decoder: Decoder = create_local_decoder(local_key)
50
- self._encoder: Encoder = create_local_encoder(local_key)
60
+ self._local_protocol_version: LocalProtocolVersion | None = None
61
+ self._update_encoder_decoder(
62
+ LocalChannelParams(local_key=local_key, connect_nonce=get_next_int(10000, 32767), ack_nonce=None)
63
+ )
64
+
65
+ def _update_encoder_decoder(self, params: LocalChannelParams):
66
+ self._params = params
67
+ self._encoder = create_local_encoder(
68
+ local_key=params.local_key, connect_nonce=params.connect_nonce, ack_nonce=params.ack_nonce
69
+ )
70
+ self._decoder = create_local_decoder(
71
+ local_key=params.local_key, connect_nonce=params.connect_nonce, ack_nonce=params.ack_nonce
72
+ )
51
73
  # Callback to decode messages and dispatch to subscribers
52
74
  self._data_received: Callable[[bytes], None] = decoder_callback(self._decoder, self._subscribers, _LOGGER)
53
75
 
76
+ async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> LocalChannelParams | None:
77
+ """Perform the initial handshaking and return encoder params if successful."""
78
+ _LOGGER.debug(
79
+ "Attempting to use the %s protocol for client %s...",
80
+ local_protocol_version,
81
+ self._host,
82
+ )
83
+ request = RoborockMessage(
84
+ protocol=RoborockMessageProtocol.HELLO_REQUEST,
85
+ version=local_protocol_version.encode(),
86
+ random=self._params.connect_nonce,
87
+ seq=1,
88
+ )
89
+ try:
90
+ response = await self._send_message(
91
+ roborock_message=request,
92
+ request_id=request.seq,
93
+ response_protocol=RoborockMessageProtocol.HELLO_RESPONSE,
94
+ )
95
+ _LOGGER.debug(
96
+ "Client %s speaks the %s protocol.",
97
+ self._host,
98
+ local_protocol_version,
99
+ )
100
+ return LocalChannelParams(
101
+ local_key=self._params.local_key, connect_nonce=self._params.connect_nonce, ack_nonce=response.random
102
+ )
103
+ except RoborockException as e:
104
+ _LOGGER.debug(
105
+ "Client %s did not respond or does not speak the %s protocol. %s",
106
+ self._host,
107
+ local_protocol_version,
108
+ e,
109
+ )
110
+ return None
111
+
112
+ async def _hello(self):
113
+ """Send hello to the device to negotiate protocol."""
114
+ attempt_versions = [LocalProtocolVersion.V1, LocalProtocolVersion.L01]
115
+ if self._local_protocol_version:
116
+ # Sort to try the preferred version first
117
+ attempt_versions.sort(key=lambda v: v != self._local_protocol_version)
118
+
119
+ for version in attempt_versions:
120
+ params = await self._do_hello(version)
121
+ if params is not None:
122
+ self._local_protocol_version = version
123
+ self._update_encoder_decoder(params)
124
+ return
125
+
126
+ raise RoborockException("Failed to connect to device with any known protocol")
127
+
54
128
  @property
55
129
  def is_connected(self) -> bool:
56
130
  """Check if the channel is currently connected."""
@@ -62,7 +136,7 @@ class LocalChannel(Channel):
62
136
  return self._is_connected
63
137
 
64
138
  async def connect(self) -> None:
65
- """Connect to the device."""
139
+ """Connect to the device and negotiate protocol."""
66
140
  if self._is_connected:
67
141
  _LOGGER.warning("Already connected")
68
142
  return
@@ -75,6 +149,14 @@ class LocalChannel(Channel):
75
149
  except OSError as e:
76
150
  raise RoborockConnectionException(f"Failed to connect to {self._host}:{_PORT}") from e
77
151
 
152
+ # Perform protocol negotiation
153
+ try:
154
+ await self._hello()
155
+ except RoborockException:
156
+ # If protocol negotiation fails, clean up the connection state
157
+ self.close()
158
+ raise
159
+
78
160
  def close(self) -> None:
79
161
  """Disconnect from the device."""
80
162
  if self._transport:
@@ -113,6 +195,29 @@ class LocalChannel(Channel):
113
195
  logging.exception("Uncaught error sending command")
114
196
  raise RoborockException(f"Failed to send message: {message}") from err
115
197
 
198
+ async def _send_message(
199
+ self,
200
+ roborock_message: RoborockMessage,
201
+ request_id: int,
202
+ response_protocol: int,
203
+ ) -> RoborockMessage:
204
+ """Send a raw message and wait for a raw response."""
205
+ future: asyncio.Future[RoborockMessage] = asyncio.Future()
206
+
207
+ def find_response(response_message: RoborockMessage) -> None:
208
+ if response_message.protocol == response_protocol and response_message.seq == request_id:
209
+ future.set_result(response_message)
210
+
211
+ unsub = await self.subscribe(find_response)
212
+ try:
213
+ await self.publish(roborock_message)
214
+ return await asyncio.wait_for(future, timeout=_TIMEOUT)
215
+ except TimeoutError as ex:
216
+ future.cancel()
217
+ raise RoborockException(f"Command timed out after {_TIMEOUT}s") from ex
218
+ finally:
219
+ unsub()
220
+
116
221
 
117
222
  # This module provides a factory function to create LocalChannel instances.
118
223
  #
@@ -106,6 +106,7 @@ class RoborockMqttSession(MqttSession):
106
106
  # Reset backoff once we've successfully connected
107
107
  self._backoff = MIN_BACKOFF_INTERVAL
108
108
  self._healthy = True
109
+ _LOGGER.info("MQTT Session connected.")
109
110
  if start_future:
110
111
  start_future.set_result(None)
111
112
  start_future = None
@@ -11,6 +11,7 @@ import struct
11
11
  import time
12
12
  from collections.abc import Callable
13
13
  from dataclasses import dataclass, field
14
+ from enum import StrEnum
14
15
  from typing import Any
15
16
 
16
17
  from roborock.data import RRiot
@@ -32,6 +33,13 @@ CommandType = RoborockCommand | str
32
33
  ParamsType = list | dict | int | None
33
34
 
34
35
 
36
+ class LocalProtocolVersion(StrEnum):
37
+ """Supported local protocol versions. Different from vacuum protocol versions."""
38
+
39
+ L01 = "L01"
40
+ V1 = "1.0"
41
+
42
+
35
43
  @dataclass(frozen=True, kw_only=True)
36
44
  class SecurityData:
37
45
  """Security data included in the request for some V1 commands."""
@@ -3,13 +3,12 @@ import logging
3
3
  from asyncio import Lock, TimerHandle, Transport, get_running_loop
4
4
  from collections.abc import Callable
5
5
  from dataclasses import dataclass
6
- from enum import StrEnum
7
6
 
8
7
  from .. import CommandVacuumError, DeviceData, RoborockCommand
9
8
  from ..api import RoborockClient
10
9
  from ..exceptions import RoborockConnectionException, RoborockException, VacuumError
11
10
  from ..protocol import create_local_decoder, create_local_encoder
12
- from ..protocols.v1_protocol import RequestMessage
11
+ from ..protocols.v1_protocol import LocalProtocolVersion, RequestMessage
13
12
  from ..roborock_message import RoborockMessage, RoborockMessageProtocol
14
13
  from ..util import RoborockLoggerAdapter, get_next_int
15
14
  from .roborock_client_v1 import CLOUD_REQUIRED, RoborockClientV1
@@ -17,13 +16,6 @@ from .roborock_client_v1 import CLOUD_REQUIRED, RoborockClientV1
17
16
  _LOGGER = logging.getLogger(__name__)
18
17
 
19
18
 
20
- class LocalProtocolVersion(StrEnum):
21
- """Supported local protocol versions. Different from vacuum protocol versions."""
22
-
23
- L01 = "L01"
24
- V1 = "1.0"
25
-
26
-
27
19
  @dataclass
28
20
  class _LocalProtocol(asyncio.Protocol):
29
21
  """Callbacks for the Roborock local client transport."""
File without changes