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.
Files changed (66) hide show
  1. {python_roborock-2.52.0 → python_roborock-2.53.0}/PKG-INFO +1 -1
  2. {python_roborock-2.52.0 → python_roborock-2.53.0}/pyproject.toml +1 -1
  3. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/cli.py +46 -0
  4. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/device_manager.py +10 -1
  5. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/__init__.py +21 -4
  6. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/common.py +10 -0
  7. python_roborock-2.53.0/roborock/devices/traits/v1/map_content.py +49 -0
  8. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/v1_channel.py +7 -1
  9. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/v1_rpc_channel.py +34 -5
  10. python_roborock-2.53.0/roborock/map/__init__.py +7 -0
  11. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocols/v1_protocol.py +1 -1
  12. {python_roborock-2.52.0 → python_roborock-2.53.0}/LICENSE +0 -0
  13. {python_roborock-2.52.0 → python_roborock-2.53.0}/README.md +0 -0
  14. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/__init__.py +0 -0
  15. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/api.py +0 -0
  16. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/b01_containers.py +0 -0
  17. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/broadcast_protocol.py +0 -0
  18. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/callbacks.py +0 -0
  19. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/clean_modes.py +0 -0
  20. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/cloud_api.py +0 -0
  21. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/code_mappings.py +0 -0
  22. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/command_cache.py +0 -0
  23. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/const.py +0 -0
  24. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/containers.py +0 -0
  25. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/device_features.py +0 -0
  26. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/README.md +0 -0
  27. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/__init__.py +0 -0
  28. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/a01_channel.py +0 -0
  29. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/b01_channel.py +0 -0
  30. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/cache.py +0 -0
  31. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/channel.py +0 -0
  32. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/device.py +0 -0
  33. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/local_channel.py +0 -0
  34. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/mqtt_channel.py +0 -0
  35. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/__init__.py +0 -0
  36. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/a01/__init__.py +0 -0
  37. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/b01/__init__.py +0 -0
  38. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/traits_mixin.py +0 -0
  39. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  40. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  41. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  42. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/maps.py +0 -0
  43. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/rooms.py +0 -0
  44. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/status.py +0 -0
  45. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/devices/traits/v1/volume.py +0 -0
  46. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/exceptions.py +0 -0
  47. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/map/map_parser.py +0 -0
  48. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/mqtt/__init__.py +0 -0
  49. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/mqtt/roborock_session.py +0 -0
  50. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/mqtt/session.py +0 -0
  51. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocol.py +0 -0
  52. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocols/a01_protocol.py +0 -0
  53. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/protocols/b01_protocol.py +0 -0
  54. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/py.typed +0 -0
  55. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/roborock_future.py +0 -0
  56. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/roborock_message.py +0 -0
  57. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/roborock_typing.py +0 -0
  58. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/util.py +0 -0
  59. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/__init__.py +0 -0
  60. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  61. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  62. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  63. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_a01_apis/__init__.py +0 -0
  64. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  65. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  66. {python_roborock-2.52.0 → python_roborock-2.53.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.52.0
3
+ Version: 2.53.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Home-page: https://github.com/humbertogontijo/python-roborock
6
6
  License: GPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.52.0"
3
+ version = "2.53.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
6
6
  license = "GPL-3.0-only"
@@ -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(product, home_data, channel.rpc_channel, channel.mqtt_rpc_channel)
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, product: HomeDataProduct, home_data: HomeData, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel
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, home_data: HomeData, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel
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 = decode_rpc_response(response_message)
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
- _LOGGER.debug("Received response (request_id=%s): %s", self._name, decoded.request_id)
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
  )
@@ -0,0 +1,7 @@
1
+ """Module for Roborock map related data classes."""
2
+
3
+ from .map_parser import MapParserConfig, ParsedMapData
4
+
5
+ __all__ = [
6
+ "MapParserConfig",
7
+ ]
@@ -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 requested not made by this device, ignoring.")
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)