python-roborock 4.20.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 (99) hide show
  1. {python_roborock-4.20.0 → python_roborock-4.22.0}/PKG-INFO +1 -1
  2. {python_roborock-4.20.0 → python_roborock-4.22.0}/pyproject.toml +1 -1
  3. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/b01_q7/b01_q7_containers.py +27 -0
  4. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/v1/v1_code_mappings.py +0 -11
  5. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/v1/v1_containers.py +2 -2
  6. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/rpc/b01_q7_channel.py +76 -34
  7. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q7/__init__.py +22 -2
  8. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q7/clean_summary.py +2 -2
  9. python_roborock-4.22.0/roborock/devices/traits/b01/q7/map.py +59 -0
  10. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/__init__.py +6 -0
  11. python_roborock-4.22.0/roborock/devices/traits/v1/wash_towel_mode.py +48 -0
  12. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/protocols/b01_q7_protocol.py +1 -0
  13. python_roborock-4.20.0/roborock/devices/traits/v1/wash_towel_mode.py +0 -13
  14. {python_roborock-4.20.0 → python_roborock-4.22.0}/.gitignore +0 -0
  15. {python_roborock-4.20.0 → python_roborock-4.22.0}/LICENSE +0 -0
  16. {python_roborock-4.20.0 → python_roborock-4.22.0}/README.md +0 -0
  17. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/__init__.py +0 -0
  18. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/broadcast_protocol.py +0 -0
  19. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/callbacks.py +0 -0
  20. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/cli.py +0 -0
  21. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/const.py +0 -0
  22. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/__init__.py +0 -0
  23. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/b01_q10/__init__.py +0 -0
  24. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  25. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  26. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/b01_q7/__init__.py +0 -0
  27. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  28. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/code_mappings.py +0 -0
  29. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/containers.py +0 -0
  30. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/dyad/__init__.py +0 -0
  31. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  32. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/dyad/dyad_containers.py +0 -0
  33. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/v1/__init__.py +0 -0
  34. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  35. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/zeo/__init__.py +0 -0
  36. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  37. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/data/zeo/zeo_containers.py +0 -0
  38. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/device_features.py +0 -0
  39. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/README.md +0 -0
  40. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/__init__.py +0 -0
  41. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/cache.py +0 -0
  42. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/device.py +0 -0
  43. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/device_manager.py +0 -0
  44. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/file_cache.py +0 -0
  45. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/rpc/__init__.py +0 -0
  46. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/rpc/a01_channel.py +0 -0
  47. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  48. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/rpc/v1_channel.py +0 -0
  49. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/__init__.py +0 -0
  50. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/a01/__init__.py +0 -0
  51. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/__init__.py +0 -0
  52. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  53. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  54. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  55. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  56. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  57. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/traits_mixin.py +0 -0
  58. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  59. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  60. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/command.py +0 -0
  61. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/common.py +0 -0
  62. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  63. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/device_features.py +0 -0
  64. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  65. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  66. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  67. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/home.py +0 -0
  68. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/led_status.py +0 -0
  69. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/map_content.py +0 -0
  70. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/maps.py +0 -0
  71. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/network_info.py +0 -0
  72. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/rooms.py +0 -0
  73. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/routines.py +0 -0
  74. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  75. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/status.py +0 -0
  76. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  77. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/traits/v1/volume.py +0 -0
  78. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/transport/__init__.py +0 -0
  79. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/transport/channel.py +0 -0
  80. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/transport/local_channel.py +0 -0
  81. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  82. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/diagnostics.py +0 -0
  83. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/exceptions.py +0 -0
  84. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/map/__init__.py +0 -0
  85. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/map/map_parser.py +0 -0
  86. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/mqtt/__init__.py +0 -0
  87. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/mqtt/health_manager.py +0 -0
  88. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/mqtt/roborock_session.py +0 -0
  89. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/mqtt/session.py +0 -0
  90. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/protocol.py +0 -0
  91. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/protocols/__init__.py +0 -0
  92. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/protocols/a01_protocol.py +0 -0
  93. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  94. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/protocols/v1_protocol.py +0 -0
  95. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/py.typed +0 -0
  96. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/roborock_message.py +0 -0
  97. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/roborock_typing.py +0 -0
  98. {python_roborock-4.20.0 → python_roborock-4.22.0}/roborock/util.py +0 -0
  99. {python_roborock-4.20.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.20.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.20.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
  """
@@ -587,17 +587,6 @@ class RoborockDockDustCollectionModeCode(RoborockEnum):
587
587
  max = 4
588
588
 
589
589
 
590
- class RoborockDockWashTowelModeCode(RoborockEnum):
591
- """Describes the wash towel mode of the vacuum cleaner."""
592
-
593
- # TODO: Get the correct values for various different docks
594
- unknown = -9999
595
- light = 0
596
- balanced = 1
597
- deep = 2
598
- smart = 10
599
-
600
-
601
590
  class RoborockStateCode(RoborockEnum):
602
591
  unknown = 0
603
592
  starting = 1
@@ -39,6 +39,7 @@ from roborock.const import (
39
39
  from roborock.exceptions import RoborockException
40
40
 
41
41
  from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr
42
+ from .v1_clean_modes import WashTowelModes
42
43
  from .v1_code_mappings import (
43
44
  CleanFluidStatus,
44
45
  ClearWaterBoxStatus,
@@ -48,7 +49,6 @@ from .v1_code_mappings import (
48
49
  RoborockDockDustCollectionModeCode,
49
50
  RoborockDockErrorCode,
50
51
  RoborockDockTypeCode,
51
- RoborockDockWashTowelModeCode,
52
52
  RoborockErrorCode,
53
53
  RoborockFanPowerCode,
54
54
  RoborockFanSpeedP10,
@@ -750,7 +750,7 @@ class DustCollectionMode(RoborockBase):
750
750
 
751
751
  @dataclass
752
752
  class WashTowelMode(RoborockBase):
753
- wash_mode: RoborockDockWashTowelModeCode | None = None
753
+ wash_mode: WashTowelModes | None = None
754
754
 
755
755
 
756
756
  @dataclass
@@ -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)
@@ -234,6 +234,12 @@ class PropertiesApi(Trait):
234
234
  # Dock type also acts like a device feature for some traits.
235
235
  dock_type = await self._dock_type()
236
236
 
237
+ # Initialize traits with special arguments before the generic loop
238
+ if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_type):
239
+ wash_towel_mode = WashTowelModeTrait(self.device_features)
240
+ wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode) # type: ignore[assignment]
241
+ self.wash_towel_mode = wash_towel_mode
242
+
237
243
  # Dynamically create any traits that need to be populated
238
244
  for item in fields(self):
239
245
  if (trait := getattr(self, item.name, None)) is not None:
@@ -0,0 +1,48 @@
1
+ """Trait for wash towel mode."""
2
+
3
+ from functools import cached_property
4
+ from typing import Self
5
+
6
+ from roborock.data import WashTowelMode, WashTowelModes, get_wash_towel_modes
7
+ from roborock.device_features import is_wash_n_fill_dock
8
+ from roborock.devices.traits.v1 import common
9
+ from roborock.devices.traits.v1.device_features import DeviceFeaturesTrait
10
+ from roborock.roborock_typing import RoborockCommand
11
+
12
+
13
+ class WashTowelModeTrait(WashTowelMode, common.V1TraitMixin):
14
+ """Trait for wash towel mode."""
15
+
16
+ command = RoborockCommand.GET_WASH_TOWEL_MODE
17
+ requires_dock_type = is_wash_n_fill_dock
18
+
19
+ def __init__(
20
+ self,
21
+ device_feature_trait: DeviceFeaturesTrait,
22
+ ) -> None:
23
+ super().__init__()
24
+ self.device_feature_trait = device_feature_trait
25
+
26
+ def _parse_response(self, response: common.V1ResponseData) -> Self:
27
+ """Parse the response from the device into a WashTowelMode object."""
28
+ if isinstance(response, list):
29
+ response = response[0]
30
+ if isinstance(response, dict):
31
+ return WashTowelMode.from_dict(response)
32
+ raise ValueError(f"Unexpected wash towel mode format: {response!r}")
33
+
34
+ @cached_property
35
+ def wash_towel_mode_options(self) -> list[WashTowelModes]:
36
+ return get_wash_towel_modes(self.device_feature_trait)
37
+
38
+ async def set_wash_towel_mode(self, mode: WashTowelModes) -> None:
39
+ """Set the wash towel mode."""
40
+ await self.rpc_channel.send_command(RoborockCommand.SET_WASH_TOWEL_MODE, params={"wash_mode": mode.code})
41
+
42
+ async def start_wash(self) -> None:
43
+ """Start washing the mop."""
44
+ await self.rpc_channel.send_command(RoborockCommand.APP_START_WASH)
45
+
46
+ async def stop_wash(self) -> None:
47
+ """Stop washing the mop."""
48
+ await self.rpc_channel.send_command(RoborockCommand.APP_STOP_WASH)
@@ -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
 
@@ -1,13 +0,0 @@
1
- """Trait for wash towel mode."""
2
-
3
- from roborock.data import WashTowelMode
4
- from roborock.device_features import is_wash_n_fill_dock
5
- from roborock.devices.traits.v1 import common
6
- from roborock.roborock_typing import RoborockCommand
7
-
8
-
9
- class WashTowelModeTrait(WashTowelMode, common.V1TraitMixin):
10
- """Trait for wash towel mode."""
11
-
12
- command = RoborockCommand.GET_WASH_TOWEL_MODE
13
- requires_dock_type = is_wash_n_fill_dock