python-roborock 5.35.0__tar.gz → 5.37.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.
- {python_roborock-5.35.0 → python_roborock-5.37.0}/PKG-INFO +1 -1
- {python_roborock-5.35.0 → python_roborock-5.37.0}/pyproject.toml +1 -1
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/device_features.py +6 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/device_manager.py +1 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/rpc/a01_channel.py +13 -2
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/rpc/v1_channel.py +52 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/__init__.py +17 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/common.py +1 -1
- python_roborock-5.37.0/roborock/devices/traits/v1/obstacle_photos.py +104 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/transport/mqtt_channel.py +7 -3
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/mqtt/roborock_session.py +12 -6
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/mqtt/session.py +25 -1
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/protocols/a01_protocol.py +5 -1
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/protocols/v1_protocol.py +45 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/roborock_typing.py +1 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/testing/channel.py +6 -1
- {python_roborock-5.35.0 → python_roborock-5.37.0}/.gitignore +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/LICENSE +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/README.md +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/callbacks.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/cli.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/const.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/containers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/mower/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/mower/mower_containers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/README.md +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/cache.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/device.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/button_light.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/consumable.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/map.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/network_info.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/volume.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/common.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/diagnostics.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/exceptions.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/b01_grid_layers.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/b01_q10_map_parser.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/b01_q10_overlays.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/b01_q10_render.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/protocol.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/py.typed +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/roborock_message.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/testing/__init__.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/testing/b01_q10_simulator.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/testing/cloud.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/testing/simulator.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/testing/v1_simulator.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/util.py +0 -0
- {python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.37.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/python-roborock/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 = "5.
|
|
3
|
+
version = "5.37.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"
|
|
@@ -539,6 +539,12 @@ class DeviceFeatures(RoborockBase):
|
|
|
539
539
|
]
|
|
540
540
|
}
|
|
541
541
|
)
|
|
542
|
+
is_ai_recognition_setting_supported: bool = field(
|
|
543
|
+
metadata={"product_features": [ProductFeatures.AIRECOGNITION_SETTING]}
|
|
544
|
+
)
|
|
545
|
+
is_ai_recognition_obstacle_supported: bool = field(
|
|
546
|
+
metadata={"product_features": [ProductFeatures.AIRECOGNITION_OBSTACLE]}
|
|
547
|
+
)
|
|
542
548
|
is_pure_clean_mop_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_PURECLEANMOP]})
|
|
543
549
|
is_new_remote_view_supported: bool = field(metadata={"product_features": [ProductFeatures.REMOTE_BACK]})
|
|
544
550
|
is_max_plus_mode_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_MAXPLUS]})
|
|
@@ -7,6 +7,7 @@ from typing import Any, overload
|
|
|
7
7
|
|
|
8
8
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
9
9
|
from roborock.exceptions import RoborockException
|
|
10
|
+
from roborock.mqtt.session import MqttQos
|
|
10
11
|
from roborock.protocols.a01_protocol import (
|
|
11
12
|
decode_rpc_response,
|
|
12
13
|
encode_mqtt_payload,
|
|
@@ -30,6 +31,7 @@ async def send_decoded_command(
|
|
|
30
31
|
mqtt_channel: MqttChannel,
|
|
31
32
|
params: dict[RoborockDyadDataProtocol, Any],
|
|
32
33
|
value_encoder: Callable[[Any], Any] | None = None,
|
|
34
|
+
qos: MqttQos = MqttQos.AT_MOST_ONCE,
|
|
33
35
|
) -> dict[RoborockDyadDataProtocol, Any]: ...
|
|
34
36
|
|
|
35
37
|
|
|
@@ -38,6 +40,7 @@ async def send_decoded_command(
|
|
|
38
40
|
mqtt_channel: MqttChannel,
|
|
39
41
|
params: dict[RoborockZeoProtocol, Any],
|
|
40
42
|
value_encoder: Callable[[Any], Any] | None = None,
|
|
43
|
+
qos: MqttQos = MqttQos.AT_MOST_ONCE,
|
|
41
44
|
) -> dict[RoborockZeoProtocol, Any]: ...
|
|
42
45
|
|
|
43
46
|
|
|
@@ -45,8 +48,16 @@ async def send_decoded_command(
|
|
|
45
48
|
mqtt_channel: MqttChannel,
|
|
46
49
|
params: dict[RoborockDyadDataProtocol, Any] | dict[RoborockZeoProtocol, Any],
|
|
47
50
|
value_encoder: Callable[[Any], Any] | None = None,
|
|
51
|
+
qos: MqttQos = MqttQos.AT_MOST_ONCE,
|
|
48
52
|
) -> dict[RoborockDyadDataProtocol, Any] | dict[RoborockZeoProtocol, Any]:
|
|
49
|
-
"""Send a command on the MQTT channel and get a decoded response.
|
|
53
|
+
"""Send a command on the MQTT channel and get a decoded response.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
mqtt_channel: The MQTT channel to send the command on.
|
|
57
|
+
params: The parameters to send.
|
|
58
|
+
value_encoder: A function to encode the values of the dictionary.
|
|
59
|
+
qos: The MQTT QoS level. Defaults to AT_MOST_ONCE.
|
|
60
|
+
"""
|
|
50
61
|
_LOGGER.debug("Sending MQTT command: %s", params)
|
|
51
62
|
roborock_message = encode_mqtt_payload(params, value_encoder)
|
|
52
63
|
|
|
@@ -54,7 +65,7 @@ async def send_decoded_command(
|
|
|
54
65
|
# block waiting for a response. Queries are handled below.
|
|
55
66
|
param_values = {int(k): v for k, v in params.items()}
|
|
56
67
|
if not (query_values := param_values.get(_ID_QUERY)):
|
|
57
|
-
await mqtt_channel.publish(roborock_message)
|
|
68
|
+
await mqtt_channel.publish(roborock_message, qos=qos)
|
|
58
69
|
return {}
|
|
59
70
|
|
|
60
71
|
# Merge any results together than contain the requested data. This
|
|
@@ -29,6 +29,7 @@ from roborock.protocols.v1_protocol import (
|
|
|
29
29
|
ResponseMessage,
|
|
30
30
|
SecurityData,
|
|
31
31
|
V1RpcChannel,
|
|
32
|
+
create_blob_response_decoder,
|
|
32
33
|
create_map_response_decoder,
|
|
33
34
|
create_security_data,
|
|
34
35
|
decode_data_protocol_message,
|
|
@@ -161,6 +162,50 @@ class RpcChannel(V1RpcChannel):
|
|
|
161
162
|
return result
|
|
162
163
|
|
|
163
164
|
|
|
165
|
+
class BlobRpcChannel(V1RpcChannel):
|
|
166
|
+
"""RPC channel that adds the security envelope required for blob requests."""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
rpc_channel: V1RpcChannel,
|
|
171
|
+
raw_blob_rpc_channel: V1RpcChannel,
|
|
172
|
+
security_data: SecurityData,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Initialize the blob RPC channel."""
|
|
175
|
+
self._rpc_channel = rpc_channel
|
|
176
|
+
self._raw_blob_rpc_channel = raw_blob_rpc_channel
|
|
177
|
+
self._security_data = security_data
|
|
178
|
+
|
|
179
|
+
async def send_command(
|
|
180
|
+
self,
|
|
181
|
+
method: CommandType,
|
|
182
|
+
*,
|
|
183
|
+
response_type: type[_T] | None = None,
|
|
184
|
+
params: ParamsType = None,
|
|
185
|
+
) -> _T | Any:
|
|
186
|
+
"""Send a blob command after adding the device security parameters."""
|
|
187
|
+
public_key = await self._rpc_channel.send_command(RoborockCommand.GET_RANDOM_PKEY)
|
|
188
|
+
if not isinstance(public_key, dict) or not isinstance(public_key.get("pub_key"), dict):
|
|
189
|
+
raise RoborockException("get_random_pkey response did not contain a public key")
|
|
190
|
+
security = self._security_data.to_dict()["security"]
|
|
191
|
+
blob_params = {
|
|
192
|
+
"security": {
|
|
193
|
+
"pub_key": public_key["pub_key"],
|
|
194
|
+
"cipher_suite": 0,
|
|
195
|
+
},
|
|
196
|
+
"endpoint": security["endpoint"],
|
|
197
|
+
"nonce": security["nonce"],
|
|
198
|
+
"data_filter": params,
|
|
199
|
+
}
|
|
200
|
+
if response_type is not None:
|
|
201
|
+
return await self._raw_blob_rpc_channel.send_command(
|
|
202
|
+
method,
|
|
203
|
+
response_type=response_type,
|
|
204
|
+
params=blob_params,
|
|
205
|
+
)
|
|
206
|
+
return await self._raw_blob_rpc_channel.send_command(method, params=blob_params)
|
|
207
|
+
|
|
208
|
+
|
|
164
209
|
class V1Channel(Channel):
|
|
165
210
|
"""Unified V1 protocol channel with automatic MQTT/local connection handling.
|
|
166
211
|
|
|
@@ -245,6 +290,13 @@ class V1Channel(Channel):
|
|
|
245
290
|
decoder = create_map_response_decoder(security_data=self._security_data)
|
|
246
291
|
return RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger)
|
|
247
292
|
|
|
293
|
+
@property
|
|
294
|
+
def blob_rpc_channel(self) -> V1RpcChannel:
|
|
295
|
+
"""Return the blob RPC channel used for fetching binary content."""
|
|
296
|
+
decoder = create_blob_response_decoder()
|
|
297
|
+
raw_blob_rpc_channel = RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger)
|
|
298
|
+
return BlobRpcChannel(self.rpc_channel, raw_blob_rpc_channel, self._security_data)
|
|
299
|
+
|
|
248
300
|
def _create_local_rpc_strategy(self) -> RpcStrategy | None:
|
|
249
301
|
"""Create the RPC strategy for local transport."""
|
|
250
302
|
if self._local_channel is None or not self.is_local_connected:
|
|
@@ -83,6 +83,7 @@ from . import (
|
|
|
83
83
|
map_content,
|
|
84
84
|
maps,
|
|
85
85
|
network_info,
|
|
86
|
+
obstacle_photos,
|
|
86
87
|
rooms,
|
|
87
88
|
routines,
|
|
88
89
|
smart_wash_params,
|
|
@@ -105,6 +106,7 @@ from .led_status import LedStatusTrait
|
|
|
105
106
|
from .map_content import MapContentTrait
|
|
106
107
|
from .maps import MapsTrait
|
|
107
108
|
from .network_info import NetworkInfoTrait
|
|
109
|
+
from .obstacle_photos import ObstaclePhotoTrait
|
|
108
110
|
from .rooms import RoomsTrait
|
|
109
111
|
from .routines import RoutinesTrait
|
|
110
112
|
from .smart_wash_params import SmartWashParamsTrait
|
|
@@ -131,6 +133,7 @@ __all__ = [
|
|
|
131
133
|
"map_content",
|
|
132
134
|
"maps",
|
|
133
135
|
"network_info",
|
|
136
|
+
"obstacle_photos",
|
|
134
137
|
"rooms",
|
|
135
138
|
"routines",
|
|
136
139
|
"smart_wash_params",
|
|
@@ -171,6 +174,7 @@ class PropertiesApi(Trait):
|
|
|
171
174
|
dust_collection_mode: DustCollectionModeTrait | None = None
|
|
172
175
|
wash_towel_mode: WashTowelModeTrait | None = None
|
|
173
176
|
smart_wash_params: SmartWashParamsTrait | None = None
|
|
177
|
+
obstacle_photos: ObstaclePhotoTrait | None = None
|
|
174
178
|
|
|
175
179
|
def __init__(
|
|
176
180
|
self,
|
|
@@ -180,6 +184,7 @@ class PropertiesApi(Trait):
|
|
|
180
184
|
rpc_channel: V1RpcChannel,
|
|
181
185
|
mqtt_rpc_channel: V1RpcChannel,
|
|
182
186
|
map_rpc_channel: V1RpcChannel,
|
|
187
|
+
blob_rpc_channel: V1RpcChannel,
|
|
183
188
|
add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]],
|
|
184
189
|
web_api: UserWebApiClient,
|
|
185
190
|
device_cache: DeviceCache,
|
|
@@ -191,6 +196,7 @@ class PropertiesApi(Trait):
|
|
|
191
196
|
self._rpc_channel = rpc_channel
|
|
192
197
|
self._mqtt_rpc_channel = mqtt_rpc_channel
|
|
193
198
|
self._map_rpc_channel = map_rpc_channel
|
|
199
|
+
self._blob_rpc_channel = blob_rpc_channel
|
|
194
200
|
self._web_api = web_api
|
|
195
201
|
self._device_cache = device_cache
|
|
196
202
|
self._region = region
|
|
@@ -229,6 +235,8 @@ class PropertiesApi(Trait):
|
|
|
229
235
|
# to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
|
|
230
236
|
if hasattr(trait, "mqtt_rpc_channel"):
|
|
231
237
|
return self._mqtt_rpc_channel
|
|
238
|
+
elif hasattr(trait, "blob_rpc_channel"):
|
|
239
|
+
return self._blob_rpc_channel
|
|
232
240
|
elif hasattr(trait, "map_rpc_channel"):
|
|
233
241
|
return self._map_rpc_channel
|
|
234
242
|
else:
|
|
@@ -272,6 +280,11 @@ class PropertiesApi(Trait):
|
|
|
272
280
|
wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode) # type: ignore[assignment]
|
|
273
281
|
self.wash_towel_mode = wash_towel_mode
|
|
274
282
|
|
|
283
|
+
if self.obstacle_photos is None and self._is_supported(ObstaclePhotoTrait, "obstacle_photos", dock_features):
|
|
284
|
+
obstacle_photos = ObstaclePhotoTrait(self._rpc_channel)
|
|
285
|
+
obstacle_photos._rpc_channel = self._get_rpc_channel(obstacle_photos)
|
|
286
|
+
self.obstacle_photos = obstacle_photos
|
|
287
|
+
|
|
275
288
|
# Dynamically create any traits that need to be populated
|
|
276
289
|
for item in fields(self):
|
|
277
290
|
if (trait := getattr(self, item.name, None)) is not None:
|
|
@@ -283,6 +296,8 @@ class PropertiesApi(Trait):
|
|
|
283
296
|
|
|
284
297
|
# Union args may not be in declared order
|
|
285
298
|
item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
|
|
299
|
+
if item_type is ObstaclePhotoTrait:
|
|
300
|
+
continue
|
|
286
301
|
if not self._is_supported(item_type, item.name, dock_features):
|
|
287
302
|
_LOGGER.debug("Trait '%s' not supported, skipping", item.name)
|
|
288
303
|
continue
|
|
@@ -369,6 +384,7 @@ def create(
|
|
|
369
384
|
rpc_channel: V1RpcChannel,
|
|
370
385
|
mqtt_rpc_channel: V1RpcChannel,
|
|
371
386
|
map_rpc_channel: V1RpcChannel,
|
|
387
|
+
blob_rpc_channel: V1RpcChannel,
|
|
372
388
|
add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]],
|
|
373
389
|
web_api: UserWebApiClient,
|
|
374
390
|
device_cache: DeviceCache,
|
|
@@ -383,6 +399,7 @@ def create(
|
|
|
383
399
|
rpc_channel,
|
|
384
400
|
mqtt_rpc_channel,
|
|
385
401
|
map_rpc_channel,
|
|
402
|
+
blob_rpc_channel,
|
|
386
403
|
add_dps_listener,
|
|
387
404
|
web_api,
|
|
388
405
|
device_cache,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Trait for fetching obstacle photos from V1 vacuums."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from roborock.data import RoborockBase
|
|
6
|
+
from roborock.devices.traits.v1 import common
|
|
7
|
+
from roborock.exceptions import RoborockException
|
|
8
|
+
from roborock.protocols.v1_protocol import V1RpcChannel
|
|
9
|
+
from roborock.roborock_typing import RoborockCommand
|
|
10
|
+
|
|
11
|
+
_PHOTO_TYPE_SMALL = 1
|
|
12
|
+
_PHOTO_DATA_BLOCK_TYPE = 3
|
|
13
|
+
_MAP_OBJECT_PHOTO_ENABLED_BIT = 10
|
|
14
|
+
_TYPE_SIZE = 2
|
|
15
|
+
_HEADER_SIZE_SIZE = 2
|
|
16
|
+
_PAYLOAD_SIZE_SIZE = 4
|
|
17
|
+
_MIN_BLOCK_HEADER_SIZE = _TYPE_SIZE + _HEADER_SIZE_SIZE + _PAYLOAD_SIZE_SIZE
|
|
18
|
+
_IMAGE_HEADERS = (b"\x89PNG\r\n\x1a\n", b"\xff\xd8\xff")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ObstaclePhoto(RoborockBase):
|
|
23
|
+
"""Obstacle photo content."""
|
|
24
|
+
|
|
25
|
+
photo_id: str
|
|
26
|
+
image_content: bytes
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ObstaclePhotoConverter(common.V1TraitDataConverter):
|
|
30
|
+
"""Convert a decrypted get_photo payload to an obstacle photo."""
|
|
31
|
+
|
|
32
|
+
def convert(self, response: common.V1ResponseData) -> ObstaclePhoto:
|
|
33
|
+
"""Parse the response from the device into an obstacle photo."""
|
|
34
|
+
if not isinstance(response, bytes):
|
|
35
|
+
raise ValueError(f"Unexpected ObstaclePhotoTrait response format: {type(response)}")
|
|
36
|
+
return ObstaclePhoto(photo_id="", image_content=parse_photo_data(response))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_photo_data(response: bytes) -> bytes:
|
|
40
|
+
"""Parse the get_photo response payload and return image bytes.
|
|
41
|
+
|
|
42
|
+
Roborock's app parses get_photo as a sequence of little-endian typed blocks.
|
|
43
|
+
Block type 3 contains the image bytes.
|
|
44
|
+
"""
|
|
45
|
+
offset = 0
|
|
46
|
+
while offset + _MIN_BLOCK_HEADER_SIZE <= len(response):
|
|
47
|
+
block_type = int.from_bytes(response[offset : offset + _TYPE_SIZE], "little")
|
|
48
|
+
header_size = int.from_bytes(
|
|
49
|
+
response[offset + _TYPE_SIZE : offset + _TYPE_SIZE + _HEADER_SIZE_SIZE],
|
|
50
|
+
"little",
|
|
51
|
+
)
|
|
52
|
+
payload_size = int.from_bytes(
|
|
53
|
+
response[offset + _TYPE_SIZE + _HEADER_SIZE_SIZE : offset + _MIN_BLOCK_HEADER_SIZE],
|
|
54
|
+
"little",
|
|
55
|
+
)
|
|
56
|
+
next_offset = offset + header_size + payload_size
|
|
57
|
+
if header_size < _MIN_BLOCK_HEADER_SIZE or next_offset > len(response):
|
|
58
|
+
raise RoborockException("Invalid obstacle photo payload")
|
|
59
|
+
|
|
60
|
+
if block_type == _PHOTO_DATA_BLOCK_TYPE:
|
|
61
|
+
image_content = response[offset + header_size : next_offset]
|
|
62
|
+
if not image_content.startswith(_IMAGE_HEADERS):
|
|
63
|
+
raise RoborockException("Obstacle photo payload is not a supported image")
|
|
64
|
+
return image_content
|
|
65
|
+
|
|
66
|
+
offset = next_offset
|
|
67
|
+
|
|
68
|
+
raise RoborockException("Obstacle photo payload does not contain photo data")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ObstaclePhotoTrait(RoborockBase, common.V1TraitMixin):
|
|
72
|
+
"""Trait for fetching obstacle photos."""
|
|
73
|
+
|
|
74
|
+
command = RoborockCommand.GET_PHOTO
|
|
75
|
+
converter = ObstaclePhotoConverter()
|
|
76
|
+
blob_rpc_channel = True
|
|
77
|
+
requires_feature = "is_ai_recognition_obstacle_supported"
|
|
78
|
+
|
|
79
|
+
def __init__(self, standard_rpc_channel: V1RpcChannel) -> None:
|
|
80
|
+
"""Initialize the obstacle photo trait."""
|
|
81
|
+
super().__init__()
|
|
82
|
+
self._standard_rpc_channel = standard_rpc_channel
|
|
83
|
+
|
|
84
|
+
async def get_enabled(self) -> bool:
|
|
85
|
+
"""Return whether map object photo capture is enabled on the vacuum."""
|
|
86
|
+
response = await self._standard_rpc_channel.send_command(RoborockCommand.GET_CAMERA_STATUS)
|
|
87
|
+
if not isinstance(response, list) or not response or not isinstance(response[0], int):
|
|
88
|
+
raise RoborockException("get_camera_status response did not contain camera status")
|
|
89
|
+
return bool((response[0] >> _MAP_OBJECT_PHOTO_ENABLED_BIT) & 1)
|
|
90
|
+
|
|
91
|
+
async def get_photo(self, photo_id: str) -> ObstaclePhoto:
|
|
92
|
+
"""Fetch the small obstacle photo for a map photo ID.
|
|
93
|
+
|
|
94
|
+
Photo IDs are available as ``Obstacle.details.photo_name`` in
|
|
95
|
+
``ParsedMapData.map_data.obstacles_with_photo`` and
|
|
96
|
+
``ParsedMapData.map_data.ignored_obstacles_with_photo``.
|
|
97
|
+
"""
|
|
98
|
+
response = await self.rpc_channel.send_command(
|
|
99
|
+
self.command,
|
|
100
|
+
params={"img_id": photo_id, "type": _PHOTO_TYPE_SMALL},
|
|
101
|
+
)
|
|
102
|
+
photo = self.converter.convert(response)
|
|
103
|
+
photo.photo_id = photo_id
|
|
104
|
+
return photo
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/transport/mqtt_channel.py
RENAMED
|
@@ -8,7 +8,7 @@ from roborock.callbacks import decoder_callback
|
|
|
8
8
|
from roborock.data import HomeDataDevice, RRiot, UserData
|
|
9
9
|
from roborock.exceptions import RoborockException
|
|
10
10
|
from roborock.mqtt.health_manager import HealthManager
|
|
11
|
-
from roborock.mqtt.session import MqttParams, MqttSession, MqttSessionException
|
|
11
|
+
from roborock.mqtt.session import MqttParams, MqttQos, MqttSession, MqttSessionException
|
|
12
12
|
from roborock.protocol import create_mqtt_decoder, create_mqtt_encoder
|
|
13
13
|
from roborock.roborock_message import RoborockMessage
|
|
14
14
|
from roborock.util import RoborockLoggerAdapter
|
|
@@ -89,11 +89,15 @@ class MqttChannel(Channel):
|
|
|
89
89
|
finally:
|
|
90
90
|
unsub()
|
|
91
91
|
|
|
92
|
-
async def publish(self, message: RoborockMessage) -> None:
|
|
92
|
+
async def publish(self, message: RoborockMessage, qos: MqttQos = MqttQos.AT_MOST_ONCE) -> None:
|
|
93
93
|
"""Publish a command message.
|
|
94
94
|
|
|
95
95
|
The caller is responsible for handling any responses and associating them
|
|
96
96
|
with the incoming request.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
message: The message to publish.
|
|
100
|
+
qos: The MQTT QoS level. Defaults to AT_MOST_ONCE.
|
|
97
101
|
"""
|
|
98
102
|
try:
|
|
99
103
|
encoded_msg = self._encoder(message)
|
|
@@ -101,7 +105,7 @@ class MqttChannel(Channel):
|
|
|
101
105
|
self._logger.exception("Error encoding MQTT message: %s", e)
|
|
102
106
|
raise RoborockException(f"Failed to encode MQTT message: {e}") from e
|
|
103
107
|
try:
|
|
104
|
-
return await self._mqtt_session.publish(self._publish_topic, encoded_msg)
|
|
108
|
+
return await self._mqtt_session.publish(self._publish_topic, encoded_msg, qos=qos)
|
|
105
109
|
except MqttSessionException as e:
|
|
106
110
|
self._logger.debug("Error publishing MQTT message: %s", e)
|
|
107
111
|
raise RoborockException(f"Failed to publish MQTT message: {e}") from e
|
|
@@ -22,7 +22,7 @@ from roborock.callbacks import CallbackMap
|
|
|
22
22
|
from roborock.diagnostics import Diagnostics, redact_topic_name
|
|
23
23
|
|
|
24
24
|
from .health_manager import HealthManager
|
|
25
|
-
from .session import MqttParams, MqttSession, MqttSessionException, MqttSessionUnauthorized
|
|
25
|
+
from .session import MqttParams, MqttQos, MqttSession, MqttSessionException, MqttSessionUnauthorized
|
|
26
26
|
|
|
27
27
|
_LOGGER = logging.getLogger(__name__)
|
|
28
28
|
_MQTT_LOGGER = logging.getLogger(f"{__name__}.aiomqtt")
|
|
@@ -361,8 +361,14 @@ class RoborockMqttSession(MqttSession):
|
|
|
361
361
|
|
|
362
362
|
return delayed_unsub
|
|
363
363
|
|
|
364
|
-
async def publish(self, topic: str, message: bytes) -> None:
|
|
365
|
-
"""Publish a message on the topic.
|
|
364
|
+
async def publish(self, topic: str, message: bytes, qos: MqttQos = MqttQos.AT_MOST_ONCE) -> None:
|
|
365
|
+
"""Publish a message on the topic.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
topic: The MQTT topic to publish to.
|
|
369
|
+
message: The message payload.
|
|
370
|
+
qos: The MQTT QoS level. Defaults to AT_MOST_ONCE.
|
|
371
|
+
"""
|
|
366
372
|
_LOGGER.debug("Sending message to topic %s: %s", topic, message)
|
|
367
373
|
client: aiomqtt.Client
|
|
368
374
|
async with self._client_lock:
|
|
@@ -371,7 +377,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
371
377
|
client = self._client
|
|
372
378
|
try:
|
|
373
379
|
with self._diagnostics.timer("publish"):
|
|
374
|
-
await client.publish(topic, message)
|
|
380
|
+
await client.publish(topic, message, qos=qos)
|
|
375
381
|
except MqttError as err:
|
|
376
382
|
raise MqttSessionException(f"Error publishing message: {err}") from err
|
|
377
383
|
|
|
@@ -417,13 +423,13 @@ class LazyMqttSession(MqttSession):
|
|
|
417
423
|
await self._maybe_start()
|
|
418
424
|
return await self._session.subscribe(device_id, callback)
|
|
419
425
|
|
|
420
|
-
async def publish(self, topic: str, message: bytes) -> None:
|
|
426
|
+
async def publish(self, topic: str, message: bytes, qos: MqttQos = MqttQos.AT_MOST_ONCE) -> None:
|
|
421
427
|
"""Publish a message on the specified topic.
|
|
422
428
|
|
|
423
429
|
This will raise an exception if the message could not be sent.
|
|
424
430
|
"""
|
|
425
431
|
await self._maybe_start()
|
|
426
|
-
return await self._session.publish(topic, message)
|
|
432
|
+
return await self._session.publish(topic, message, qos=qos)
|
|
427
433
|
|
|
428
434
|
async def close(self) -> None:
|
|
429
435
|
"""Cancels the mqtt loop.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
|
+
from enum import IntEnum
|
|
6
7
|
|
|
7
8
|
from roborock.diagnostics import Diagnostics
|
|
8
9
|
from roborock.exceptions import RoborockException
|
|
@@ -10,6 +11,24 @@ from roborock.mqtt.health_manager import HealthManager
|
|
|
10
11
|
|
|
11
12
|
DEFAULT_TIMEOUT = 30.0
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
class MqttQos(IntEnum):
|
|
16
|
+
"""MQTT Quality of Service levels.
|
|
17
|
+
|
|
18
|
+
A01 devices (Zeo, Dyad) require ``AT_LEAST_ONCE`` for DP200 (start)
|
|
19
|
+
commands. Other protocol versions use ``AT_MOST_ONCE``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
AT_MOST_ONCE = 0
|
|
23
|
+
"""Fire-and-forget. No acknowledgment required."""
|
|
24
|
+
|
|
25
|
+
AT_LEAST_ONCE = 1
|
|
26
|
+
"""Guaranteed delivery with possible duplicates. Broker sends PUBACK."""
|
|
27
|
+
|
|
28
|
+
EXACTLY_ONCE = 2
|
|
29
|
+
"""Guaranteed delivery with no duplicates. Broker sends PUBREC/PUBREL/PUBCOMP."""
|
|
30
|
+
|
|
31
|
+
|
|
13
32
|
SessionUnauthorizedHook = Callable[[], None]
|
|
14
33
|
|
|
15
34
|
|
|
@@ -76,10 +95,15 @@ class MqttSession(ABC):
|
|
|
76
95
|
"""
|
|
77
96
|
|
|
78
97
|
@abstractmethod
|
|
79
|
-
async def publish(self, topic: str, message: bytes) -> None:
|
|
98
|
+
async def publish(self, topic: str, message: bytes, qos: MqttQos = MqttQos.AT_MOST_ONCE) -> None:
|
|
80
99
|
"""Publish a message on the specified topic.
|
|
81
100
|
|
|
82
101
|
This will raise an exception if the message could not be sent.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
topic: The MQTT topic to publish to.
|
|
105
|
+
message: The message payload.
|
|
106
|
+
qos: The MQTT QoS level. Defaults to AT_MOST_ONCE.
|
|
83
107
|
"""
|
|
84
108
|
|
|
85
109
|
@abstractmethod
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import time
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -42,7 +43,10 @@ def encode_mqtt_payload(
|
|
|
42
43
|
"""
|
|
43
44
|
if value_encoder is None:
|
|
44
45
|
value_encoder = _no_encode
|
|
45
|
-
dps_data = {
|
|
46
|
+
dps_data = {
|
|
47
|
+
"dps": {key: value_encoder(value) for key, value in data.items()},
|
|
48
|
+
"t": int(time.time()),
|
|
49
|
+
}
|
|
46
50
|
payload = pad(json.dumps(dps_data).encode("utf-8"), AES.block_size)
|
|
47
51
|
return RoborockMessage(
|
|
48
52
|
protocol=RoborockMessageProtocol.RPC_REQUEST,
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
|
+
import gzip
|
|
6
7
|
import json
|
|
7
8
|
import logging
|
|
8
9
|
import secrets
|
|
9
10
|
import struct
|
|
11
|
+
import zlib
|
|
10
12
|
from collections.abc import Callable
|
|
11
13
|
from dataclasses import dataclass, field
|
|
12
14
|
from enum import StrEnum
|
|
@@ -24,6 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
24
26
|
__all__ = [
|
|
25
27
|
"SecurityData",
|
|
26
28
|
"create_security_data",
|
|
29
|
+
"create_blob_response_decoder",
|
|
27
30
|
"decode_data_protocol_message",
|
|
28
31
|
"decode_rpc_response",
|
|
29
32
|
"V1RpcChannel",
|
|
@@ -261,6 +264,48 @@ class MapResponse:
|
|
|
261
264
|
"""The map data, decrypted and decompressed."""
|
|
262
265
|
|
|
263
266
|
|
|
267
|
+
@dataclass
|
|
268
|
+
class BlobResponse:
|
|
269
|
+
"""Data structure for V1 blob responses."""
|
|
270
|
+
|
|
271
|
+
request_id: int
|
|
272
|
+
"""The request ID of the blob response."""
|
|
273
|
+
|
|
274
|
+
data: bytes
|
|
275
|
+
"""The blob data, decompressed."""
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def create_blob_response_decoder() -> Callable[[RoborockMessage], BlobResponse | None]:
|
|
279
|
+
"""Create a decoder for V1 blob response messages.
|
|
280
|
+
|
|
281
|
+
Obstacle photos are acknowledged through the normal RPC response with
|
|
282
|
+
``["ok"]`` and delivered later as a protocol-301 blob frame. The frame starts
|
|
283
|
+
with ``ROBOROCK``, stores the RPC request id at bytes 8-11, the header size
|
|
284
|
+
at bytes 16-17, and the gzip payload length at bytes 20-23.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
def _decode_blob_response(message: RoborockMessage) -> BlobResponse | None:
|
|
288
|
+
"""Decode a V1 blob response message."""
|
|
289
|
+
if message.protocol != RoborockMessageProtocol.MAP_RESPONSE:
|
|
290
|
+
return None
|
|
291
|
+
payload = message.payload
|
|
292
|
+
if not payload or not payload.startswith(b"ROBOROCK") or len(payload) < 24:
|
|
293
|
+
return None
|
|
294
|
+
request_id = int.from_bytes(payload[8:12], "little")
|
|
295
|
+
header_size = int.from_bytes(payload[16:18], "little")
|
|
296
|
+
payload_size = int.from_bytes(payload[20:24], "little")
|
|
297
|
+
end_offset = header_size + payload_size
|
|
298
|
+
if header_size < 24 or end_offset > len(payload):
|
|
299
|
+
raise RoborockException("Invalid V1 blob response format")
|
|
300
|
+
try:
|
|
301
|
+
data = Utils.decompress(payload[header_size:end_offset])
|
|
302
|
+
except (gzip.BadGzipFile, EOFError, zlib.error) as err:
|
|
303
|
+
raise RoborockException("Failed to decode blob message payload") from err
|
|
304
|
+
return BlobResponse(request_id=request_id, data=data)
|
|
305
|
+
|
|
306
|
+
return _decode_blob_response
|
|
307
|
+
|
|
308
|
+
|
|
264
309
|
def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse | None]:
|
|
265
310
|
"""Create a decoder for V1 map response messages."""
|
|
266
311
|
|
|
@@ -117,6 +117,7 @@ class RoborockCommand(str, Enum):
|
|
|
117
117
|
GET_NETWORK_INFO = "get_network_info"
|
|
118
118
|
GET_OFFLINE_MAP_STATUS = "get_offline_map_status"
|
|
119
119
|
GET_PERSIST = "get_persist_map"
|
|
120
|
+
GET_PHOTO = "get_photo"
|
|
120
121
|
GET_PROP = "get_prop"
|
|
121
122
|
GET_RANDOM_PKEY = "get_random_pkey"
|
|
122
123
|
GET_RECOVER_MAP = "get_recover_map"
|
|
@@ -12,6 +12,7 @@ from unittest.mock import AsyncMock, MagicMock
|
|
|
12
12
|
|
|
13
13
|
from roborock.devices.transport.channel import Channel
|
|
14
14
|
from roborock.mqtt.health_manager import HealthManager
|
|
15
|
+
from roborock.mqtt.session import MqttQos
|
|
15
16
|
from roborock.protocols.v1_protocol import LocalProtocolVersion
|
|
16
17
|
from roborock.roborock_message import RoborockMessage
|
|
17
18
|
|
|
@@ -83,6 +84,7 @@ class FakeChannel(Channel):
|
|
|
83
84
|
self.close = MagicMock(side_effect=self._close)
|
|
84
85
|
|
|
85
86
|
self.protocol_version = LocalProtocolVersion.V1
|
|
87
|
+
|
|
86
88
|
self.restart = AsyncMock()
|
|
87
89
|
self.health_manager = HealthManager(self.restart)
|
|
88
90
|
|
|
@@ -102,10 +104,13 @@ class FakeChannel(Channel):
|
|
|
102
104
|
"""Return true if locally connected."""
|
|
103
105
|
return self._is_connected and self._is_local
|
|
104
106
|
|
|
105
|
-
async def _publish(self, message: RoborockMessage) -> None:
|
|
107
|
+
async def _publish(self, message: RoborockMessage, qos: MqttQos = MqttQos.AT_MOST_ONCE) -> None:
|
|
106
108
|
"""Default publish implementation.
|
|
107
109
|
|
|
108
110
|
Records the message in ``published_messages`` and executes ``publish_handler``.
|
|
111
|
+
|
|
112
|
+
The ``qos`` parameter is accepted for compatibility with
|
|
113
|
+
``MqttChannel.publish`` but not simulated by the fake channel.
|
|
109
114
|
"""
|
|
110
115
|
self.published_messages.append(message)
|
|
111
116
|
if self.publish_side_effect:
|
|
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-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/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
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/button_light.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/child_lock.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/clean_history.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/command.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/consumable.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/dust_collection.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q10/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/b01/q7/map_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.35.0 → python_roborock-5.37.0}/roborock/devices/transport/local_channel.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
|