python-roborock 4.21.0__tar.gz → 4.22.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 (98) hide show
  1. {python_roborock-4.21.0 → python_roborock-4.22.0}/PKG-INFO +1 -1
  2. {python_roborock-4.21.0 → python_roborock-4.22.0}/pyproject.toml +1 -1
  3. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/b01_q7/b01_q7_containers.py +27 -0
  4. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/rpc/b01_q7_channel.py +76 -34
  5. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q7/__init__.py +22 -2
  6. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q7/clean_summary.py +2 -2
  7. python_roborock-4.22.0/roborock/devices/traits/b01/q7/map.py +59 -0
  8. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/protocols/b01_q7_protocol.py +1 -0
  9. {python_roborock-4.21.0 → python_roborock-4.22.0}/.gitignore +0 -0
  10. {python_roborock-4.21.0 → python_roborock-4.22.0}/LICENSE +0 -0
  11. {python_roborock-4.21.0 → python_roborock-4.22.0}/README.md +0 -0
  12. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/__init__.py +0 -0
  13. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/broadcast_protocol.py +0 -0
  14. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/callbacks.py +0 -0
  15. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/cli.py +0 -0
  16. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/const.py +0 -0
  17. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/__init__.py +0 -0
  18. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/b01_q10/__init__.py +0 -0
  19. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  20. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  21. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/b01_q7/__init__.py +0 -0
  22. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  23. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/code_mappings.py +0 -0
  24. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/containers.py +0 -0
  25. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/dyad/__init__.py +0 -0
  26. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  27. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/dyad/dyad_containers.py +0 -0
  28. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/v1/__init__.py +0 -0
  29. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  30. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  31. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/v1/v1_containers.py +0 -0
  32. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/zeo/__init__.py +0 -0
  33. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  34. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/data/zeo/zeo_containers.py +0 -0
  35. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/device_features.py +0 -0
  36. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/README.md +0 -0
  37. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/__init__.py +0 -0
  38. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/cache.py +0 -0
  39. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/device.py +0 -0
  40. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/device_manager.py +0 -0
  41. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/file_cache.py +0 -0
  42. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/rpc/__init__.py +0 -0
  43. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/rpc/a01_channel.py +0 -0
  44. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  45. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/rpc/v1_channel.py +0 -0
  46. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/__init__.py +0 -0
  47. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/a01/__init__.py +0 -0
  48. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/__init__.py +0 -0
  49. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  50. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  51. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  52. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  53. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  54. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/traits_mixin.py +0 -0
  55. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/__init__.py +0 -0
  56. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  57. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  58. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/command.py +0 -0
  59. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/common.py +0 -0
  60. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  61. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/device_features.py +0 -0
  62. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  63. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  64. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  65. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/home.py +0 -0
  66. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/led_status.py +0 -0
  67. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/map_content.py +0 -0
  68. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/maps.py +0 -0
  69. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/network_info.py +0 -0
  70. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/rooms.py +0 -0
  71. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/routines.py +0 -0
  72. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  73. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/status.py +0 -0
  74. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  75. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/volume.py +0 -0
  76. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  77. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/transport/__init__.py +0 -0
  78. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/transport/channel.py +0 -0
  79. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/transport/local_channel.py +0 -0
  80. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  81. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/diagnostics.py +0 -0
  82. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/exceptions.py +0 -0
  83. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/map/__init__.py +0 -0
  84. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/map/map_parser.py +0 -0
  85. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/mqtt/__init__.py +0 -0
  86. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/mqtt/health_manager.py +0 -0
  87. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/mqtt/roborock_session.py +0 -0
  88. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/mqtt/session.py +0 -0
  89. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/protocol.py +0 -0
  90. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/protocols/__init__.py +0 -0
  91. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/protocols/a01_protocol.py +0 -0
  92. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  93. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/protocols/v1_protocol.py +0 -0
  94. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/py.typed +0 -0
  95. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/roborock_message.py +0 -0
  96. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/roborock_typing.py +0 -0
  97. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/util.py +0 -0
  98. {python_roborock-4.21.0 → python_roborock-4.22.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 4.21.0
3
+ Version: 4.22.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
6
6
  Project-URL: Documentation, https://python-roborock.readthedocs.io/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "4.21.0"
3
+ version = "4.22.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"
@@ -75,6 +75,33 @@ class Recommend(RoborockBase):
75
75
  room_id: list[int] = field(default_factory=list)
76
76
 
77
77
 
78
+ @dataclass
79
+ class Q7MapListEntry(RoborockBase):
80
+ """Single map list entry returned by `service.get_map_list`."""
81
+
82
+ id: int | None = None
83
+ cur: bool | None = None
84
+
85
+
86
+ @dataclass
87
+ class Q7MapList(RoborockBase):
88
+ """Map list response returned by `service.get_map_list`."""
89
+
90
+ map_list: list[Q7MapListEntry] = field(default_factory=list)
91
+
92
+ @property
93
+ def current_map_id(self) -> int | None:
94
+ """Current map id, preferring the entry marked current."""
95
+ if not self.map_list:
96
+ return None
97
+
98
+ ordered = sorted(self.map_list, key=lambda entry: entry.cur or False, reverse=True)
99
+ first = next(iter(ordered), None)
100
+ if first is None or not isinstance(first.id, int):
101
+ return None
102
+ return first.id
103
+
104
+
78
105
  @dataclass
79
106
  class B01Props(RoborockBase):
80
107
  """
@@ -5,31 +5,67 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
- from typing import Any
8
+ from collections.abc import Callable
9
+ from typing import Any, TypeVar
9
10
 
10
11
  from roborock.devices.transport.mqtt_channel import MqttChannel
11
12
  from roborock.exceptions import RoborockException
12
- from roborock.protocols.b01_q7_protocol import (
13
- Q7RequestMessage,
14
- decode_rpc_response,
15
- encode_mqtt_payload,
16
- )
17
- from roborock.roborock_message import RoborockMessage
13
+ from roborock.protocols.b01_q7_protocol import B01_VERSION, Q7RequestMessage, decode_rpc_response, encode_mqtt_payload
14
+ from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
18
15
 
19
16
  _LOGGER = logging.getLogger(__name__)
20
17
  _TIMEOUT = 10.0
18
+ _T = TypeVar("_T")
19
+
20
+
21
+ def _matches_map_response(response_message: RoborockMessage, *, version: bytes | None) -> bytes | None:
22
+ """Return raw map payload bytes for matching MAP_RESPONSE messages."""
23
+ if (
24
+ response_message.protocol == RoborockMessageProtocol.MAP_RESPONSE
25
+ and response_message.payload
26
+ and response_message.version == version
27
+ ):
28
+ return response_message.payload
29
+ return None
30
+
31
+
32
+ async def _send_command(
33
+ mqtt_channel: MqttChannel,
34
+ request_message: Q7RequestMessage,
35
+ *,
36
+ response_matcher: Callable[[RoborockMessage], _T | None],
37
+ ) -> _T:
38
+ """Publish a B01 command and resolve on the first matching response."""
39
+ roborock_message = encode_mqtt_payload(request_message)
40
+ future: asyncio.Future[_T] = asyncio.get_running_loop().create_future()
41
+
42
+ def on_message(response_message: RoborockMessage) -> None:
43
+ if future.done():
44
+ return
45
+ try:
46
+ response = response_matcher(response_message)
47
+ except Exception as ex:
48
+ future.set_exception(ex)
49
+ return
50
+ if response is not None:
51
+ future.set_result(response)
52
+
53
+ unsub = await mqtt_channel.subscribe(on_message)
54
+ try:
55
+ await mqtt_channel.publish(roborock_message)
56
+ return await asyncio.wait_for(future, timeout=_TIMEOUT)
57
+ finally:
58
+ unsub()
21
59
 
22
60
 
23
61
  async def send_decoded_command(
24
62
  mqtt_channel: MqttChannel,
25
63
  request_message: Q7RequestMessage,
26
- ) -> dict[str, Any] | None:
64
+ ) -> Any:
27
65
  """Send a command on the MQTT channel and get a decoded response."""
28
66
  _LOGGER.debug("Sending B01 MQTT command: %s", request_message)
29
- roborock_message = encode_mqtt_payload(request_message)
30
- future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
31
67
 
32
- def find_response(response_message: RoborockMessage) -> None:
68
+ def find_response(response_message: RoborockMessage) -> Any | None:
33
69
  """Handle incoming messages and resolve the future."""
34
70
  try:
35
71
  decoded_dps = decode_rpc_response(response_message)
@@ -41,7 +77,7 @@ async def send_decoded_command(
41
77
  response_message,
42
78
  ex,
43
79
  )
44
- return
80
+ return None
45
81
  for dps_value in decoded_dps.values():
46
82
  # valid responses are JSON strings wrapped in the dps value
47
83
  if not isinstance(dps_value, str):
@@ -55,31 +91,23 @@ async def send_decoded_command(
55
91
  continue
56
92
  if isinstance(inner, dict) and inner.get("msgId") == str(request_message.msg_id):
57
93
  _LOGGER.debug("Received query response: %s", inner)
58
- # Check for error code (0 = success, non-zero = error)
59
94
  code = inner.get("code", 0)
60
95
  if code != 0:
61
96
  error_msg = f"B01 command failed with code {code} ({request_message})"
62
97
  _LOGGER.debug("B01 error response: %s", error_msg)
63
- if not future.done():
64
- future.set_exception(RoborockException(error_msg))
65
- return
98
+ raise RoborockException(error_msg)
66
99
  data = inner.get("data")
67
- # All get commands should be dicts
68
- if request_message.command.endswith(".get") and not isinstance(data, dict):
69
- if not future.done():
70
- future.set_exception(
71
- RoborockException(f"Unexpected data type for response {data} ({request_message})")
72
- )
73
- return
74
- if not future.done():
75
- future.set_result(data)
76
-
77
- unsub = await mqtt_channel.subscribe(find_response)
78
-
79
- _LOGGER.debug("Sending MQTT message: %s", roborock_message)
100
+ if request_message.command == "prop.get" and not isinstance(data, dict):
101
+ raise RoborockException(f"Unexpected data type for response {data} ({request_message})")
102
+ return data
103
+ return None
104
+
80
105
  try:
81
- await mqtt_channel.publish(roborock_message)
82
- return await asyncio.wait_for(future, timeout=_TIMEOUT)
106
+ return await _send_command(
107
+ mqtt_channel,
108
+ request_message,
109
+ response_matcher=find_response,
110
+ )
83
111
  except TimeoutError as ex:
84
112
  raise RoborockException(f"B01 command timed out after {_TIMEOUT}s ({request_message})") from ex
85
113
  except RoborockException as ex:
@@ -89,7 +117,6 @@ async def send_decoded_command(
89
117
  ex,
90
118
  )
91
119
  raise
92
-
93
120
  except Exception as ex:
94
121
  _LOGGER.exception(
95
122
  "Error sending B01 decoded command (%ss): %s",
@@ -97,5 +124,20 @@ async def send_decoded_command(
97
124
  ex,
98
125
  )
99
126
  raise
100
- finally:
101
- unsub()
127
+
128
+
129
+ async def send_map_command(mqtt_channel: MqttChannel, request_message: Q7RequestMessage) -> bytes:
130
+ """Send map upload command and wait for MAP_RESPONSE payload bytes.
131
+
132
+ This stays separate from ``send_decoded_command()`` because map uploads arrive as
133
+ raw ``MAP_RESPONSE`` payload bytes instead of a decoded RPC ``data`` payload.
134
+ """
135
+
136
+ try:
137
+ return await _send_command(
138
+ mqtt_channel,
139
+ request_message,
140
+ response_matcher=lambda response_message: _matches_map_response(response_message, version=B01_VERSION),
141
+ )
142
+ except TimeoutError as ex:
143
+ raise RoborockException(f"B01 map command timed out after {_TIMEOUT}s ({request_message})") from ex
@@ -4,6 +4,7 @@ Potentially other devices may fall into this category in the future."""
4
4
  from typing import Any
5
5
 
6
6
  from roborock import B01Props
7
+ from roborock.data import Q7MapList, Q7MapListEntry
7
8
  from roborock.data.b01_q7.b01_q7_code_mappings import (
8
9
  CleanPathPreferenceMapping,
9
10
  CleanRepeatMapping,
@@ -16,15 +17,19 @@ from roborock.data.b01_q7.b01_q7_code_mappings import (
16
17
  from roborock.devices.rpc.b01_q7_channel import send_decoded_command
17
18
  from roborock.devices.traits import Trait
18
19
  from roborock.devices.transport.mqtt_channel import MqttChannel
19
- from roborock.protocols.b01_q7_protocol import CommandType, ParamsType, Q7RequestMessage
20
+ from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, CommandType, ParamsType, Q7RequestMessage
20
21
  from roborock.roborock_message import RoborockB01Props
21
22
  from roborock.roborock_typing import RoborockB01Q7Methods
22
23
 
23
24
  from .clean_summary import CleanSummaryTrait
25
+ from .map import MapTrait
24
26
 
25
27
  __all__ = [
26
28
  "Q7PropertiesApi",
27
29
  "CleanSummaryTrait",
30
+ "MapTrait",
31
+ "Q7MapList",
32
+ "Q7MapListEntry",
28
33
  ]
29
34
 
30
35
 
@@ -34,10 +39,14 @@ class Q7PropertiesApi(Trait):
34
39
  clean_summary: CleanSummaryTrait
35
40
  """Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
36
41
 
42
+ map: MapTrait
43
+ """Trait for map list metadata + raw map payload retrieval."""
44
+
37
45
  def __init__(self, channel: MqttChannel) -> None:
38
46
  """Initialize the B01Props API."""
39
47
  self._channel = channel
40
48
  self.clean_summary = CleanSummaryTrait(channel)
49
+ self.map = MapTrait(channel)
41
50
 
42
51
  async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
43
52
  """Query the device for the values of the given Q7 properties."""
@@ -87,6 +96,17 @@ class Q7PropertiesApi(Trait):
87
96
  },
88
97
  )
89
98
 
99
+ async def clean_segments(self, segment_ids: list[int]) -> None:
100
+ """Start segment cleaning for the given ids (Q7 uses room ids)."""
101
+ await self.send(
102
+ command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
103
+ params={
104
+ "clean_type": CleanTaskTypeMapping.ROOM.code,
105
+ "ctrl_value": SCDeviceCleanParam.START.code,
106
+ "room_ids": segment_ids,
107
+ },
108
+ )
109
+
90
110
  async def pause_clean(self) -> None:
91
111
  """Pause cleaning."""
92
112
  await self.send(
@@ -127,7 +147,7 @@ class Q7PropertiesApi(Trait):
127
147
  """Send a command to the device."""
128
148
  return await send_decoded_command(
129
149
  self._channel,
130
- Q7RequestMessage(dps=10000, command=command, params=params),
150
+ Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
131
151
  )
132
152
 
133
153
 
@@ -13,7 +13,7 @@ from roborock.devices.rpc.b01_q7_channel import send_decoded_command
13
13
  from roborock.devices.traits import Trait
14
14
  from roborock.devices.transport.mqtt_channel import MqttChannel
15
15
  from roborock.exceptions import RoborockException
16
- from roborock.protocols.b01_q7_protocol import Q7RequestMessage
16
+ from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
17
17
  from roborock.roborock_typing import RoborockB01Q7Methods
18
18
 
19
19
  __all__ = [
@@ -50,7 +50,7 @@ class CleanSummaryTrait(CleanRecordSummary, Trait):
50
50
  """Fetch the raw device clean record list (`service.get_record_list`)."""
51
51
  result = await send_decoded_command(
52
52
  self._channel,
53
- Q7RequestMessage(dps=10000, command=RoborockB01Q7Methods.GET_RECORD_LIST, params={}),
53
+ Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_RECORD_LIST, params={}),
54
54
  )
55
55
 
56
56
  if not isinstance(result, dict):
@@ -0,0 +1,59 @@
1
+ """Map trait for B01 Q7 devices."""
2
+
3
+ import asyncio
4
+
5
+ from roborock.data import Q7MapList
6
+ from roborock.devices.rpc.b01_q7_channel import send_decoded_command, send_map_command
7
+ from roborock.devices.traits import Trait
8
+ from roborock.devices.transport.mqtt_channel import MqttChannel
9
+ from roborock.exceptions import RoborockException
10
+ from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
11
+ from roborock.roborock_typing import RoborockB01Q7Methods
12
+
13
+
14
+ class MapTrait(Q7MapList, Trait):
15
+ """Map retrieval + map metadata helpers for Q7 devices."""
16
+
17
+ def __init__(self, channel: MqttChannel) -> None:
18
+ super().__init__()
19
+ self._channel = channel
20
+ # Map uploads are serialized per-device to avoid response cross-wiring.
21
+ self._map_command_lock = asyncio.Lock()
22
+ self._loaded = False
23
+
24
+ async def refresh(self) -> None:
25
+ """Refresh cached map list metadata from the device."""
26
+ response = await send_decoded_command(
27
+ self._channel,
28
+ Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
29
+ )
30
+ if not isinstance(response, dict):
31
+ raise RoborockException(
32
+ f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
33
+ )
34
+
35
+ if (parsed := Q7MapList.from_dict(response)) is None:
36
+ raise RoborockException(f"Failed to decode map list response: {response!r}")
37
+
38
+ self.map_list = parsed.map_list
39
+ self._loaded = True
40
+
41
+ async def _get_map_payload(self, *, map_id: int) -> bytes:
42
+ """Fetch raw map payload bytes for the given map id."""
43
+ request = Q7RequestMessage(
44
+ dps=B01_Q7_DPS,
45
+ command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
46
+ params={"map_id": map_id},
47
+ )
48
+ async with self._map_command_lock:
49
+ return await send_map_command(self._channel, request)
50
+
51
+ async def get_current_map_payload(self) -> bytes:
52
+ """Fetch raw map payload bytes for the currently selected map."""
53
+ if not self._loaded:
54
+ await self.refresh()
55
+
56
+ map_id = self.current_map_id
57
+ if map_id is None:
58
+ raise RoborockException(f"Unable to determine map_id from map list response: {self!r}")
59
+ return await self._get_map_payload(map_id=map_id)
@@ -19,6 +19,7 @@ from roborock.util import get_next_int
19
19
  _LOGGER = logging.getLogger(__name__)
20
20
 
21
21
  B01_VERSION = b"B01"
22
+ B01_Q7_DPS = 10000
22
23
  CommandType = RoborockB01Q7Methods | str
23
24
  ParamsType = list | dict | int | None
24
25