python-roborock 3.8.3__tar.gz → 3.8.5__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.
- {python_roborock-3.8.3 → python_roborock-3.8.5}/PKG-INFO +1 -1
- {python_roborock-3.8.3 → python_roborock-3.8.5}/pyproject.toml +1 -1
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/cache.py +7 -1
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/mqtt_channel.py +1 -1
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/__init__.py +1 -1
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/common.py +1 -1
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/home.py +28 -6
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/v1_channel.py +178 -37
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/protocols/v1_protocol.py +35 -2
- python_roborock-3.8.3/roborock/devices/v1_rpc_channel.py +0 -221
- {python_roborock-3.8.3 → python_roborock-3.8.5}/.gitignore +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/LICENSE +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/README.md +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/api.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/callbacks.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/cli.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/cloud_api.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/command_cache.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/const.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/code_mappings.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/containers.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/device_features.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/README.md +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/a01_channel.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/b01_channel.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/channel.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/device.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/device_manager.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/file_cache.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/local_channel.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/exceptions.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/map/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/map/map_parser.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/mqtt/session.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/protocol.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/protocols/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/protocols/b01_protocol.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/py.typed +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/roborock_future.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/roborock_message.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/roborock_typing.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/util.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.5
|
|
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.8.
|
|
3
|
+
version = "3.8.5"
|
|
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"
|
|
@@ -26,7 +26,13 @@ class CacheData:
|
|
|
26
26
|
"""Home map information indexed by map_flag."""
|
|
27
27
|
|
|
28
28
|
home_map_content: dict[int, bytes] = field(default_factory=dict)
|
|
29
|
-
"""Home cache content for each map data indexed by map_flag.
|
|
29
|
+
"""Home cache content for each map data indexed by map_flag.
|
|
30
|
+
|
|
31
|
+
This is deprecated in favor of `home_map_content_base64`.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
home_map_content_base64: dict[int, str] = field(default_factory=dict)
|
|
35
|
+
"""Home cache content for each map data (encoded base64) indexed by map_flag."""
|
|
30
36
|
|
|
31
37
|
device_features: DeviceFeatures | None = None
|
|
32
38
|
"""Device features information."""
|
|
@@ -90,5 +90,5 @@ class MqttChannel(Channel):
|
|
|
90
90
|
def create_mqtt_channel(
|
|
91
91
|
user_data: UserData, mqtt_params: MqttParams, mqtt_session: MqttSession, device: HomeDataDevice
|
|
92
92
|
) -> MqttChannel:
|
|
93
|
-
"""Create a
|
|
93
|
+
"""Create a MQTT channel for the given device."""
|
|
94
94
|
return MqttChannel(mqtt_session, device.duid, device.local_key, user_data.rriot, mqtt_params)
|
|
@@ -38,8 +38,8 @@ from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase
|
|
|
38
38
|
from roborock.data.v1.v1_code_mappings import RoborockDockTypeCode
|
|
39
39
|
from roborock.devices.cache import Cache
|
|
40
40
|
from roborock.devices.traits import Trait
|
|
41
|
-
from roborock.devices.v1_rpc_channel import V1RpcChannel
|
|
42
41
|
from roborock.map.map_parser import MapParserConfig
|
|
42
|
+
from roborock.protocols.v1_protocol import V1RpcChannel
|
|
43
43
|
from roborock.web_api import UserWebApiClient
|
|
44
44
|
|
|
45
45
|
from .child_lock import ChildLockTrait
|
|
@@ -9,7 +9,7 @@ from dataclasses import dataclass, fields
|
|
|
9
9
|
from typing import ClassVar, Self
|
|
10
10
|
|
|
11
11
|
from roborock.data import RoborockBase
|
|
12
|
-
from roborock.
|
|
12
|
+
from roborock.protocols.v1_protocol import V1RpcChannel
|
|
13
13
|
from roborock.roborock_typing import RoborockCommand
|
|
14
14
|
|
|
15
15
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -16,6 +16,7 @@ the current map's information and room names as needed.
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import asyncio
|
|
19
|
+
import base64
|
|
19
20
|
import logging
|
|
20
21
|
from typing import Self
|
|
21
22
|
|
|
@@ -86,14 +87,20 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
|
|
|
86
87
|
After discovery, the home cache will be populated and can be accessed via the `home_map_info` property.
|
|
87
88
|
"""
|
|
88
89
|
cache_data = await self._cache.get()
|
|
89
|
-
if cache_data.home_map_info and cache_data.home_map_content:
|
|
90
|
+
if cache_data.home_map_info and (cache_data.home_map_content or cache_data.home_map_content_base64):
|
|
90
91
|
_LOGGER.debug("Home cache already populated, skipping discovery")
|
|
91
92
|
self._home_map_info = cache_data.home_map_info
|
|
92
93
|
self._discovery_completed = True
|
|
93
94
|
try:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
if cache_data.home_map_content_base64:
|
|
96
|
+
self._home_map_content = {
|
|
97
|
+
k: self._map_content.parse_map_content(base64.b64decode(v))
|
|
98
|
+
for k, v in cache_data.home_map_content_base64.items()
|
|
99
|
+
}
|
|
100
|
+
else:
|
|
101
|
+
self._home_map_content = {
|
|
102
|
+
k: self._map_content.parse_map_content(v) for k, v in cache_data.home_map_content.items()
|
|
103
|
+
}
|
|
97
104
|
except (ValueError, RoborockException) as ex:
|
|
98
105
|
_LOGGER.warning("Failed to parse cached home map content, will re-discover: %s", ex)
|
|
99
106
|
self._home_map_content = {}
|
|
@@ -218,7 +225,12 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
|
|
|
218
225
|
"""Update the entire home cache with new map info and content."""
|
|
219
226
|
cache_data = await self._cache.get()
|
|
220
227
|
cache_data.home_map_info = home_map_info
|
|
221
|
-
cache_data.
|
|
228
|
+
cache_data.home_map_content_base64 = {
|
|
229
|
+
k: base64.b64encode(v.raw_api_response).decode("utf-8")
|
|
230
|
+
for k, v in home_map_content.items()
|
|
231
|
+
if v.raw_api_response
|
|
232
|
+
}
|
|
233
|
+
cache_data.home_map_content = {}
|
|
222
234
|
await self._cache.set(cache_data)
|
|
223
235
|
self._home_map_info = home_map_info
|
|
224
236
|
self._home_map_content = home_map_content
|
|
@@ -237,8 +249,18 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
|
|
|
237
249
|
if update_cache:
|
|
238
250
|
cache_data = await self._cache.get()
|
|
239
251
|
cache_data.home_map_info[map_flag] = map_info
|
|
252
|
+
# Migrate existing cached content to base64 if needed
|
|
253
|
+
if cache_data.home_map_content and not cache_data.home_map_content_base64:
|
|
254
|
+
cache_data.home_map_content_base64 = {
|
|
255
|
+
k: base64.b64encode(v).decode("utf-8") for k, v in cache_data.home_map_content.items()
|
|
256
|
+
}
|
|
257
|
+
cache_data.home_map_content = {}
|
|
240
258
|
if map_content.raw_api_response:
|
|
241
|
-
cache_data.
|
|
259
|
+
if cache_data.home_map_content_base64 is None:
|
|
260
|
+
cache_data.home_map_content_base64 = {}
|
|
261
|
+
cache_data.home_map_content_base64[map_flag] = base64.b64encode(map_content.raw_api_response).decode(
|
|
262
|
+
"utf-8"
|
|
263
|
+
)
|
|
242
264
|
await self._cache.set(cache_data)
|
|
243
265
|
|
|
244
266
|
if self._home_map_info is None:
|
|
@@ -8,37 +8,43 @@ import asyncio
|
|
|
8
8
|
import datetime
|
|
9
9
|
import logging
|
|
10
10
|
from collections.abc import Callable
|
|
11
|
-
from
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, TypeVar
|
|
12
13
|
|
|
13
14
|
from roborock.data import HomeDataDevice, NetworkInfo, RoborockBase, UserData
|
|
14
15
|
from roborock.exceptions import RoborockException
|
|
16
|
+
from roborock.mqtt.health_manager import HealthManager
|
|
15
17
|
from roborock.mqtt.session import MqttParams, MqttSession
|
|
16
18
|
from roborock.protocols.v1_protocol import (
|
|
19
|
+
CommandType,
|
|
20
|
+
MapResponse,
|
|
21
|
+
ParamsType,
|
|
22
|
+
RequestMessage,
|
|
23
|
+
ResponseData,
|
|
24
|
+
ResponseMessage,
|
|
17
25
|
SecurityData,
|
|
26
|
+
V1RpcChannel,
|
|
27
|
+
create_map_response_decoder,
|
|
18
28
|
create_security_data,
|
|
29
|
+
decode_rpc_response,
|
|
19
30
|
)
|
|
20
|
-
from roborock.roborock_message import RoborockMessage
|
|
31
|
+
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
21
32
|
from roborock.roborock_typing import RoborockCommand
|
|
22
33
|
|
|
23
34
|
from .cache import Cache
|
|
24
35
|
from .channel import Channel
|
|
25
36
|
from .local_channel import LocalChannel, LocalSession, create_local_session
|
|
26
37
|
from .mqtt_channel import MqttChannel
|
|
27
|
-
from .v1_rpc_channel import (
|
|
28
|
-
PickFirstAvailable,
|
|
29
|
-
V1RpcChannel,
|
|
30
|
-
create_local_rpc_channel,
|
|
31
|
-
create_map_rpc_channel,
|
|
32
|
-
create_mqtt_rpc_channel,
|
|
33
|
-
)
|
|
34
38
|
|
|
35
39
|
_LOGGER = logging.getLogger(__name__)
|
|
36
40
|
|
|
37
41
|
__all__ = [
|
|
38
|
-
"
|
|
42
|
+
"create_v1_channel",
|
|
39
43
|
]
|
|
40
44
|
|
|
41
45
|
_T = TypeVar("_T", bound=RoborockBase)
|
|
46
|
+
_TIMEOUT = 10.0
|
|
47
|
+
|
|
42
48
|
|
|
43
49
|
# Exponential backoff parameters for reconnecting to local
|
|
44
50
|
MIN_RECONNECT_INTERVAL = datetime.timedelta(minutes=1)
|
|
@@ -50,6 +56,106 @@ NETWORK_INFO_REFRESH_INTERVAL = datetime.timedelta(hours=12)
|
|
|
50
56
|
LOCAL_CONNECTION_CHECK_INTERVAL = datetime.timedelta(seconds=15)
|
|
51
57
|
|
|
52
58
|
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class RpcStrategy:
|
|
61
|
+
"""Strategy for encoding/sending/decoding RPC commands."""
|
|
62
|
+
|
|
63
|
+
name: str # For debug logging
|
|
64
|
+
channel: LocalChannel | MqttChannel
|
|
65
|
+
encoder: Callable[[RequestMessage], RoborockMessage]
|
|
66
|
+
decoder: Callable[[RoborockMessage], ResponseMessage | MapResponse | None]
|
|
67
|
+
health_manager: HealthManager | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RpcChannel(V1RpcChannel):
|
|
71
|
+
"""Provides an RPC interface around a pub/sub transport channel."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, rpc_strategies: list[RpcStrategy]) -> None:
|
|
74
|
+
"""Initialize the RpcChannel with on ordered list of strategies."""
|
|
75
|
+
self._rpc_strategies = rpc_strategies
|
|
76
|
+
|
|
77
|
+
async def send_command(
|
|
78
|
+
self,
|
|
79
|
+
method: CommandType,
|
|
80
|
+
*,
|
|
81
|
+
response_type: type[_T] | None = None,
|
|
82
|
+
params: ParamsType = None,
|
|
83
|
+
) -> _T | Any:
|
|
84
|
+
"""Send a command and return either a decoded or parsed response."""
|
|
85
|
+
request = RequestMessage(method, params=params)
|
|
86
|
+
|
|
87
|
+
# Try each channel in order until one succeeds
|
|
88
|
+
last_exception = None
|
|
89
|
+
for strategy in self._rpc_strategies:
|
|
90
|
+
try:
|
|
91
|
+
decoded_response = await self._send_rpc(strategy, request)
|
|
92
|
+
except RoborockException as e:
|
|
93
|
+
_LOGGER.warning("Command %s failed on %s channel: %s", method, strategy.name, e)
|
|
94
|
+
last_exception = e
|
|
95
|
+
except Exception as e:
|
|
96
|
+
_LOGGER.exception("Unexpected error sending command %s on %s channel", method, strategy.name)
|
|
97
|
+
last_exception = RoborockException(f"Unexpected error: {e}")
|
|
98
|
+
else:
|
|
99
|
+
if response_type is not None:
|
|
100
|
+
if not isinstance(decoded_response, dict):
|
|
101
|
+
raise RoborockException(
|
|
102
|
+
f"Expected dict response to parse {response_type.__name__}, got {type(decoded_response)}"
|
|
103
|
+
)
|
|
104
|
+
return response_type.from_dict(decoded_response)
|
|
105
|
+
return decoded_response
|
|
106
|
+
|
|
107
|
+
raise last_exception or RoborockException("No available connection to send command")
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
async def _send_rpc(strategy: RpcStrategy, request: RequestMessage) -> ResponseData | bytes:
|
|
111
|
+
"""Send a command and return a decoded response type.
|
|
112
|
+
|
|
113
|
+
This provides an RPC interface over a given channel strategy. The device
|
|
114
|
+
channel only supports publish and subscribe, so this function handles
|
|
115
|
+
associating requests with their corresponding responses.
|
|
116
|
+
"""
|
|
117
|
+
future: asyncio.Future[ResponseData | bytes] = asyncio.Future()
|
|
118
|
+
_LOGGER.debug(
|
|
119
|
+
"Sending command (%s, request_id=%s): %s, params=%s",
|
|
120
|
+
strategy.name,
|
|
121
|
+
request.request_id,
|
|
122
|
+
request.method,
|
|
123
|
+
request.params,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
message = strategy.encoder(request)
|
|
127
|
+
|
|
128
|
+
def find_response(response_message: RoborockMessage) -> None:
|
|
129
|
+
try:
|
|
130
|
+
decoded = strategy.decoder(response_message)
|
|
131
|
+
except RoborockException as ex:
|
|
132
|
+
_LOGGER.debug("Exception while decoding message (%s): %s", response_message, ex)
|
|
133
|
+
return
|
|
134
|
+
if decoded is None:
|
|
135
|
+
return
|
|
136
|
+
_LOGGER.debug("Received response (%s, request_id=%s)", strategy.name, decoded.request_id)
|
|
137
|
+
if decoded.request_id == request.request_id:
|
|
138
|
+
if isinstance(decoded, ResponseMessage) and decoded.api_error:
|
|
139
|
+
future.set_exception(decoded.api_error)
|
|
140
|
+
else:
|
|
141
|
+
future.set_result(decoded.data)
|
|
142
|
+
|
|
143
|
+
unsub = await strategy.channel.subscribe(find_response)
|
|
144
|
+
try:
|
|
145
|
+
await strategy.channel.publish(message)
|
|
146
|
+
result = await asyncio.wait_for(future, timeout=_TIMEOUT)
|
|
147
|
+
except TimeoutError as ex:
|
|
148
|
+
if strategy.health_manager:
|
|
149
|
+
await strategy.health_manager.on_timeout()
|
|
150
|
+
future.cancel()
|
|
151
|
+
raise RoborockException(f"Command timed out after {_TIMEOUT}s") from ex
|
|
152
|
+
finally:
|
|
153
|
+
unsub()
|
|
154
|
+
if strategy.health_manager:
|
|
155
|
+
await strategy.health_manager.on_success()
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
53
159
|
class V1Channel(Channel):
|
|
54
160
|
"""Unified V1 protocol channel with automatic MQTT/local connection handling.
|
|
55
161
|
|
|
@@ -66,23 +172,13 @@ class V1Channel(Channel):
|
|
|
66
172
|
local_session: LocalSession,
|
|
67
173
|
cache: Cache,
|
|
68
174
|
) -> None:
|
|
69
|
-
"""Initialize the V1Channel.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
mqtt_channel: MQTT channel for cloud communication
|
|
73
|
-
local_session: Factory that creates LocalChannels for a hostname.
|
|
74
|
-
"""
|
|
175
|
+
"""Initialize the V1Channel."""
|
|
75
176
|
self._device_uid = device_uid
|
|
177
|
+
self._security_data = security_data
|
|
76
178
|
self._mqtt_channel = mqtt_channel
|
|
77
|
-
self.
|
|
179
|
+
self._mqtt_health_manager = HealthManager(self._mqtt_channel.restart)
|
|
78
180
|
self._local_session = local_session
|
|
79
181
|
self._local_channel: LocalChannel | None = None
|
|
80
|
-
self._local_rpc_channel: V1RpcChannel | None = None
|
|
81
|
-
# Prefer local, fallback to MQTT
|
|
82
|
-
self._combined_rpc_channel = PickFirstAvailable(
|
|
83
|
-
[lambda: self._local_rpc_channel, lambda: self._mqtt_rpc_channel]
|
|
84
|
-
)
|
|
85
|
-
self._map_rpc_channel = create_map_rpc_channel(mqtt_channel, security_data)
|
|
86
182
|
self._mqtt_unsub: Callable[[], None] | None = None
|
|
87
183
|
self._local_unsub: Callable[[], None] | None = None
|
|
88
184
|
self._callback: Callable[[RoborockMessage], None] | None = None
|
|
@@ -107,18 +203,60 @@ class V1Channel(Channel):
|
|
|
107
203
|
|
|
108
204
|
@property
|
|
109
205
|
def rpc_channel(self) -> V1RpcChannel:
|
|
110
|
-
"""Return the combined RPC channel prefers local with a fallback to MQTT."""
|
|
111
|
-
|
|
206
|
+
"""Return the combined RPC channel that prefers local with a fallback to MQTT."""
|
|
207
|
+
strategies = []
|
|
208
|
+
if local_rpc_strategy := self._create_local_rpc_strategy():
|
|
209
|
+
strategies.append(local_rpc_strategy)
|
|
210
|
+
strategies.append(self._create_mqtt_rpc_strategy())
|
|
211
|
+
return RpcChannel(strategies)
|
|
112
212
|
|
|
113
213
|
@property
|
|
114
214
|
def mqtt_rpc_channel(self) -> V1RpcChannel:
|
|
115
|
-
"""Return the MQTT RPC channel."""
|
|
116
|
-
return self.
|
|
215
|
+
"""Return the MQTT-only RPC channel."""
|
|
216
|
+
return RpcChannel([self._create_mqtt_rpc_strategy()])
|
|
117
217
|
|
|
118
218
|
@property
|
|
119
219
|
def map_rpc_channel(self) -> V1RpcChannel:
|
|
120
220
|
"""Return the map RPC channel used for fetching map content."""
|
|
121
|
-
|
|
221
|
+
decoder = create_map_response_decoder(security_data=self._security_data)
|
|
222
|
+
return RpcChannel([self._create_mqtt_rpc_strategy(decoder)])
|
|
223
|
+
|
|
224
|
+
def _create_local_rpc_strategy(self) -> RpcStrategy | None:
|
|
225
|
+
"""Create the RPC strategy for local transport."""
|
|
226
|
+
if self._local_channel is None or not self.is_local_connected:
|
|
227
|
+
return None
|
|
228
|
+
return RpcStrategy(
|
|
229
|
+
name="local",
|
|
230
|
+
channel=self._local_channel,
|
|
231
|
+
encoder=self._local_encoder,
|
|
232
|
+
decoder=decode_rpc_response,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def _local_encoder(self, x: RequestMessage) -> RoborockMessage:
|
|
236
|
+
"""Encode a request message for local transport.
|
|
237
|
+
|
|
238
|
+
This will read the current local channel's protocol version which
|
|
239
|
+
changes as the protocol version is discovered.
|
|
240
|
+
"""
|
|
241
|
+
if self._local_channel is None:
|
|
242
|
+
raise ValueError("Local channel unavailable for encoding")
|
|
243
|
+
return x.encode_message(
|
|
244
|
+
RoborockMessageProtocol.GENERAL_REQUEST,
|
|
245
|
+
version=self._local_channel.protocol_version,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def _create_mqtt_rpc_strategy(self, decoder: Callable[[RoborockMessage], Any] = decode_rpc_response) -> RpcStrategy:
|
|
249
|
+
"""Create the RPC strategy for MQTT transport with optional custom decoder."""
|
|
250
|
+
return RpcStrategy(
|
|
251
|
+
name="mqtt",
|
|
252
|
+
channel=self._mqtt_channel,
|
|
253
|
+
encoder=lambda x: x.encode_message(
|
|
254
|
+
RoborockMessageProtocol.RPC_REQUEST,
|
|
255
|
+
security_data=self._security_data,
|
|
256
|
+
),
|
|
257
|
+
decoder=decoder,
|
|
258
|
+
health_manager=self._mqtt_health_manager,
|
|
259
|
+
)
|
|
122
260
|
|
|
123
261
|
async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
|
|
124
262
|
"""Subscribe to all messages from the device.
|
|
@@ -142,7 +280,7 @@ class V1Channel(Channel):
|
|
|
142
280
|
# Make an initial, optimistic attempt to connect to local with the
|
|
143
281
|
# cache. The cache information will be refreshed by the background task.
|
|
144
282
|
try:
|
|
145
|
-
await self._local_connect(
|
|
283
|
+
await self._local_connect(prefer_cache=True)
|
|
146
284
|
except RoborockException as err:
|
|
147
285
|
_LOGGER.warning("Could not establish local connection for device %s: %s", self._device_uid, err)
|
|
148
286
|
|
|
@@ -175,20 +313,24 @@ class V1Channel(Channel):
|
|
|
175
313
|
self._callback = callback
|
|
176
314
|
return unsub
|
|
177
315
|
|
|
178
|
-
async def _get_networking_info(self, *,
|
|
316
|
+
async def _get_networking_info(self, *, prefer_cache: bool = True) -> NetworkInfo:
|
|
179
317
|
"""Retrieve networking information for the device.
|
|
180
318
|
|
|
181
319
|
This is a cloud only command used to get the local device's IP address.
|
|
182
320
|
"""
|
|
183
321
|
cache_data = await self._cache.get()
|
|
184
|
-
if
|
|
322
|
+
if prefer_cache and cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
|
|
185
323
|
_LOGGER.debug("Using cached network info for device %s", self._device_uid)
|
|
186
324
|
return network_info
|
|
187
325
|
try:
|
|
188
|
-
network_info = await self.
|
|
326
|
+
network_info = await self.mqtt_rpc_channel.send_command(
|
|
189
327
|
RoborockCommand.GET_NETWORK_INFO, response_type=NetworkInfo
|
|
190
328
|
)
|
|
191
329
|
except RoborockException as e:
|
|
330
|
+
_LOGGER.debug("Error fetching network info for device %s", self._device_uid)
|
|
331
|
+
if cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
|
|
332
|
+
_LOGGER.debug("Falling back to cached network info for device %s after error", self._device_uid)
|
|
333
|
+
return network_info
|
|
192
334
|
raise RoborockException(f"Network info failed for device {self._device_uid}") from e
|
|
193
335
|
_LOGGER.debug("Network info for device %s: %s", self._device_uid, network_info)
|
|
194
336
|
self._last_network_info_refresh = datetime.datetime.now(datetime.UTC)
|
|
@@ -196,12 +338,12 @@ class V1Channel(Channel):
|
|
|
196
338
|
await self._cache.set(cache_data)
|
|
197
339
|
return network_info
|
|
198
340
|
|
|
199
|
-
async def _local_connect(self, *,
|
|
341
|
+
async def _local_connect(self, *, prefer_cache: bool = True) -> None:
|
|
200
342
|
"""Set up local connection if possible."""
|
|
201
343
|
_LOGGER.debug(
|
|
202
|
-
"Attempting to connect to local channel for device %s (
|
|
344
|
+
"Attempting to connect to local channel for device %s (prefer_cache=%s)", self._device_uid, prefer_cache
|
|
203
345
|
)
|
|
204
|
-
networking_info = await self._get_networking_info(
|
|
346
|
+
networking_info = await self._get_networking_info(prefer_cache=prefer_cache)
|
|
205
347
|
host = networking_info.ip
|
|
206
348
|
_LOGGER.debug("Connecting to local channel at %s", host)
|
|
207
349
|
# Create a new local channel and connect
|
|
@@ -212,7 +354,6 @@ class V1Channel(Channel):
|
|
|
212
354
|
raise RoborockException(f"Error connecting to local device {self._device_uid}: {e}") from e
|
|
213
355
|
# Wire up the new channel
|
|
214
356
|
self._local_channel = local_channel
|
|
215
|
-
self._local_rpc_channel = create_local_rpc_channel(self._local_channel)
|
|
216
357
|
self._local_unsub = await self._local_channel.subscribe(self._on_local_message)
|
|
217
358
|
_LOGGER.info("Successfully connected to local device %s", self._device_uid)
|
|
218
359
|
|
|
@@ -236,7 +377,7 @@ class V1Channel(Channel):
|
|
|
236
377
|
reconnect_backoff = min(reconnect_backoff * RECONNECT_MULTIPLIER, MAX_RECONNECT_INTERVAL)
|
|
237
378
|
|
|
238
379
|
use_cache = self._should_use_cache(local_connect_failures)
|
|
239
|
-
await self._local_connect(
|
|
380
|
+
await self._local_connect(prefer_cache=use_cache)
|
|
240
381
|
# Reset backoff and failures on success
|
|
241
382
|
reconnect_backoff = MIN_RECONNECT_INTERVAL
|
|
242
383
|
local_connect_failures = 0
|
|
@@ -12,9 +12,9 @@ import time
|
|
|
12
12
|
from collections.abc import Callable
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from enum import StrEnum
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import Any, Protocol, TypeVar, overload
|
|
16
16
|
|
|
17
|
-
from roborock.data import RRiot
|
|
17
|
+
from roborock.data import RoborockBase, RRiot
|
|
18
18
|
from roborock.exceptions import RoborockException, RoborockUnsupportedFeature
|
|
19
19
|
from roborock.protocol import Utils
|
|
20
20
|
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
@@ -27,6 +27,7 @@ __all__ = [
|
|
|
27
27
|
"SecurityData",
|
|
28
28
|
"create_security_data",
|
|
29
29
|
"decode_rpc_response",
|
|
30
|
+
"V1RpcChannel",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
CommandType = RoborockCommand | str
|
|
@@ -208,3 +209,35 @@ def create_map_response_decoder(security_data: SecurityData) -> Callable[[Roboro
|
|
|
208
209
|
return MapResponse(request_id=request_id, data=decompressed)
|
|
209
210
|
|
|
210
211
|
return _decode_map_response
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
_T = TypeVar("_T", bound=RoborockBase)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class V1RpcChannel(Protocol):
|
|
218
|
+
"""Protocol for V1 RPC channels.
|
|
219
|
+
|
|
220
|
+
This is a wrapper around a raw channel that provides a high-level interface
|
|
221
|
+
for sending commands and receiving responses.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
@overload
|
|
225
|
+
async def send_command(
|
|
226
|
+
self,
|
|
227
|
+
method: CommandType,
|
|
228
|
+
*,
|
|
229
|
+
params: ParamsType = None,
|
|
230
|
+
) -> Any:
|
|
231
|
+
"""Send a command and return a decoded response."""
|
|
232
|
+
...
|
|
233
|
+
|
|
234
|
+
@overload
|
|
235
|
+
async def send_command(
|
|
236
|
+
self,
|
|
237
|
+
method: CommandType,
|
|
238
|
+
*,
|
|
239
|
+
response_type: type[_T],
|
|
240
|
+
params: ParamsType = None,
|
|
241
|
+
) -> _T:
|
|
242
|
+
"""Send a command and return a parsed response RoborockBase type."""
|
|
243
|
+
...
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
"""V1 Rpc Channel for Roborock devices.
|
|
2
|
-
|
|
3
|
-
This is a wrapper around the V1 channel that provides a higher level interface
|
|
4
|
-
for sending typed commands and receiving typed responses. This also provides
|
|
5
|
-
a simple interface for sending commands and receiving responses over both MQTT
|
|
6
|
-
and local connections, preferring local when available.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import asyncio
|
|
10
|
-
import logging
|
|
11
|
-
from collections.abc import Callable
|
|
12
|
-
from typing import Any, Protocol, TypeVar, overload
|
|
13
|
-
|
|
14
|
-
from roborock.data import RoborockBase
|
|
15
|
-
from roborock.exceptions import RoborockException
|
|
16
|
-
from roborock.mqtt.health_manager import HealthManager
|
|
17
|
-
from roborock.protocols.v1_protocol import (
|
|
18
|
-
CommandType,
|
|
19
|
-
MapResponse,
|
|
20
|
-
ParamsType,
|
|
21
|
-
RequestMessage,
|
|
22
|
-
ResponseData,
|
|
23
|
-
ResponseMessage,
|
|
24
|
-
SecurityData,
|
|
25
|
-
create_map_response_decoder,
|
|
26
|
-
decode_rpc_response,
|
|
27
|
-
)
|
|
28
|
-
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
29
|
-
|
|
30
|
-
from .local_channel import LocalChannel
|
|
31
|
-
from .mqtt_channel import MqttChannel
|
|
32
|
-
|
|
33
|
-
_LOGGER = logging.getLogger(__name__)
|
|
34
|
-
_TIMEOUT = 10.0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
_T = TypeVar("_T", bound=RoborockBase)
|
|
38
|
-
_V = TypeVar("_V")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class V1RpcChannel(Protocol):
|
|
42
|
-
"""Protocol for V1 RPC channels.
|
|
43
|
-
|
|
44
|
-
This is a wrapper around a raw channel that provides a high-level interface
|
|
45
|
-
for sending commands and receiving responses.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
@overload
|
|
49
|
-
async def send_command(
|
|
50
|
-
self,
|
|
51
|
-
method: CommandType,
|
|
52
|
-
*,
|
|
53
|
-
params: ParamsType = None,
|
|
54
|
-
) -> Any:
|
|
55
|
-
"""Send a command and return a decoded response."""
|
|
56
|
-
...
|
|
57
|
-
|
|
58
|
-
@overload
|
|
59
|
-
async def send_command(
|
|
60
|
-
self,
|
|
61
|
-
method: CommandType,
|
|
62
|
-
*,
|
|
63
|
-
response_type: type[_T],
|
|
64
|
-
params: ParamsType = None,
|
|
65
|
-
) -> _T:
|
|
66
|
-
"""Send a command and return a parsed response RoborockBase type."""
|
|
67
|
-
...
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class BaseV1RpcChannel(V1RpcChannel):
|
|
71
|
-
"""Base implementation that provides the typed response logic."""
|
|
72
|
-
|
|
73
|
-
async def send_command(
|
|
74
|
-
self,
|
|
75
|
-
method: CommandType,
|
|
76
|
-
*,
|
|
77
|
-
response_type: type[_T] | None = None,
|
|
78
|
-
params: ParamsType = None,
|
|
79
|
-
) -> _T | Any:
|
|
80
|
-
"""Send a command and return either a decoded or parsed response."""
|
|
81
|
-
decoded_response = await self._send_raw_command(method, params=params)
|
|
82
|
-
|
|
83
|
-
if response_type is not None:
|
|
84
|
-
return response_type.from_dict(decoded_response)
|
|
85
|
-
return decoded_response
|
|
86
|
-
|
|
87
|
-
async def _send_raw_command(
|
|
88
|
-
self,
|
|
89
|
-
method: CommandType,
|
|
90
|
-
*,
|
|
91
|
-
params: ParamsType = None,
|
|
92
|
-
) -> Any:
|
|
93
|
-
"""Send a raw command and return the decoded response. Must be implemented by subclasses."""
|
|
94
|
-
raise NotImplementedError
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class PickFirstAvailable(BaseV1RpcChannel):
|
|
98
|
-
"""A V1 RPC channel that tries multiple channels and picks the first that works."""
|
|
99
|
-
|
|
100
|
-
def __init__(
|
|
101
|
-
self,
|
|
102
|
-
channel_cbs: list[Callable[[], V1RpcChannel | None]],
|
|
103
|
-
) -> None:
|
|
104
|
-
"""Initialize the pick-first-available channel."""
|
|
105
|
-
self._channel_cbs = channel_cbs
|
|
106
|
-
|
|
107
|
-
async def _send_raw_command(
|
|
108
|
-
self,
|
|
109
|
-
method: CommandType,
|
|
110
|
-
*,
|
|
111
|
-
params: ParamsType = None,
|
|
112
|
-
) -> Any:
|
|
113
|
-
"""Send a command and return a parsed response RoborockBase type."""
|
|
114
|
-
for channel_cb in self._channel_cbs:
|
|
115
|
-
if channel := channel_cb():
|
|
116
|
-
return await channel.send_command(method, params=params)
|
|
117
|
-
raise RoborockException("No available connection to send command")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class PayloadEncodedV1RpcChannel(BaseV1RpcChannel):
|
|
121
|
-
"""Protocol for V1 channels that send encoded commands."""
|
|
122
|
-
|
|
123
|
-
def __init__(
|
|
124
|
-
self,
|
|
125
|
-
name: str,
|
|
126
|
-
channel: MqttChannel | LocalChannel,
|
|
127
|
-
payload_encoder: Callable[[RequestMessage], RoborockMessage],
|
|
128
|
-
decoder: Callable[[RoborockMessage], ResponseMessage] | Callable[[RoborockMessage], MapResponse | None],
|
|
129
|
-
health_manager: HealthManager | None = None,
|
|
130
|
-
) -> None:
|
|
131
|
-
"""Initialize the channel with a raw channel and an encoder function."""
|
|
132
|
-
self._name = name
|
|
133
|
-
self._channel = channel
|
|
134
|
-
self._payload_encoder = payload_encoder
|
|
135
|
-
self._decoder = decoder
|
|
136
|
-
self._health_manager = health_manager
|
|
137
|
-
|
|
138
|
-
async def _send_raw_command(
|
|
139
|
-
self,
|
|
140
|
-
method: CommandType,
|
|
141
|
-
*,
|
|
142
|
-
params: ParamsType = None,
|
|
143
|
-
) -> ResponseData | bytes:
|
|
144
|
-
"""Send a command and return a parsed response RoborockBase type."""
|
|
145
|
-
request_message = RequestMessage(method, params=params)
|
|
146
|
-
_LOGGER.debug(
|
|
147
|
-
"Sending command (%s, request_id=%s): %s, params=%s", self._name, request_message.request_id, method, params
|
|
148
|
-
)
|
|
149
|
-
message = self._payload_encoder(request_message)
|
|
150
|
-
|
|
151
|
-
future: asyncio.Future[ResponseData | bytes] = asyncio.Future()
|
|
152
|
-
|
|
153
|
-
def find_response(response_message: RoborockMessage) -> None:
|
|
154
|
-
try:
|
|
155
|
-
decoded = self._decoder(response_message)
|
|
156
|
-
except RoborockException as ex:
|
|
157
|
-
_LOGGER.debug("Exception while decoding message (%s): %s", response_message, ex)
|
|
158
|
-
return
|
|
159
|
-
if decoded is None:
|
|
160
|
-
return
|
|
161
|
-
_LOGGER.debug("Received response (%s, request_id=%s)", self._name, decoded.request_id)
|
|
162
|
-
if decoded.request_id == request_message.request_id:
|
|
163
|
-
if isinstance(decoded, ResponseMessage) and decoded.api_error:
|
|
164
|
-
future.set_exception(decoded.api_error)
|
|
165
|
-
else:
|
|
166
|
-
future.set_result(decoded.data)
|
|
167
|
-
|
|
168
|
-
unsub = await self._channel.subscribe(find_response)
|
|
169
|
-
try:
|
|
170
|
-
await self._channel.publish(message)
|
|
171
|
-
result = await asyncio.wait_for(future, timeout=_TIMEOUT)
|
|
172
|
-
except TimeoutError as ex:
|
|
173
|
-
if self._health_manager:
|
|
174
|
-
await self._health_manager.on_timeout()
|
|
175
|
-
future.cancel()
|
|
176
|
-
raise RoborockException(f"Command timed out after {_TIMEOUT}s") from ex
|
|
177
|
-
finally:
|
|
178
|
-
unsub()
|
|
179
|
-
|
|
180
|
-
if self._health_manager:
|
|
181
|
-
await self._health_manager.on_success()
|
|
182
|
-
return result
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def create_mqtt_rpc_channel(mqtt_channel: MqttChannel, security_data: SecurityData) -> V1RpcChannel:
|
|
186
|
-
"""Create a V1 RPC channel using an MQTT channel."""
|
|
187
|
-
return PayloadEncodedV1RpcChannel(
|
|
188
|
-
"mqtt",
|
|
189
|
-
mqtt_channel,
|
|
190
|
-
lambda x: x.encode_message(RoborockMessageProtocol.RPC_REQUEST, security_data=security_data),
|
|
191
|
-
decode_rpc_response,
|
|
192
|
-
health_manager=HealthManager(mqtt_channel.restart),
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def create_local_rpc_channel(local_channel: LocalChannel) -> V1RpcChannel:
|
|
197
|
-
"""Create a V1 RPC channel using a local channel."""
|
|
198
|
-
return PayloadEncodedV1RpcChannel(
|
|
199
|
-
"local",
|
|
200
|
-
local_channel,
|
|
201
|
-
lambda x: x.encode_message(RoborockMessageProtocol.GENERAL_REQUEST, version=local_channel.protocol_version),
|
|
202
|
-
decode_rpc_response,
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def create_map_rpc_channel(
|
|
207
|
-
mqtt_channel: MqttChannel,
|
|
208
|
-
security_data: SecurityData,
|
|
209
|
-
) -> V1RpcChannel:
|
|
210
|
-
"""Create a V1 RPC channel that fetches map data.
|
|
211
|
-
|
|
212
|
-
This will prefer local channels when available, falling back to MQTT
|
|
213
|
-
channels if not. If neither is available, an exception will be raised
|
|
214
|
-
when trying to send a command.
|
|
215
|
-
"""
|
|
216
|
-
return PayloadEncodedV1RpcChannel(
|
|
217
|
-
"map",
|
|
218
|
-
mqtt_channel,
|
|
219
|
-
lambda x: x.encode_message(RoborockMessageProtocol.RPC_REQUEST, security_data=security_data),
|
|
220
|
-
create_map_response_decoder(security_data=security_data),
|
|
221
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/data/b01_q7/b01_q7_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_local_client_v1.py
RENAMED
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-3.8.3 → python_roborock-3.8.5}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|