python-roborock 2.52.0__tar.gz → 2.53.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-2.52.0 → python_roborock-2.53.0}/PKG-INFO +1 -1
- {python_roborock-2.52.0 → python_roborock-2.53.0}/pyproject.toml +1 -1
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/cli.py +46 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/device_manager.py +10 -1
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/__init__.py +21 -4
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/common.py +10 -0
- python_roborock-2.53.0/roborock/devices/traits/v1/map_content.py +49 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/v1_channel.py +7 -1
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/v1_rpc_channel.py +34 -5
- python_roborock-2.53.0/roborock/map/__init__.py +7 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocols/v1_protocol.py +1 -1
- {python_roborock-2.52.0 → python_roborock-2.53.0}/LICENSE +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/README.md +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/api.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/b01_containers.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/callbacks.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/clean_modes.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/cloud_api.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/code_mappings.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/command_cache.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/const.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/containers.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/device_features.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/README.md +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/a01_channel.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/b01_channel.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/cache.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/channel.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/device.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/local_channel.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/mqtt_channel.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/exceptions.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocol.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocols/b01_protocol.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/py.typed +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/roborock_future.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/roborock_message.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/util.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/web_api.py +0 -0
|
@@ -48,6 +48,7 @@ from roborock.devices.device_manager import DeviceManager, create_device_manager
|
|
|
48
48
|
from roborock.devices.traits import Trait
|
|
49
49
|
from roborock.devices.traits.v1 import V1TraitMixin
|
|
50
50
|
from roborock.devices.traits.v1.consumeable import ConsumableAttribute
|
|
51
|
+
from roborock.devices.traits.v1.map_content import MapContentTrait
|
|
51
52
|
from roborock.protocol import MessageParser
|
|
52
53
|
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
|
|
53
54
|
from roborock.web_api import RoborockApiClient
|
|
@@ -451,6 +452,49 @@ async def maps(ctx, device_id: str):
|
|
|
451
452
|
await _display_v1_trait(context, device_id, lambda v1: v1.maps)
|
|
452
453
|
|
|
453
454
|
|
|
455
|
+
@session.command()
|
|
456
|
+
@click.option("--device_id", required=True)
|
|
457
|
+
@click.option("--output-file", required=True, help="Path to save the map image.")
|
|
458
|
+
@click.pass_context
|
|
459
|
+
@async_command
|
|
460
|
+
async def map_image(ctx, device_id: str, output_file: str):
|
|
461
|
+
"""Get device map image and save it to a file."""
|
|
462
|
+
context: RoborockContext = ctx.obj
|
|
463
|
+
trait: MapContentTrait = await _v1_trait(context, device_id, lambda v1: v1.map_content)
|
|
464
|
+
if trait.image_content:
|
|
465
|
+
with open(output_file, "wb") as f:
|
|
466
|
+
f.write(trait.image_content)
|
|
467
|
+
click.echo(f"Map image saved to {output_file}")
|
|
468
|
+
else:
|
|
469
|
+
click.echo("No map image content available.")
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@session.command()
|
|
473
|
+
@click.option("--device_id", required=True)
|
|
474
|
+
@click.option("--include_path", is_flag=True, default=False, help="Include path data in the output.")
|
|
475
|
+
@click.pass_context
|
|
476
|
+
@async_command
|
|
477
|
+
async def map_data(ctx, device_id: str, include_path: bool):
|
|
478
|
+
"""Get parsed map data as JSON."""
|
|
479
|
+
context: RoborockContext = ctx.obj
|
|
480
|
+
trait: MapContentTrait = await _v1_trait(context, device_id, lambda v1: v1.map_content)
|
|
481
|
+
if not trait.map_data:
|
|
482
|
+
click.echo("No parsed map data available.")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
# Pick some parts of the map data to display.
|
|
486
|
+
data_summary = {
|
|
487
|
+
"charger": trait.map_data.charger.as_dict() if trait.map_data.charger else None,
|
|
488
|
+
"image_size": trait.map_data.image.data.size if trait.map_data.image else None,
|
|
489
|
+
"vacuum_position": trait.map_data.vacuum_position.as_dict() if trait.map_data.vacuum_position else None,
|
|
490
|
+
"calibration": trait.map_data.calibration(),
|
|
491
|
+
"zones": [z.as_dict() for z in trait.map_data.zones or ()],
|
|
492
|
+
}
|
|
493
|
+
if include_path and trait.map_data.path:
|
|
494
|
+
data_summary["path"] = trait.map_data.path.as_dict()
|
|
495
|
+
click.echo(dump_json(data_summary))
|
|
496
|
+
|
|
497
|
+
|
|
454
498
|
@session.command()
|
|
455
499
|
@click.option("--device_id", required=True)
|
|
456
500
|
@click.pass_context
|
|
@@ -727,6 +771,8 @@ cli.add_command(clean_summary)
|
|
|
727
771
|
cli.add_command(volume)
|
|
728
772
|
cli.add_command(set_volume)
|
|
729
773
|
cli.add_command(maps)
|
|
774
|
+
cli.add_command(map_image)
|
|
775
|
+
cli.add_command(map_data)
|
|
730
776
|
cli.add_command(consumables)
|
|
731
777
|
cli.add_command(reset_consumable)
|
|
732
778
|
cli.add_command(rooms)
|
|
@@ -14,6 +14,7 @@ from roborock.containers import (
|
|
|
14
14
|
UserData,
|
|
15
15
|
)
|
|
16
16
|
from roborock.devices.device import RoborockDevice
|
|
17
|
+
from roborock.map.map_parser import MapParserConfig
|
|
17
18
|
from roborock.mqtt.roborock_session import create_lazy_mqtt_session
|
|
18
19
|
from roborock.mqtt.session import MqttSession
|
|
19
20
|
from roborock.protocol import create_mqtt_params
|
|
@@ -130,6 +131,7 @@ async def create_device_manager(
|
|
|
130
131
|
user_data: UserData,
|
|
131
132
|
home_data_api: HomeDataApi,
|
|
132
133
|
cache: Cache | None = None,
|
|
134
|
+
map_parser_config: MapParserConfig | None = None,
|
|
133
135
|
) -> DeviceManager:
|
|
134
136
|
"""Convenience function to create and initialize a DeviceManager.
|
|
135
137
|
|
|
@@ -149,7 +151,14 @@ async def create_device_manager(
|
|
|
149
151
|
match device.pv:
|
|
150
152
|
case DeviceVersion.V1:
|
|
151
153
|
channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache)
|
|
152
|
-
trait = v1.create(
|
|
154
|
+
trait = v1.create(
|
|
155
|
+
product,
|
|
156
|
+
home_data,
|
|
157
|
+
channel.rpc_channel,
|
|
158
|
+
channel.mqtt_rpc_channel,
|
|
159
|
+
channel.map_rpc_channel,
|
|
160
|
+
map_parser_config=map_parser_config,
|
|
161
|
+
)
|
|
153
162
|
case DeviceVersion.A01:
|
|
154
163
|
channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
|
|
155
164
|
trait = a01.create(product, channel)
|
|
@@ -6,11 +6,13 @@ from dataclasses import dataclass, field, fields
|
|
|
6
6
|
from roborock.containers import HomeData, HomeDataProduct
|
|
7
7
|
from roborock.devices.traits import Trait
|
|
8
8
|
from roborock.devices.v1_rpc_channel import V1RpcChannel
|
|
9
|
+
from roborock.map.map_parser import MapParserConfig
|
|
9
10
|
|
|
10
11
|
from .clean_summary import CleanSummaryTrait
|
|
11
12
|
from .common import V1TraitMixin
|
|
12
13
|
from .consumeable import ConsumableTrait
|
|
13
14
|
from .do_not_disturb import DoNotDisturbTrait
|
|
15
|
+
from .map_content import MapContentTrait
|
|
14
16
|
from .maps import MapsTrait
|
|
15
17
|
from .rooms import RoomsTrait
|
|
16
18
|
from .status import StatusTrait
|
|
@@ -26,6 +28,7 @@ __all__ = [
|
|
|
26
28
|
"CleanSummaryTrait",
|
|
27
29
|
"SoundVolumeTrait",
|
|
28
30
|
"MapsTrait",
|
|
31
|
+
"MapContentTrait",
|
|
29
32
|
"ConsumableTrait",
|
|
30
33
|
]
|
|
31
34
|
|
|
@@ -44,18 +47,25 @@ class PropertiesApi(Trait):
|
|
|
44
47
|
sound_volume: SoundVolumeTrait
|
|
45
48
|
rooms: RoomsTrait
|
|
46
49
|
maps: MapsTrait
|
|
50
|
+
map_content: MapContentTrait
|
|
47
51
|
consumables: ConsumableTrait
|
|
48
52
|
|
|
49
53
|
# In the future optional fields can be added below based on supported features
|
|
50
54
|
|
|
51
55
|
def __init__(
|
|
52
|
-
self,
|
|
56
|
+
self,
|
|
57
|
+
product: HomeDataProduct,
|
|
58
|
+
home_data: HomeData,
|
|
59
|
+
rpc_channel: V1RpcChannel,
|
|
60
|
+
mqtt_rpc_channel: V1RpcChannel,
|
|
61
|
+
map_rpc_channel: V1RpcChannel,
|
|
62
|
+
map_parser_config: MapParserConfig | None = None,
|
|
53
63
|
) -> None:
|
|
54
64
|
"""Initialize the V1TraitProps."""
|
|
55
65
|
self.status = StatusTrait(product)
|
|
56
66
|
self.rooms = RoomsTrait(home_data)
|
|
57
67
|
self.maps = MapsTrait(self.status)
|
|
58
|
-
|
|
68
|
+
self.map_content = MapContentTrait(map_parser_config)
|
|
59
69
|
# This is a hack to allow setting the rpc_channel on all traits. This is
|
|
60
70
|
# used so we can preserve the dataclass behavior when the values in the
|
|
61
71
|
# traits are updated, but still want to allow them to have a reference
|
|
@@ -68,12 +78,19 @@ class PropertiesApi(Trait):
|
|
|
68
78
|
# to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
|
|
69
79
|
if hasattr(trait, "mqtt_rpc_channel"):
|
|
70
80
|
trait._rpc_channel = mqtt_rpc_channel
|
|
81
|
+
elif hasattr(trait, "map_rpc_channel"):
|
|
82
|
+
trait._rpc_channel = map_rpc_channel
|
|
71
83
|
else:
|
|
72
84
|
trait._rpc_channel = rpc_channel
|
|
73
85
|
|
|
74
86
|
|
|
75
87
|
def create(
|
|
76
|
-
product: HomeDataProduct,
|
|
88
|
+
product: HomeDataProduct,
|
|
89
|
+
home_data: HomeData,
|
|
90
|
+
rpc_channel: V1RpcChannel,
|
|
91
|
+
mqtt_rpc_channel: V1RpcChannel,
|
|
92
|
+
map_rpc_channel: V1RpcChannel,
|
|
93
|
+
map_parser_config: MapParserConfig | None = None,
|
|
77
94
|
) -> PropertiesApi:
|
|
78
95
|
"""Create traits for V1 devices."""
|
|
79
|
-
return PropertiesApi(product, home_data, rpc_channel, mqtt_rpc_channel)
|
|
96
|
+
return PropertiesApi(product, home_data, rpc_channel, mqtt_rpc_channel, map_rpc_channel, map_parser_config)
|
|
@@ -133,3 +133,13 @@ def mqtt_rpc_channel(cls):
|
|
|
133
133
|
|
|
134
134
|
cls.mqtt_rpc_channel = True # type: ignore[attr-defined]
|
|
135
135
|
return wrapper
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def map_rpc_channel(cls):
|
|
139
|
+
"""Decorator to mark a function as cloud only using the map rpc format."""
|
|
140
|
+
|
|
141
|
+
def wrapper(*args, **kwargs):
|
|
142
|
+
return cls(*args, **kwargs)
|
|
143
|
+
|
|
144
|
+
cls.map_rpc_channel = True # type: ignore[attr-defined]
|
|
145
|
+
return wrapper
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Trait for fetching the map content from Roborock devices."""
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from vacuum_map_parser_base.map_data import MapData
|
|
6
|
+
|
|
7
|
+
from roborock.containers import RoborockBase
|
|
8
|
+
from roborock.devices.traits.v1 import common
|
|
9
|
+
from roborock.map.map_parser import MapParser, MapParserConfig
|
|
10
|
+
from roborock.roborock_typing import RoborockCommand
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class MapContent(RoborockBase):
|
|
17
|
+
"""Dataclass representing map content."""
|
|
18
|
+
|
|
19
|
+
image_content: bytes | None = None
|
|
20
|
+
"""The rendered image of the map in PNG format."""
|
|
21
|
+
|
|
22
|
+
map_data: MapData | None = None
|
|
23
|
+
"""The parsed map data which contains metadata for points on the map."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@common.map_rpc_channel
|
|
27
|
+
class MapContentTrait(MapContent, common.V1TraitMixin):
|
|
28
|
+
"""Trait for fetching the map content."""
|
|
29
|
+
|
|
30
|
+
command = RoborockCommand.GET_MAP_V1
|
|
31
|
+
|
|
32
|
+
def __init__(self, map_parser_config: MapParserConfig | None = None) -> None:
|
|
33
|
+
"""Initialize MapContentTrait."""
|
|
34
|
+
super().__init__()
|
|
35
|
+
self._map_parser = MapParser(map_parser_config or MapParserConfig())
|
|
36
|
+
|
|
37
|
+
def _parse_response(self, response: common.V1ResponseData) -> MapContent:
|
|
38
|
+
"""Parse the response from the device into a MapContentTrait instance."""
|
|
39
|
+
if not isinstance(response, bytes):
|
|
40
|
+
raise ValueError(f"Unexpected MapContentTrait response format: {type(response)}")
|
|
41
|
+
|
|
42
|
+
parsed_data = self._map_parser.parse(response)
|
|
43
|
+
if parsed_data is None:
|
|
44
|
+
raise ValueError("Failed to parse map data")
|
|
45
|
+
|
|
46
|
+
return MapContent(
|
|
47
|
+
image_content=parsed_data.image_content,
|
|
48
|
+
map_data=parsed_data.map_data,
|
|
49
|
+
)
|
|
@@ -27,6 +27,7 @@ from .v1_rpc_channel import (
|
|
|
27
27
|
PickFirstAvailable,
|
|
28
28
|
V1RpcChannel,
|
|
29
29
|
create_local_rpc_channel,
|
|
30
|
+
create_map_rpc_channel,
|
|
30
31
|
create_mqtt_rpc_channel,
|
|
31
32
|
)
|
|
32
33
|
|
|
@@ -80,6 +81,7 @@ class V1Channel(Channel):
|
|
|
80
81
|
self._combined_rpc_channel = PickFirstAvailable(
|
|
81
82
|
[lambda: self._local_rpc_channel, lambda: self._mqtt_rpc_channel]
|
|
82
83
|
)
|
|
84
|
+
self._map_rpc_channel = create_map_rpc_channel(mqtt_channel, security_data)
|
|
83
85
|
self._mqtt_unsub: Callable[[], None] | None = None
|
|
84
86
|
self._local_unsub: Callable[[], None] | None = None
|
|
85
87
|
self._callback: Callable[[RoborockMessage], None] | None = None
|
|
@@ -112,6 +114,11 @@ class V1Channel(Channel):
|
|
|
112
114
|
"""Return the MQTT RPC channel."""
|
|
113
115
|
return self._mqtt_rpc_channel
|
|
114
116
|
|
|
117
|
+
@property
|
|
118
|
+
def map_rpc_channel(self) -> V1RpcChannel:
|
|
119
|
+
"""Return the map RPC channel used for fetching map content."""
|
|
120
|
+
return self._map_rpc_channel
|
|
121
|
+
|
|
115
122
|
async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
|
|
116
123
|
"""Subscribe to all messages from the device.
|
|
117
124
|
|
|
@@ -132,7 +139,6 @@ class V1Channel(Channel):
|
|
|
132
139
|
|
|
133
140
|
# Start a background task to manage the local connection health. This
|
|
134
141
|
# happens independent of whether we were able to connect locally now.
|
|
135
|
-
_LOGGER.info("self._reconnect_task=%s", self._reconnect_task)
|
|
136
142
|
if self._reconnect_task is None:
|
|
137
143
|
loop = asyncio.get_running_loop()
|
|
138
144
|
self._reconnect_task = loop.create_task(self._background_reconnect())
|
|
@@ -6,6 +6,7 @@ a simple interface for sending commands and receiving responses over both MQTT
|
|
|
6
6
|
and local connections, preferring local when available.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
import asyncio
|
|
10
11
|
import logging
|
|
11
12
|
from collections.abc import Callable
|
|
@@ -15,10 +16,13 @@ from roborock.containers import RoborockBase
|
|
|
15
16
|
from roborock.exceptions import RoborockException
|
|
16
17
|
from roborock.protocols.v1_protocol import (
|
|
17
18
|
CommandType,
|
|
19
|
+
MapResponse,
|
|
18
20
|
ParamsType,
|
|
19
21
|
RequestMessage,
|
|
20
22
|
ResponseData,
|
|
23
|
+
ResponseMessage,
|
|
21
24
|
SecurityData,
|
|
25
|
+
create_map_response_decoder,
|
|
22
26
|
decode_rpc_response,
|
|
23
27
|
)
|
|
24
28
|
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
@@ -31,6 +35,7 @@ _TIMEOUT = 10.0
|
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
_T = TypeVar("_T", bound=RoborockBase)
|
|
38
|
+
_V = TypeVar("_V")
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
class V1RpcChannel(Protocol):
|
|
@@ -120,18 +125,20 @@ class PayloadEncodedV1RpcChannel(BaseV1RpcChannel):
|
|
|
120
125
|
name: str,
|
|
121
126
|
channel: MqttChannel | LocalChannel,
|
|
122
127
|
payload_encoder: Callable[[RequestMessage], RoborockMessage],
|
|
128
|
+
decoder: Callable[[RoborockMessage], ResponseMessage] | Callable[[RoborockMessage], MapResponse | None],
|
|
123
129
|
) -> None:
|
|
124
130
|
"""Initialize the channel with a raw channel and an encoder function."""
|
|
125
131
|
self._name = name
|
|
126
132
|
self._channel = channel
|
|
127
133
|
self._payload_encoder = payload_encoder
|
|
134
|
+
self._decoder = decoder
|
|
128
135
|
|
|
129
136
|
async def _send_raw_command(
|
|
130
137
|
self,
|
|
131
138
|
method: CommandType,
|
|
132
139
|
*,
|
|
133
140
|
params: ParamsType = None,
|
|
134
|
-
) -> ResponseData:
|
|
141
|
+
) -> ResponseData | bytes:
|
|
135
142
|
"""Send a command and return a parsed response RoborockBase type."""
|
|
136
143
|
request_message = RequestMessage(method, params=params)
|
|
137
144
|
_LOGGER.debug(
|
|
@@ -139,17 +146,19 @@ class PayloadEncodedV1RpcChannel(BaseV1RpcChannel):
|
|
|
139
146
|
)
|
|
140
147
|
message = self._payload_encoder(request_message)
|
|
141
148
|
|
|
142
|
-
future: asyncio.Future[ResponseData] = asyncio.Future()
|
|
149
|
+
future: asyncio.Future[ResponseData | bytes] = asyncio.Future()
|
|
143
150
|
|
|
144
151
|
def find_response(response_message: RoborockMessage) -> None:
|
|
145
152
|
try:
|
|
146
|
-
decoded =
|
|
153
|
+
decoded = self._decoder(response_message)
|
|
147
154
|
except RoborockException as ex:
|
|
148
155
|
_LOGGER.debug("Exception while decoding message (%s): %s", response_message, ex)
|
|
149
156
|
return
|
|
150
|
-
|
|
157
|
+
if decoded is None:
|
|
158
|
+
return
|
|
159
|
+
_LOGGER.debug("Received response (%s, request_id=%s)", self._name, decoded.request_id)
|
|
151
160
|
if decoded.request_id == request_message.request_id:
|
|
152
|
-
if decoded.api_error:
|
|
161
|
+
if isinstance(decoded, ResponseMessage) and decoded.api_error:
|
|
153
162
|
future.set_exception(decoded.api_error)
|
|
154
163
|
else:
|
|
155
164
|
future.set_result(decoded.data)
|
|
@@ -171,6 +180,7 @@ def create_mqtt_rpc_channel(mqtt_channel: MqttChannel, security_data: SecurityDa
|
|
|
171
180
|
"mqtt",
|
|
172
181
|
mqtt_channel,
|
|
173
182
|
lambda x: x.encode_message(RoborockMessageProtocol.RPC_REQUEST, security_data=security_data),
|
|
183
|
+
decode_rpc_response,
|
|
174
184
|
)
|
|
175
185
|
|
|
176
186
|
|
|
@@ -180,4 +190,23 @@ def create_local_rpc_channel(local_channel: LocalChannel) -> V1RpcChannel:
|
|
|
180
190
|
"local",
|
|
181
191
|
local_channel,
|
|
182
192
|
lambda x: x.encode_message(RoborockMessageProtocol.GENERAL_REQUEST),
|
|
193
|
+
decode_rpc_response,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def create_map_rpc_channel(
|
|
198
|
+
mqtt_channel: MqttChannel,
|
|
199
|
+
security_data: SecurityData,
|
|
200
|
+
) -> V1RpcChannel:
|
|
201
|
+
"""Create a V1 RPC channel that fetches map data.
|
|
202
|
+
|
|
203
|
+
This will prefer local channels when available, falling back to MQTT
|
|
204
|
+
channels if not. If neither is available, an exception will be raised
|
|
205
|
+
when trying to send a command.
|
|
206
|
+
"""
|
|
207
|
+
return PayloadEncodedV1RpcChannel(
|
|
208
|
+
"map",
|
|
209
|
+
mqtt_channel,
|
|
210
|
+
lambda x: x.encode_message(RoborockMessageProtocol.RPC_REQUEST, security_data=security_data),
|
|
211
|
+
create_map_response_decoder(security_data=security_data),
|
|
183
212
|
)
|
|
@@ -187,7 +187,7 @@ def create_map_response_decoder(security_data: SecurityData) -> Callable[[Roboro
|
|
|
187
187
|
header, body = message.payload[:24], message.payload[24:]
|
|
188
188
|
[endpoint, _, request_id, _] = struct.unpack("<8s8sH6s", header)
|
|
189
189
|
if not endpoint.decode().startswith(security_data.endpoint):
|
|
190
|
-
_LOGGER.debug("Received map response
|
|
190
|
+
_LOGGER.debug("Received map response not requested by this device, ignoring.")
|
|
191
191
|
return None
|
|
192
192
|
try:
|
|
193
193
|
decrypted = Utils.decrypt_cbc(body, security_data.nonce)
|
|
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-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/do_not_disturb.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
|
{python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|