python-roborock 3.8.4__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.4 → python_roborock-3.8.5}/PKG-INFO +1 -1
- {python_roborock-3.8.4 → python_roborock-3.8.5}/pyproject.toml +1 -1
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/mqtt_channel.py +1 -1
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/__init__.py +1 -1
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/common.py +1 -1
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/v1_channel.py +167 -30
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/protocols/v1_protocol.py +35 -2
- python_roborock-3.8.4/roborock/devices/v1_rpc_channel.py +0 -221
- {python_roborock-3.8.4 → python_roborock-3.8.5}/.gitignore +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/LICENSE +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/README.md +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/api.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/callbacks.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/cli.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/cloud_api.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/command_cache.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/const.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/code_mappings.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/containers.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/device_features.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/README.md +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/a01_channel.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/b01_channel.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/cache.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/channel.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/device.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/device_manager.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/file_cache.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/local_channel.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/exceptions.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/map/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/map/map_parser.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/mqtt/session.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/protocol.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/protocols/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/protocols/b01_protocol.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/py.typed +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/roborock_future.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/roborock_message.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/roborock_typing.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/util.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-3.8.4 → 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"
|
|
@@ -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__)
|
|
@@ -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.
|
|
@@ -185,7 +323,7 @@ class V1Channel(Channel):
|
|
|
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:
|
|
@@ -216,7 +354,6 @@ class V1Channel(Channel):
|
|
|
216
354
|
raise RoborockException(f"Error connecting to local device {self._device_uid}: {e}") from e
|
|
217
355
|
# Wire up the new channel
|
|
218
356
|
self._local_channel = local_channel
|
|
219
|
-
self._local_rpc_channel = create_local_rpc_channel(self._local_channel)
|
|
220
357
|
self._local_unsub = await self._local_channel.subscribe(self._on_local_message)
|
|
221
358
|
_LOGGER.info("Successfully connected to local device %s", self._device_uid)
|
|
222
359
|
|
|
@@ -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.4 → 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.4 → 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
|
|
File without changes
|
{python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-3.8.4 → 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
|
|
File without changes
|
{python_roborock-3.8.4 → 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.4 → 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.4 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
{python_roborock-3.8.4 → python_roborock-3.8.5}/roborock/version_1_apis/roborock_local_client_v1.py
RENAMED
|
File without changes
|
{python_roborock-3.8.4 → 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.4 → python_roborock-3.8.5}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|