python-roborock 3.19.0__tar.gz → 3.20.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 (101) hide show
  1. {python_roborock-3.19.0 → python_roborock-3.20.0}/PKG-INFO +1 -1
  2. {python_roborock-3.19.0 → python_roborock-3.20.0}/pyproject.toml +1 -1
  3. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/callbacks.py +1 -1
  4. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +8 -1
  5. python_roborock-3.20.0/roborock/devices/b01_channel.py +129 -0
  6. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/mqtt_channel.py +1 -1
  7. python_roborock-3.20.0/roborock/devices/traits/b01/q7/__init__.py +115 -0
  8. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocol.py +2 -2
  9. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/b01_protocol.py +4 -1
  10. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/roborock_typing.py +54 -1
  11. python_roborock-3.19.0/roborock/devices/b01_channel.py +0 -77
  12. python_roborock-3.19.0/roborock/devices/traits/b01/q7/__init__.py +0 -33
  13. {python_roborock-3.19.0 → python_roborock-3.20.0}/.gitignore +0 -0
  14. {python_roborock-3.19.0 → python_roborock-3.20.0}/LICENSE +0 -0
  15. {python_roborock-3.19.0 → python_roborock-3.20.0}/README.md +0 -0
  16. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/__init__.py +0 -0
  17. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/api.py +0 -0
  18. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/broadcast_protocol.py +0 -0
  19. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/cli.py +0 -0
  20. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/cloud_api.py +0 -0
  21. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/command_cache.py +0 -0
  22. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/const.py +0 -0
  23. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/__init__.py +0 -0
  24. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/__init__.py +0 -0
  25. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  26. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  27. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q7/__init__.py +0 -0
  28. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  29. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/code_mappings.py +0 -0
  30. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/containers.py +0 -0
  31. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/dyad/__init__.py +0 -0
  32. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  33. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/dyad/dyad_containers.py +0 -0
  34. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/__init__.py +0 -0
  35. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  36. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  37. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/v1_containers.py +0 -0
  38. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/zeo/__init__.py +0 -0
  39. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  40. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/zeo/zeo_containers.py +0 -0
  41. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/device_features.py +0 -0
  42. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/README.md +0 -0
  43. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/__init__.py +0 -0
  44. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/a01_channel.py +0 -0
  45. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/cache.py +0 -0
  46. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/channel.py +0 -0
  47. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/device.py +0 -0
  48. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/device_manager.py +0 -0
  49. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/file_cache.py +0 -0
  50. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/local_channel.py +0 -0
  51. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/__init__.py +0 -0
  52. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/a01/__init__.py +0 -0
  53. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/b01/__init__.py +0 -0
  54. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  55. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/traits_mixin.py +0 -0
  56. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/__init__.py +0 -0
  57. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  58. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  59. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/command.py +0 -0
  60. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/common.py +0 -0
  61. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  62. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/device_features.py +0 -0
  63. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  64. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  65. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  66. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/home.py +0 -0
  67. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/led_status.py +0 -0
  68. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/map_content.py +0 -0
  69. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/maps.py +0 -0
  70. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/network_info.py +0 -0
  71. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/rooms.py +0 -0
  72. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/routines.py +0 -0
  73. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  74. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/status.py +0 -0
  75. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  76. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/volume.py +0 -0
  77. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  78. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/v1_channel.py +0 -0
  79. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/diagnostics.py +0 -0
  80. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/exceptions.py +0 -0
  81. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/map/__init__.py +0 -0
  82. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/map/map_parser.py +0 -0
  83. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/__init__.py +0 -0
  84. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/health_manager.py +0 -0
  85. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/roborock_session.py +0 -0
  86. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/session.py +0 -0
  87. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/__init__.py +0 -0
  88. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/a01_protocol.py +0 -0
  89. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/v1_protocol.py +0 -0
  90. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/py.typed +0 -0
  91. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/roborock_future.py +0 -0
  92. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/roborock_message.py +0 -0
  93. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/util.py +0 -0
  94. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/__init__.py +0 -0
  95. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  96. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  97. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  98. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_a01_apis/__init__.py +0 -0
  99. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  100. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  101. {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 3.19.0
3
+ Version: 3.20.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 = "3.19.0"
3
+ version = "3.20.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"
@@ -121,7 +121,7 @@ def decoder_callback(
121
121
 
122
122
  def wrapper(data: K) -> None:
123
123
  if not (messages := decoder(data)):
124
- logger.warning("Failed to decode message: %s", data)
124
+ logger.debug("Failed to decode message: %s", data)
125
125
  return
126
126
  for message in messages:
127
127
  logger.debug("Decoded message: %s", message)
@@ -24,7 +24,6 @@ class SCWindMapping(RoborockModeEnum):
24
24
  STANDARD = ("balanced", 1)
25
25
  STRONG = ("turbo", 2)
26
26
  SUPER_STRONG = ("max", 3)
27
- MAX = ("max_plus", 4)
28
27
 
29
28
 
30
29
  class WaterLevelMapping(RoborockModeEnum):
@@ -50,6 +49,14 @@ class CleanRepeatMapping(RoborockModeEnum):
50
49
  TWICE = ("twice", 1)
51
50
 
52
51
 
52
+ class SCDeviceCleanParam(RoborockModeEnum):
53
+ """Maps the control values for cleaning tasks."""
54
+
55
+ STOP = ("stop", 0)
56
+ START = ("start", 1)
57
+ PAUSE = ("pause", 2)
58
+
59
+
53
60
  class WorkModeMapping(RoborockModeEnum):
54
61
  """Maps the detailed work modes of the robot."""
55
62
 
@@ -0,0 +1,129 @@
1
+ """Thin wrapper around the MQTT channel for Roborock B01 devices."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ from typing import Any
9
+
10
+ from roborock.exceptions import RoborockException
11
+ from roborock.protocols.b01_protocol import (
12
+ CommandType,
13
+ ParamsType,
14
+ decode_rpc_response,
15
+ encode_mqtt_payload,
16
+ )
17
+ from roborock.roborock_message import RoborockMessage
18
+ from roborock.util import get_next_int
19
+
20
+ from .mqtt_channel import MqttChannel
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+ _TIMEOUT = 10.0
24
+
25
+
26
+ async def send_decoded_command(
27
+ mqtt_channel: MqttChannel,
28
+ dps: int,
29
+ command: CommandType,
30
+ params: ParamsType,
31
+ ) -> dict[str, Any] | None:
32
+ """Send a command on the MQTT channel and get a decoded response."""
33
+ msg_id = str(get_next_int(100000000000, 999999999999))
34
+ _LOGGER.debug(
35
+ "Sending B01 MQTT command: dps=%s method=%s msg_id=%s params=%s",
36
+ dps,
37
+ command,
38
+ msg_id,
39
+ params,
40
+ )
41
+ roborock_message = encode_mqtt_payload(dps, command, params, msg_id)
42
+ future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
43
+
44
+ def find_response(response_message: RoborockMessage) -> None:
45
+ """Handle incoming messages and resolve the future."""
46
+ try:
47
+ decoded_dps = decode_rpc_response(response_message)
48
+ except RoborockException as ex:
49
+ _LOGGER.debug(
50
+ "Failed to decode B01 RPC response (expecting method=%s msg_id=%s): %s: %s",
51
+ command,
52
+ msg_id,
53
+ response_message,
54
+ ex,
55
+ )
56
+ return
57
+
58
+ for dps_value in decoded_dps.values():
59
+ # valid responses are JSON strings wrapped in the dps value
60
+ if not isinstance(dps_value, str):
61
+ _LOGGER.debug("Received unexpected response: %s", dps_value)
62
+ continue
63
+
64
+ try:
65
+ inner = json.loads(dps_value)
66
+ except (json.JSONDecodeError, TypeError):
67
+ _LOGGER.debug("Received unexpected response: %s", dps_value)
68
+ continue
69
+
70
+ if isinstance(inner, dict) and inner.get("msgId") == msg_id:
71
+ _LOGGER.debug("Received query response: %s", inner)
72
+ # Check for error code (0 = success, non-zero = error)
73
+ code = inner.get("code", 0)
74
+ if code != 0:
75
+ error_msg = (
76
+ f"B01 command failed with code {code} "
77
+ f"(method={command}, msg_id={msg_id}, dps={dps}, params={params})"
78
+ )
79
+ _LOGGER.debug("B01 error response: %s", error_msg)
80
+ if not future.done():
81
+ future.set_exception(RoborockException(error_msg))
82
+ return
83
+ data = inner.get("data")
84
+ # All get commands should be dicts
85
+ if command.endswith(".get") and not isinstance(data, dict):
86
+ if not future.done():
87
+ future.set_exception(
88
+ RoborockException(
89
+ f"Unexpected data type for response "
90
+ f"(method={command}, msg_id={msg_id}, dps={dps}, params={params})"
91
+ )
92
+ )
93
+ return
94
+ if not future.done():
95
+ future.set_result(data)
96
+
97
+ unsub = await mqtt_channel.subscribe(find_response)
98
+
99
+ _LOGGER.debug("Sending MQTT message: %s", roborock_message)
100
+ try:
101
+ await mqtt_channel.publish(roborock_message)
102
+ return await asyncio.wait_for(future, timeout=_TIMEOUT)
103
+ except TimeoutError as ex:
104
+ raise RoborockException(
105
+ f"B01 command timed out after {_TIMEOUT}s (method={command}, msg_id={msg_id}, dps={dps}, params={params})"
106
+ ) from ex
107
+ except RoborockException as ex:
108
+ _LOGGER.warning(
109
+ "Error sending B01 decoded command (method=%s msg_id=%s dps=%s params=%s): %s",
110
+ command,
111
+ msg_id,
112
+ dps,
113
+ params,
114
+ ex,
115
+ )
116
+ raise
117
+
118
+ except Exception as ex:
119
+ _LOGGER.exception(
120
+ "Error sending B01 decoded command (method=%s msg_id=%s dps=%s params=%s): %s",
121
+ command,
122
+ msg_id,
123
+ dps,
124
+ params,
125
+ ex,
126
+ )
127
+ raise
128
+ finally:
129
+ unsub()
@@ -87,7 +87,7 @@ class MqttChannel(Channel):
87
87
  try:
88
88
  return await self._mqtt_session.publish(self._publish_topic, encoded_msg)
89
89
  except MqttSessionException as e:
90
- self._logger.exception("Error publishing MQTT message: %s", e)
90
+ self._logger.debug("Error publishing MQTT message: %s", e)
91
91
  raise RoborockException(f"Failed to publish MQTT message: {e}") from e
92
92
 
93
93
  async def restart(self) -> None:
@@ -0,0 +1,115 @@
1
+ """Traits for Q7 B01 devices.
2
+ Potentially other devices may fall into this category in the future."""
3
+
4
+ from typing import Any
5
+
6
+ from roborock import B01Props
7
+ from roborock.data.b01_q7.b01_q7_code_mappings import (
8
+ CleanTaskTypeMapping,
9
+ SCDeviceCleanParam,
10
+ SCWindMapping,
11
+ WaterLevelMapping,
12
+ )
13
+ from roborock.devices.b01_channel import send_decoded_command
14
+ from roborock.devices.mqtt_channel import MqttChannel
15
+ from roborock.devices.traits import Trait
16
+ from roborock.roborock_message import RoborockB01Props
17
+ from roborock.roborock_typing import RoborockB01Q7Methods
18
+
19
+ __all__ = [
20
+ "Q7PropertiesApi",
21
+ ]
22
+
23
+
24
+ class Q7PropertiesApi(Trait):
25
+ """API for interacting with B01 devices."""
26
+
27
+ def __init__(self, channel: MqttChannel) -> None:
28
+ """Initialize the B01Props API."""
29
+ self._channel = channel
30
+
31
+ async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
32
+ """Query the device for the values of the given Q7 properties."""
33
+ result = await self.send(
34
+ RoborockB01Q7Methods.GET_PROP,
35
+ {"property": props},
36
+ )
37
+ if not isinstance(result, dict):
38
+ raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
39
+ return B01Props.from_dict(result)
40
+
41
+ async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
42
+ """Set a property on the device."""
43
+ await self.send(
44
+ command=RoborockB01Q7Methods.SET_PROP,
45
+ params={prop: value},
46
+ )
47
+
48
+ async def set_fan_speed(self, fan_speed: SCWindMapping) -> None:
49
+ """Set the fan speed (wind)."""
50
+ await self.set_prop(RoborockB01Props.WIND, fan_speed.code)
51
+
52
+ async def set_water_level(self, water_level: WaterLevelMapping) -> None:
53
+ """Set the water level (water)."""
54
+ await self.set_prop(RoborockB01Props.WATER, water_level.code)
55
+
56
+ async def start_clean(self) -> None:
57
+ """Start cleaning."""
58
+ await self.send(
59
+ command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
60
+ params={
61
+ "clean_type": CleanTaskTypeMapping.ALL.code,
62
+ "ctrl_value": SCDeviceCleanParam.START.code,
63
+ "room_ids": [],
64
+ },
65
+ )
66
+
67
+ async def pause_clean(self) -> None:
68
+ """Pause cleaning."""
69
+ await self.send(
70
+ command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
71
+ params={
72
+ "clean_type": CleanTaskTypeMapping.ALL.code,
73
+ "ctrl_value": SCDeviceCleanParam.PAUSE.code,
74
+ "room_ids": [],
75
+ },
76
+ )
77
+
78
+ async def stop_clean(self) -> None:
79
+ """Stop cleaning."""
80
+ await self.send(
81
+ command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
82
+ params={
83
+ "clean_type": CleanTaskTypeMapping.ALL.code,
84
+ "ctrl_value": SCDeviceCleanParam.STOP.code,
85
+ "room_ids": [],
86
+ },
87
+ )
88
+
89
+ async def return_to_dock(self) -> None:
90
+ """Return to dock."""
91
+ await self.send(
92
+ command=RoborockB01Q7Methods.START_RECHARGE,
93
+ params={},
94
+ )
95
+
96
+ async def find_me(self) -> None:
97
+ """Locate the robot."""
98
+ await self.send(
99
+ command=RoborockB01Q7Methods.FIND_DEVICE,
100
+ params={},
101
+ )
102
+
103
+ async def send(self, command: RoborockB01Q7Methods, params: dict) -> Any:
104
+ """Send a command to the device."""
105
+ return await send_decoded_command(
106
+ self._channel,
107
+ dps=10000,
108
+ command=command,
109
+ params=params,
110
+ )
111
+
112
+
113
+ def create(channel: MqttChannel) -> Q7PropertiesApi:
114
+ """Create traits for B01 devices."""
115
+ return Q7PropertiesApi(channel)
@@ -276,7 +276,7 @@ class EncryptionAdapter(Construct):
276
276
  if context.version == b"A01":
277
277
  iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
278
278
  decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
279
- return decipher.encrypt(pad(obj, AES.block_size))
279
+ return decipher.encrypt(obj)
280
280
  elif context.version == b"B01":
281
281
  iv = md5hex(f"{context.random:08x}" + B01_HASH)[9:25]
282
282
  decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
@@ -300,7 +300,7 @@ class EncryptionAdapter(Construct):
300
300
  if context.version == b"A01":
301
301
  iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
302
302
  decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
303
- return unpad(decipher.decrypt(obj), AES.block_size)
303
+ return decipher.decrypt(obj)
304
304
  elif context.version == b"B01":
305
305
  iv = md5hex(f"{context.random:08x}" + B01_HASH)[9:25]
306
306
  decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
@@ -28,7 +28,10 @@ def encode_mqtt_payload(dps: int, command: CommandType, params: ParamsType, msg_
28
28
  dps: {
29
29
  "method": str(command),
30
30
  "msgId": msg_id,
31
- "params": params or [],
31
+ # Important: some B01 methods use an empty object `{}` (not `[]`) for
32
+ # "no params", and some setters legitimately send `0` which is falsy.
33
+ # Only default to `[]` when params is actually None.
34
+ "params": params if params is not None else [],
32
35
  }
33
36
  }
34
37
  }
@@ -280,6 +280,60 @@ class RoborockCommand(str, Enum):
280
280
  class RoborockB01Q7Methods(StrEnum):
281
281
  """Methods used by the Roborock Q7 model."""
282
282
 
283
+ # NOTE: In the Q7 Hermes dump these appear as suffixes and are also used
284
+ # with an "event." prefix at runtime (see `hermes/.../module_524.js`).
285
+ ADD_CLEAN_FAILED_POST = "add_clean_failed.post"
286
+ EVENT_ADD_CLEAN_FAILED_POST = "event.add_clean_failed.post"
287
+ CLEAN_FINISH_POST = "clean_finish.post"
288
+ EVENT_CLEAN_FINISH_POST = "event.clean_finish.post"
289
+ EVENT_BUILD_MAP_FINISH_POST = "event.BuildMapFinish.post"
290
+ EVENT_MAP_CHANGE_POST = "event.map_change.post"
291
+ EVENT_WORK_APPOINT_CLEAN_FAILED_POST = "event.work_appoint_clean_failed.post"
292
+ START_CLEAN_POST = "startClean.post"
293
+ ADD_ORDER = "service.add_order"
294
+ ADD_SWEEP_CLEAN = "service.add_sweep_clean"
295
+ ARRANGE_ROOM = "service.arrange_room"
296
+ DEL_MAP = "service.del_map"
297
+ DEL_ORDER = "service.del_order"
298
+ DEL_ORDERS = "service.del_orders"
299
+ DELETE_RECORD_BY_URL = "service.delete_record_by_url"
300
+ DOWNLOAD_VOICE_TYPE = "service.download_voice_type"
301
+ ERASE_PREFERENCE = "service.erase_preference"
302
+ FIND_DEVICE = "service.find_device"
303
+ GET_ROOM_ORDER = "service.get_room_order"
304
+ GET_VOICE_DOWNLOAD = "service.get_voice_download"
305
+ HELLO_WIKKA = "service.hello_wikka"
306
+ RENAME_MAP = "service.rename_map"
307
+ RENAME_ROOM = "service.rename_room"
308
+ RENAME_ROOMS = "service.rename_rooms"
309
+ REPLACE_MAP = "service.replace_map"
310
+ RESET_CONSUMABLE = "service.reset_consumable"
311
+ SAVE_CARPET = "service.save_carpet"
312
+ SAVE_RECOMMEND_FB = "service.save_recommend_fb"
313
+ SAVE_SILL = "service.save_sill"
314
+ SET_AREA_START = "service.set_area_start"
315
+ SET_AREAS_START = "service.set_areas_start"
316
+ SET_CUR_MAP = "service.set_cur_map"
317
+ SET_DIRECTION = "service.set_direction"
318
+ SET_GLOBAL_SORT = "service.set_global_sort"
319
+ SET_MAP_HIDE = "service.set_map_hide"
320
+ SET_MULTI_ROOM_MATERIAL = "service.set_multi_room_material"
321
+ SET_POINT_CLEAN = "service.set_point_clean"
322
+ SET_PREFERENCE = "service.set_preference"
323
+ SET_PREFERENCE_TYPE = "service.set_preference_type"
324
+ SET_QUIET_TIME = "service.set_quiet_time"
325
+ SET_ROOM_CLEAN = "service.set_room_clean"
326
+ SET_ROOM_ORDER = "service.set_room_order"
327
+ SET_VIRTUAL_WALL = "service.set_virtual_wall"
328
+ SET_ZONE_CLEAN = "service.set_zone_clean"
329
+ SET_ZONE_POINTS = "service.set_zone_points"
330
+ SPLIT_ROOM = "service.split_room"
331
+ START_EXPLORE = "service.start_explore"
332
+ START_POINT_CLEAN = "service.start_point_clean"
333
+ START_RECHARGE = "service.start_recharge"
334
+ STOP_RECHARGE = "service.stop_recharge"
335
+ UPLOAD_BY_MAPID = "service.upload_by_mapid"
336
+ UPLOAD_RECORD_BY_URL = "service.upload_record_by_url"
283
337
  GET_PROP = "prop.get"
284
338
  GET_MAP_LIST = "service.get_map_list"
285
339
  UPLOAD_BY_MAPTYPE = "service.upload_by_maptype"
@@ -287,7 +341,6 @@ class RoborockB01Q7Methods(StrEnum):
287
341
  GET_PREFERENCE = "service.get_preference"
288
342
  GET_RECORD_LIST = "service.get_record_list"
289
343
  GET_ORDER = "service.get_order"
290
- EVENT_ORDER_LIST_POST = "event.order_list.post"
291
344
  POST_PROP = "prop.post"
292
345
 
293
346
 
@@ -1,77 +0,0 @@
1
- """Thin wrapper around the MQTT channel for Roborock B01 devices."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import json
7
- import logging
8
- from typing import Any
9
-
10
- from roborock.exceptions import RoborockException
11
- from roborock.protocols.b01_protocol import (
12
- CommandType,
13
- ParamsType,
14
- decode_rpc_response,
15
- encode_mqtt_payload,
16
- )
17
- from roborock.roborock_message import RoborockMessage
18
- from roborock.util import get_next_int
19
-
20
- from .mqtt_channel import MqttChannel
21
-
22
- _LOGGER = logging.getLogger(__name__)
23
- _TIMEOUT = 10.0
24
-
25
-
26
- async def send_decoded_command(
27
- mqtt_channel: MqttChannel,
28
- dps: int,
29
- command: CommandType,
30
- params: ParamsType,
31
- ) -> dict[str, Any]:
32
- """Send a command on the MQTT channel and get a decoded response."""
33
- _LOGGER.debug("Sending MQTT command: %s", params)
34
- msg_id = str(get_next_int(100000000000, 999999999999))
35
- roborock_message = encode_mqtt_payload(dps, command, params, msg_id)
36
- future: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()
37
-
38
- def find_response(response_message: RoborockMessage) -> None:
39
- """Handle incoming messages and resolve the future."""
40
- try:
41
- decoded_dps = decode_rpc_response(response_message)
42
- except RoborockException as ex:
43
- _LOGGER.info("Failed to decode b01 message: %s: %s", response_message, ex)
44
- return
45
-
46
- for dps_value in decoded_dps.values():
47
- # valid responses are JSON strings wrapped in the dps value
48
- if not isinstance(dps_value, str):
49
- _LOGGER.debug("Received unexpected response: %s", dps_value)
50
- continue
51
-
52
- try:
53
- inner = json.loads(dps_value)
54
- except (json.JSONDecodeError, TypeError):
55
- _LOGGER.debug("Received unexpected response: %s", dps_value)
56
- continue
57
-
58
- if isinstance(inner, dict) and inner.get("msgId") == msg_id:
59
- _LOGGER.debug("Received query response: %s", inner)
60
- data = inner.get("data")
61
- if not future.done():
62
- if isinstance(data, dict):
63
- future.set_result(data)
64
- else:
65
- future.set_exception(RoborockException(f"Unexpected data type for response: {data}"))
66
-
67
- unsub = await mqtt_channel.subscribe(find_response)
68
-
69
- _LOGGER.debug("Sending MQTT message: %s", roborock_message)
70
- try:
71
- await mqtt_channel.publish(roborock_message)
72
- try:
73
- return await asyncio.wait_for(future, timeout=_TIMEOUT)
74
- except TimeoutError as ex:
75
- raise RoborockException(f"Command timed out after {_TIMEOUT}s") from ex
76
- finally:
77
- unsub()
@@ -1,33 +0,0 @@
1
- """Traits for Q7 B01 devices.
2
- Potentially other devices may fall into this category in the future."""
3
-
4
- from roborock import B01Props
5
- from roborock.devices.b01_channel import send_decoded_command
6
- from roborock.devices.mqtt_channel import MqttChannel
7
- from roborock.devices.traits import Trait
8
- from roborock.roborock_message import RoborockB01Props
9
- from roborock.roborock_typing import RoborockB01Q7Methods
10
-
11
- __all__ = [
12
- "Q7PropertiesApi",
13
- ]
14
-
15
-
16
- class Q7PropertiesApi(Trait):
17
- """API for interacting with B01 devices."""
18
-
19
- def __init__(self, channel: MqttChannel) -> None:
20
- """Initialize the B01Props API."""
21
- self._channel = channel
22
-
23
- async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
24
- """Query the device for the values of the given Q7 properties."""
25
- result = await send_decoded_command(
26
- self._channel, dps=10000, command=RoborockB01Q7Methods.GET_PROP, params={"property": props}
27
- )
28
- return B01Props.from_dict(result)
29
-
30
-
31
- def create(channel: MqttChannel) -> Q7PropertiesApi:
32
- """Create traits for B01 devices."""
33
- return Q7PropertiesApi(channel)