python-roborock 3.14.2__tar.gz → 3.15.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 (100) hide show
  1. {python_roborock-3.14.2 → python_roborock-3.15.0}/PKG-INFO +1 -1
  2. {python_roborock-3.14.2 → python_roborock-3.15.0}/pyproject.toml +1 -1
  3. python_roborock-3.15.0/roborock/data/b01_q7/b01_q7_containers.py +130 -0
  4. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/containers.py +1 -1
  5. python_roborock-3.15.0/roborock/devices/b01_channel.py +77 -0
  6. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/b01/q7/__init__.py +5 -3
  7. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/protocol.py +9 -6
  8. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/protocols/b01_protocol.py +5 -5
  9. python_roborock-3.14.2/roborock/data/b01_q7/b01_q7_containers.py +0 -130
  10. python_roborock-3.14.2/roborock/devices/b01_channel.py +0 -27
  11. {python_roborock-3.14.2 → python_roborock-3.15.0}/.gitignore +0 -0
  12. {python_roborock-3.14.2 → python_roborock-3.15.0}/LICENSE +0 -0
  13. {python_roborock-3.14.2 → python_roborock-3.15.0}/README.md +0 -0
  14. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/__init__.py +0 -0
  15. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/api.py +0 -0
  16. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/broadcast_protocol.py +0 -0
  17. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/callbacks.py +0 -0
  18. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/cli.py +0 -0
  19. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/cloud_api.py +0 -0
  20. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/command_cache.py +0 -0
  21. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/const.py +0 -0
  22. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/__init__.py +0 -0
  23. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/b01_q10/__init__.py +0 -0
  24. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  25. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  26. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/b01_q7/__init__.py +0 -0
  27. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  28. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/code_mappings.py +0 -0
  29. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/dyad/__init__.py +0 -0
  30. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  31. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/dyad/dyad_containers.py +0 -0
  32. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/v1/__init__.py +0 -0
  33. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  34. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  35. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/v1/v1_containers.py +0 -0
  36. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/zeo/__init__.py +0 -0
  37. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  38. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/data/zeo/zeo_containers.py +0 -0
  39. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/device_features.py +0 -0
  40. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/README.md +0 -0
  41. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/__init__.py +0 -0
  42. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/a01_channel.py +0 -0
  43. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/cache.py +0 -0
  44. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/channel.py +0 -0
  45. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/device.py +0 -0
  46. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/device_manager.py +0 -0
  47. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/file_cache.py +0 -0
  48. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/local_channel.py +0 -0
  49. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/mqtt_channel.py +0 -0
  50. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/__init__.py +0 -0
  51. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/a01/__init__.py +0 -0
  52. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/b01/__init__.py +0 -0
  53. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  54. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/traits_mixin.py +0 -0
  55. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/__init__.py +0 -0
  56. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  57. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  58. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/command.py +0 -0
  59. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/common.py +0 -0
  60. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  61. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/device_features.py +0 -0
  62. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  63. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  64. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  65. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/home.py +0 -0
  66. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/led_status.py +0 -0
  67. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/map_content.py +0 -0
  68. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/maps.py +0 -0
  69. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/network_info.py +0 -0
  70. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/rooms.py +0 -0
  71. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/routines.py +0 -0
  72. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  73. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/status.py +0 -0
  74. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  75. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/volume.py +0 -0
  76. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  77. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/devices/v1_channel.py +0 -0
  78. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/exceptions.py +0 -0
  79. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/map/__init__.py +0 -0
  80. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/map/map_parser.py +0 -0
  81. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/mqtt/__init__.py +0 -0
  82. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/mqtt/health_manager.py +0 -0
  83. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/mqtt/roborock_session.py +0 -0
  84. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/mqtt/session.py +0 -0
  85. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/protocols/__init__.py +0 -0
  86. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/protocols/a01_protocol.py +0 -0
  87. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/protocols/v1_protocol.py +0 -0
  88. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/py.typed +0 -0
  89. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/roborock_future.py +0 -0
  90. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/roborock_message.py +0 -0
  91. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/roborock_typing.py +0 -0
  92. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/util.py +0 -0
  93. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/version_1_apis/__init__.py +0 -0
  94. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  95. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  96. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  97. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/version_a01_apis/__init__.py +0 -0
  98. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  99. {python_roborock-3.14.2 → python_roborock-3.15.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  100. {python_roborock-3.14.2 → python_roborock-3.15.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.14.2
3
+ Version: 3.15.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.14.2"
3
+ version = "3.15.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"
@@ -0,0 +1,130 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from ..containers import RoborockBase
4
+ from .b01_q7_code_mappings import B01Fault, SCWindMapping, WorkModeMapping, WorkStatusMapping
5
+
6
+
7
+ @dataclass
8
+ class NetStatus(RoborockBase):
9
+ """Represents the network status of the device."""
10
+
11
+ rssi: str
12
+ loss: int
13
+ ping: int
14
+ ip: str
15
+ mac: str
16
+ ssid: str
17
+ frequency: int
18
+ bssid: str
19
+
20
+
21
+ @dataclass
22
+ class OrderTotal(RoborockBase):
23
+ """Represents the order total information."""
24
+
25
+ total: int
26
+ enable: int
27
+
28
+
29
+ @dataclass
30
+ class Privacy(RoborockBase):
31
+ """Represents the privacy settings of the device."""
32
+
33
+ ai_recognize: int
34
+ dirt_recognize: int
35
+ pet_recognize: int
36
+ carpet_turbo: int
37
+ carpet_avoid: int
38
+ carpet_show: int
39
+ map_uploads: int
40
+ ai_agent: int
41
+ ai_avoidance: int
42
+ record_uploads: int
43
+ along_floor: int
44
+ auto_upgrade: int
45
+
46
+
47
+ @dataclass
48
+ class PvCharging(RoborockBase):
49
+ """Represents the photovoltaic charging status."""
50
+
51
+ status: int
52
+ begin_time: int
53
+ end_time: int
54
+
55
+
56
+ @dataclass
57
+ class Recommend(RoborockBase):
58
+ """Represents cleaning recommendations."""
59
+
60
+ sill: int
61
+ wall: int
62
+ room_id: list[int] = field(default_factory=list)
63
+
64
+
65
+ @dataclass
66
+ class B01Props(RoborockBase):
67
+ """
68
+ Represents the complete properties and status for a Roborock B01 model.
69
+ This dataclass is generated based on the device's status JSON object.
70
+ """
71
+
72
+ status: WorkStatusMapping | None = None
73
+ fault: B01Fault | None = None
74
+ wind: SCWindMapping | None = None
75
+ water: int | None = None
76
+ mode: int | None = None
77
+ quantity: int | None = None
78
+ alarm: int | None = None
79
+ volume: int | None = None
80
+ hypa: int | None = None
81
+ main_brush: int | None = None
82
+ side_brush: int | None = None
83
+ mop_life: int | None = None
84
+ main_sensor: int | None = None
85
+ net_status: NetStatus | None = None
86
+ repeat_state: int | None = None
87
+ tank_state: int | None = None
88
+ sweep_type: int | None = None
89
+ clean_path_preference: int | None = None
90
+ cloth_state: int | None = None
91
+ time_zone: int | None = None
92
+ time_zone_info: str | None = None
93
+ language: int | None = None
94
+ cleaning_time: int | None = None
95
+ real_clean_time: int | None = None
96
+ cleaning_area: int | None = None
97
+ custom_type: int | None = None
98
+ sound: int | None = None
99
+ work_mode: WorkModeMapping | None = None
100
+ station_act: int | None = None
101
+ charge_state: int | None = None
102
+ current_map_id: int | None = None
103
+ map_num: int | None = None
104
+ dust_action: int | None = None
105
+ quiet_is_open: int | None = None
106
+ quiet_begin_time: int | None = None
107
+ quiet_end_time: int | None = None
108
+ clean_finish: int | None = None
109
+ voice_type: int | None = None
110
+ voice_type_version: int | None = None
111
+ order_total: OrderTotal | None = None
112
+ build_map: int | None = None
113
+ privacy: Privacy | None = None
114
+ dust_auto_state: int | None = None
115
+ dust_frequency: int | None = None
116
+ child_lock: int | None = None
117
+ multi_floor: int | None = None
118
+ map_save: int | None = None
119
+ light_mode: int | None = None
120
+ green_laser: int | None = None
121
+ dust_bag_used: int | None = None
122
+ order_save_mode: int | None = None
123
+ manufacturer: str | None = None
124
+ back_to_wash: int | None = None
125
+ charge_station_type: int | None = None
126
+ pv_cut_charge: int | None = None
127
+ pv_charging: PvCharging | None = None
128
+ serial_number: str | None = None
129
+ recommend: Recommend | None = None
130
+ add_sweep_status: int | None = None
@@ -238,8 +238,8 @@ class HomeDataDevice(RoborockBase):
238
238
  duid: str
239
239
  name: str
240
240
  local_key: str
241
- fv: str
242
241
  product_id: str
242
+ fv: str | None = None
243
243
  attribute: Any | None = None
244
244
  active_time: int | None = None
245
245
  runtime_env: Any | None = None
@@ -0,0 +1,77 @@
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,6 +1,7 @@
1
1
  """Traits for Q7 B01 devices.
2
2
  Potentially other devices may fall into this category in the future."""
3
3
 
4
+ from roborock import B01Props
4
5
  from roborock.devices.b01_channel import send_decoded_command
5
6
  from roborock.devices.mqtt_channel import MqttChannel
6
7
  from roborock.devices.traits import Trait
@@ -13,17 +14,18 @@ __all__ = [
13
14
 
14
15
 
15
16
  class Q7PropertiesApi(Trait):
16
- """API for interacting with Q7 B01 devices."""
17
+ """API for interacting with B01 devices."""
17
18
 
18
19
  def __init__(self, channel: MqttChannel) -> None:
19
20
  """Initialize the B01Props API."""
20
21
  self._channel = channel
21
22
 
22
- async def query_values(self, props: list[RoborockB01Props]) -> None:
23
+ async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
23
24
  """Query the device for the values of the given Q7 properties."""
24
- await send_decoded_command(
25
+ result = await send_decoded_command(
25
26
  self._channel, dps=10000, command=RoborockB01Q7Methods.GET_PROP, params={"property": props}
26
27
  )
28
+ return B01Props.from_dict(result)
27
29
 
28
30
 
29
31
  def create(channel: MqttChannel) -> Q7PropertiesApi:
@@ -276,12 +276,11 @@ 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
- f = decipher.encrypt(obj)
280
- return f
279
+ return decipher.encrypt(pad(obj, AES.block_size))
281
280
  elif context.version == b"B01":
282
281
  iv = md5hex(f"{context.random:08x}" + B01_HASH)[9:25]
283
282
  decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
284
- return decipher.encrypt(obj)
283
+ return decipher.encrypt(pad(obj, AES.block_size))
285
284
  elif context.version == b"L01":
286
285
  return Utils.encrypt_gcm_l01(
287
286
  plaintext=obj,
@@ -301,12 +300,11 @@ class EncryptionAdapter(Construct):
301
300
  if context.version == b"A01":
302
301
  iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
303
302
  decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
304
- f = decipher.decrypt(obj)
305
- return f
303
+ return unpad(decipher.decrypt(obj), AES.block_size)
306
304
  elif context.version == b"B01":
307
305
  iv = md5hex(f"{context.random:08x}" + B01_HASH)[9:25]
308
306
  decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
309
- return decipher.decrypt(obj)
307
+ return unpad(decipher.decrypt(obj), AES.block_size)
310
308
  elif context.version == b"L01":
311
309
  return Utils.decrypt_gcm_l01(
312
310
  payload=obj,
@@ -350,6 +348,11 @@ class PrefixedStruct(Struct):
350
348
  # Read remaining data to find a valid header
351
349
  data = stream.read()
352
350
 
351
+ if not data:
352
+ # EOF reached, let the parser fail naturally without logging
353
+ stream_seek(stream, current_pos, 0, path)
354
+ return super()._parse(stream, context, path)
355
+
353
356
  start_index = -1
354
357
  # Find the earliest occurrence of any valid version in a single pass
355
358
  for i in range(len(data) - 2):
@@ -13,7 +13,6 @@ from roborock.roborock_message import (
13
13
  RoborockMessage,
14
14
  RoborockMessageProtocol,
15
15
  )
16
- from roborock.util import get_next_int
17
16
 
18
17
  _LOGGER = logging.getLogger(__name__)
19
18
 
@@ -22,13 +21,13 @@ CommandType = RoborockB01Q7Methods | str
22
21
  ParamsType = list | dict | int | None
23
22
 
24
23
 
25
- def encode_mqtt_payload(dps: int, command: CommandType, params: ParamsType) -> RoborockMessage:
24
+ def encode_mqtt_payload(dps: int, command: CommandType, params: ParamsType, msg_id: str) -> RoborockMessage:
26
25
  """Encode payload for B01 commands over MQTT."""
27
26
  dps_data = {
28
27
  "dps": {
29
28
  dps: {
30
29
  "method": str(command),
31
- "msgId": str(get_next_int(100000000000, 999999999999)),
30
+ "msgId": msg_id,
32
31
  "params": params or [],
33
32
  }
34
33
  }
@@ -47,8 +46,9 @@ def decode_rpc_response(message: RoborockMessage) -> dict[int, Any]:
47
46
  raise RoborockException("Invalid B01 message format: missing payload")
48
47
  try:
49
48
  unpadded = unpad(message.payload, AES.block_size)
50
- except ValueError as err:
51
- raise RoborockException(f"Unable to unpad B01 payload: {err}")
49
+ except ValueError:
50
+ # It would be better to fail down the line.
51
+ unpadded = message.payload
52
52
 
53
53
  try:
54
54
  payload = json.loads(unpadded.decode())
@@ -1,130 +0,0 @@
1
- from dataclasses import dataclass, field
2
-
3
- from ..containers import RoborockBase
4
- from .b01_q7_code_mappings import B01Fault, SCWindMapping, WorkModeMapping, WorkStatusMapping
5
-
6
-
7
- @dataclass
8
- class NetStatus(RoborockBase):
9
- """Represents the network status of the device."""
10
-
11
- rssi: str
12
- loss: int
13
- ping: int
14
- ip: str
15
- mac: str
16
- ssid: str
17
- frequency: int
18
- bssid: str
19
-
20
-
21
- @dataclass
22
- class OrderTotal(RoborockBase):
23
- """Represents the order total information."""
24
-
25
- total: int
26
- enable: int
27
-
28
-
29
- @dataclass
30
- class Privacy(RoborockBase):
31
- """Represents the privacy settings of the device."""
32
-
33
- ai_recognize: int
34
- dirt_recognize: int
35
- pet_recognize: int
36
- carpet_turbo: int
37
- carpet_avoid: int
38
- carpet_show: int
39
- map_uploads: int
40
- ai_agent: int
41
- ai_avoidance: int
42
- record_uploads: int
43
- along_floor: int
44
- auto_upgrade: int
45
-
46
-
47
- @dataclass
48
- class PvCharging(RoborockBase):
49
- """Represents the photovoltaic charging status."""
50
-
51
- status: int
52
- begin_time: int
53
- end_time: int
54
-
55
-
56
- @dataclass
57
- class Recommend(RoborockBase):
58
- """Represents cleaning recommendations."""
59
-
60
- sill: int
61
- wall: int
62
- room_id: list[int] = field(default_factory=list)
63
-
64
-
65
- @dataclass
66
- class B01Props(RoborockBase):
67
- """
68
- Represents the complete properties and status for a Roborock B01 model.
69
- This dataclass is generated based on the device's status JSON object.
70
- """
71
-
72
- status: WorkStatusMapping
73
- fault: B01Fault
74
- wind: SCWindMapping
75
- water: int
76
- mode: int
77
- quantity: int
78
- alarm: int
79
- volume: int
80
- hypa: int
81
- main_brush: int
82
- side_brush: int
83
- mop_life: int
84
- main_sensor: int
85
- net_status: NetStatus
86
- repeat_state: int
87
- tank_state: int
88
- sweep_type: int
89
- clean_path_preference: int
90
- cloth_state: int
91
- time_zone: int
92
- time_zone_info: str
93
- language: int
94
- cleaning_time: int
95
- real_clean_time: int
96
- cleaning_area: int
97
- custom_type: int
98
- sound: int
99
- work_mode: WorkModeMapping
100
- station_act: int
101
- charge_state: int
102
- current_map_id: int
103
- map_num: int
104
- dust_action: int
105
- quiet_is_open: int
106
- quiet_begin_time: int
107
- quiet_end_time: int
108
- clean_finish: int
109
- voice_type: int
110
- voice_type_version: int
111
- order_total: OrderTotal
112
- build_map: int
113
- privacy: Privacy
114
- dust_auto_state: int
115
- dust_frequency: int
116
- child_lock: int
117
- multi_floor: int
118
- map_save: int
119
- light_mode: int
120
- green_laser: int
121
- dust_bag_used: int
122
- order_save_mode: int
123
- manufacturer: str
124
- back_to_wash: int
125
- charge_station_type: int
126
- pv_cut_charge: int
127
- pv_charging: PvCharging
128
- serial_number: str
129
- recommend: Recommend
130
- add_sweep_status: int
@@ -1,27 +0,0 @@
1
- """Thin wrapper around the MQTT channel for Roborock B01 devices."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
-
7
- from roborock.protocols.b01_protocol import (
8
- CommandType,
9
- ParamsType,
10
- encode_mqtt_payload,
11
- )
12
-
13
- from .mqtt_channel import MqttChannel
14
-
15
- _LOGGER = logging.getLogger(__name__)
16
-
17
-
18
- async def send_decoded_command(
19
- mqtt_channel: MqttChannel,
20
- dps: int,
21
- command: CommandType,
22
- params: ParamsType,
23
- ) -> None:
24
- """Send a command on the MQTT channel and get a decoded response."""
25
- _LOGGER.debug("Sending MQTT command: %s", params)
26
- roborock_message = encode_mqtt_payload(dps, command, params)
27
- await mqtt_channel.publish(roborock_message)