python-roborock 5.25.0__tar.gz → 5.26.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 (119) hide show
  1. {python_roborock-5.25.0 → python_roborock-5.26.0}/PKG-INFO +1 -1
  2. {python_roborock-5.25.0 → python_roborock-5.26.0}/pyproject.toml +1 -1
  3. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +64 -0
  4. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/b01_q10/b01_q10_containers.py +7 -1
  5. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/device_manager.py +13 -2
  6. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/rpc/b01_q10_channel.py +51 -1
  7. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/rpc/b01_q7_channel.py +76 -18
  8. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/__init__.py +4 -5
  9. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/command.py +5 -6
  10. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/__init__.py +24 -17
  11. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/clean_summary.py +6 -8
  12. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/map.py +5 -7
  13. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/map_content.py +6 -9
  14. {python_roborock-5.25.0 → python_roborock-5.26.0}/.gitignore +0 -0
  15. {python_roborock-5.25.0 → python_roborock-5.26.0}/LICENSE +0 -0
  16. {python_roborock-5.25.0 → python_roborock-5.26.0}/README.md +0 -0
  17. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/__init__.py +0 -0
  18. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/broadcast_protocol.py +0 -0
  19. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/callbacks.py +0 -0
  20. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/cli.py +0 -0
  21. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/const.py +0 -0
  22. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/__init__.py +0 -0
  23. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/b01_q10/__init__.py +0 -0
  24. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/b01_q7/__init__.py +0 -0
  25. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  26. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  27. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/code_mappings.py +0 -0
  28. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/containers.py +0 -0
  29. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/dyad/__init__.py +0 -0
  30. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  31. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/dyad/dyad_containers.py +0 -0
  32. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/mower/__init__.py +0 -0
  33. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/mower/mower_containers.py +0 -0
  34. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/v1/__init__.py +0 -0
  35. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  36. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  37. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/v1/v1_containers.py +0 -0
  38. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/zeo/__init__.py +0 -0
  39. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  40. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/data/zeo/zeo_containers.py +0 -0
  41. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/device_features.py +0 -0
  42. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/README.md +0 -0
  43. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/__init__.py +0 -0
  44. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/cache.py +0 -0
  45. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/device.py +0 -0
  46. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/file_cache.py +0 -0
  47. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/rpc/__init__.py +0 -0
  48. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/rpc/a01_channel.py +0 -0
  49. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/rpc/v1_channel.py +0 -0
  50. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/__init__.py +0 -0
  51. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/a01/__init__.py +0 -0
  52. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/__init__.py +0 -0
  53. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/button_light.py +0 -0
  54. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
  55. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
  56. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  57. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/consumable.py +0 -0
  58. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
  59. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
  60. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/map.py +0 -0
  61. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/network_info.py +0 -0
  62. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
  63. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  64. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  65. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/volume.py +0 -0
  66. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/common.py +0 -0
  67. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/traits_mixin.py +0 -0
  68. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/__init__.py +0 -0
  69. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  70. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  71. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/command.py +0 -0
  72. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/common.py +0 -0
  73. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  74. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/device_features.py +0 -0
  75. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  76. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  77. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  78. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/home.py +0 -0
  79. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/led_status.py +0 -0
  80. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/map_content.py +0 -0
  81. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/maps.py +0 -0
  82. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/network_info.py +0 -0
  83. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/rooms.py +0 -0
  84. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/routines.py +0 -0
  85. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  86. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/status.py +0 -0
  87. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  88. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/volume.py +0 -0
  89. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  90. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/transport/__init__.py +0 -0
  91. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/transport/channel.py +0 -0
  92. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/transport/local_channel.py +0 -0
  93. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  94. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/diagnostics.py +0 -0
  95. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/exceptions.py +0 -0
  96. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/__init__.py +0 -0
  97. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/b01_grid_layers.py +0 -0
  98. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/b01_map_parser.py +0 -0
  99. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/b01_q10_map_parser.py +0 -0
  100. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/b01_q10_overlays.py +0 -0
  101. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/map_parser.py +0 -0
  102. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/proto/__init__.py +0 -0
  103. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/proto/b01_scmap.proto +0 -0
  104. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  105. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/mqtt/__init__.py +0 -0
  106. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/mqtt/health_manager.py +0 -0
  107. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/mqtt/roborock_session.py +0 -0
  108. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/mqtt/session.py +0 -0
  109. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/protocol.py +0 -0
  110. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/protocols/__init__.py +0 -0
  111. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/protocols/a01_protocol.py +0 -0
  112. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  113. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  114. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/protocols/v1_protocol.py +0 -0
  115. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/py.typed +0 -0
  116. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/roborock_message.py +0 -0
  117. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/roborock_typing.py +0 -0
  118. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/util.py +0 -0
  119. {python_roborock-5.25.0 → python_roborock-5.26.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 5.25.0
3
+ Version: 5.26.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/python-roborock/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 = "5.25.0"
3
+ version = "5.26.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"
@@ -289,6 +289,70 @@ class YXCarpetCleanType(RoborockModeEnum):
289
289
  CROSS = "cross", 3
290
290
 
291
291
 
292
+ class YXFault(RoborockModeEnum):
293
+ """Q10 (B01/ss07) ``dpFault`` (90) codes, from the ss07 fault spec.
294
+
295
+ ``dpFault`` is *overloaded*: several values are lifecycle/status rather than
296
+ errors (e.g. 400 = scheduled clean starting, 501 = returning to dock,
297
+ 502 = recharge). A non-zero fault is not necessarily a blocking error.
298
+ The converse also holds: the device can sit in its error state (``dpStatus``
299
+ 12) with ``dpFault`` still 0 (observed live with the dust-bin module
300
+ removed), so a ``None``/``NONE`` fault does not imply the absence of an
301
+ error condition.
302
+
303
+ These labels differ from the Q7 ``B01Fault`` for several shared numbers
304
+ (500, 501, 503, 569, 570) -- so this is a Q10-specific map, not a reuse of
305
+ ``B01Fault``. Codes marked "hw-confirmed" were observed firing live on a
306
+ physical ss07 in a context matching the label; the rest are from the spec
307
+ only, not yet observed live (single device).
308
+ """
309
+
310
+ UNKNOWN = "unknown", -1
311
+ NONE = "none", 0
312
+ LIDAR_BLOCKED = "lidar_blocked", 1
313
+ BUMPER_STUCK = "bumper_stuck", 2 # hw-confirmed (bumper held in during commanded motion)
314
+ ROBOT_SUSPENDED = "robot_suspended", 3 # hw-confirmed (wheels lifted off the floor mid-task)
315
+ CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4 # hw-confirmed (one side lifted mid-task)
316
+ MAIN_BRUSH_STUCK = "main_brush_stuck", 5 # hw-confirmed (two physical brush jams)
317
+ MAIN_WHEELS_STUCK = "main_wheels_stuck", 7
318
+ ROBOT_TRAPPED = "robot_trapped", 8 # hw-confirmed
319
+ CHECK_DUSTBIN_FILTER = "check_dustbin_filter", 9
320
+ LOW_BATTERY = "low_battery", 12 # hw-confirmed (fired at 14% mid clean)
321
+ TEMPERATURE_THRESHOLD = "temperature_threshold", 14
322
+ ROBOT_TILTED = "robot_tilted", 16
323
+ LIDAR_COVER_OBSTRUCTED = "lidar_cover_obstructed", 21
324
+ NO_GO_ZONE_DETECTED = "no_go_zone_detected", 24
325
+ MOPPING_MODULE_STUCK = "mopping_module_stuck", 27
326
+ CARPET_AVOIDANCE = "carpet_avoidance", 28
327
+ CANNOT_CROSS_CARPET = "cannot_cross_carpet", 29
328
+ INSTALL_DUST_BAG = "install_dust_bag", 46 # hw-confirmed (docked with bag removed; fires at dock contact)
329
+ MOP_MOUNT_FELL_OFF = "mop_mount_fell_off", 54
330
+ LIDAR_DIRTY = "lidar_dirty", 58
331
+ FILTER_SERVICE_LIFE = "filter_service_life", 301
332
+ MAIN_BRUSH_SERVICE_LIFE = "main_brush_service_life", 302
333
+ SIDE_BRUSH_SERVICE_LIFE = "side_brush_service_life", 303
334
+ SENSOR_NEEDS_CLEANING = "sensor_needs_cleaning", 304 # hw-confirmed (fired during auto-empty)
335
+ DUST_BAG_FULL = "dust_bag_full", 310 # inferred from auto-empty context; not hw-confirmed
336
+ STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # hw-confirmed x3; lifecycle, not an error
337
+ # hw-confirmed (a due scheduled clean fired mid-clean and was ignored); lifecycle, not an error
338
+ CLEANING_IN_PROGRESS = "cleaning_in_progress", 407
339
+ EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked); spec-only
340
+ # hw-confirmed, fires per completed task; ss07 != Q7 (robot_suspended)
341
+ CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501
342
+ LOW_BATTERY_RESUME = "low_battery_resume", 502 # hw-confirmed; lifecycle
343
+ DOCKING_ERROR = "docking_error", 503 # hw-confirmed; ss07 != Q7 (dustbin_not_installed)
344
+ POSITIONING_FAILED = "positioning_failed", 556 # hw-confirmed; relocalization
345
+ # hw-confirmed (3rd auto-empty in ~15 min: dock refuses to run the cycle); ss07 != Q7 (main_wheels_entangled)
346
+ TOO_FREQUENT_EMPTYING = "too_frequent_emptying", 569
347
+ CANNOT_REACH_TARGET = "cannot_reach_target", 570 # hw-confirmed; ss07 != Q7 (main_brush_entangled)
348
+ OFFLINE_WARNING_ASLEEP = "offline_warning_asleep", 588
349
+ OFFLINE_WARNING_LOW_BATTERY = "offline_warning_low_battery", 589
350
+ DND_AUTO_TOPUP_DISABLED = "dnd_auto_topup_disabled", 591
351
+ CLEAN_CARPET_ULTRASONIC_SENSORS = "clean_carpet_ultrasonic_sensors", 707
352
+ ROBOT_ERROR_RESET = "robot_error_reset", 1002
353
+ VOICE_PACK_UPDATE_AVAILABLE = "voice_pack_update_available", 3001
354
+
355
+
292
356
  class RemoteCommand(IntEnum):
293
357
  FORWARD = 0
294
358
  LEFT = 2
@@ -23,6 +23,7 @@ from .b01_q10_code_mappings import (
23
23
  YXDeviceDustCollectionFrequency,
24
24
  YXDeviceState,
25
25
  YXFanLevel,
26
+ YXFault,
26
27
  YXStartMethod,
27
28
  YXWaterLevel,
28
29
  )
@@ -165,7 +166,7 @@ class Q10Status(RoborockBase):
165
166
  clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE})
166
167
  back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE})
167
168
  cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_PROGRESS})
168
- fault: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT})
169
+ fault: YXFault | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT})
169
170
 
170
171
  # Additional state reported in the device's full status dump.
171
172
  clean_line: YXCleanLine | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_LINE})
@@ -203,6 +204,11 @@ class Q10Status(RoborockBase):
203
204
  filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
204
205
  sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
205
206
 
207
+ @property
208
+ def fault_name(self) -> str | None:
209
+ """Returns the name of the current fault."""
210
+ return self.fault.value if self.fault is not None else None
211
+
206
212
 
207
213
  @dataclass
208
214
  class SoundVolume(RoborockBase):
@@ -26,6 +26,8 @@ from roborock.protocol import create_mqtt_params
26
26
  from roborock.web_api import RoborockApiClient, UserWebApiClient
27
27
 
28
28
  from .cache import Cache, DeviceCache, NoCache
29
+ from .rpc.b01_q7_channel import create_b01_q7_channel
30
+ from .rpc.b01_q10_channel import create_b01_q10_channel
29
31
  from .rpc.v1_channel import create_v1_channel
30
32
  from .traits import Trait, a01, b01, v1
31
33
  from .transport.channel import Channel
@@ -254,13 +256,22 @@ async def create_device_manager(
254
256
  channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
255
257
  trait = a01.create(product, channel)
256
258
  case DeviceVersion.B01:
257
- channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
259
+ mqtt_channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
258
260
  model_part = product.model.split(".")[-1]
259
261
  if "ss" in model_part:
262
+ b01_q10_channel = create_b01_q10_channel(mqtt_channel)
263
+ channel = b01_q10_channel
260
264
  trait = b01.q10.create(channel)
261
265
  elif "sc" in model_part:
262
266
  # Q7 devices start with 'sc' in their model naming.
263
- trait = b01.q7.create(product, device, channel)
267
+ b01_q7_channel = create_b01_q7_channel(device, product, mqtt_channel)
268
+ channel = b01_q7_channel
269
+ trait = b01.q7.create(
270
+ product,
271
+ device,
272
+ rpc_channel=b01_q7_channel,
273
+ map_rpc_channel=b01_q7_channel,
274
+ )
264
275
  else:
265
276
  raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}")
266
277
  case _:
@@ -1,9 +1,11 @@
1
1
  """Thin wrapper around the MQTT channel for Roborock B01 Q10 devices."""
2
2
 
3
3
  import logging
4
- from collections.abc import AsyncGenerator
4
+ from collections.abc import AsyncGenerator, Callable
5
+ from typing import Protocol
5
6
 
6
7
  from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
8
+ from roborock.devices.transport.channel import Channel
7
9
  from roborock.devices.transport.mqtt_channel import MqttChannel
8
10
  from roborock.exceptions import RoborockException
9
11
  from roborock.protocols.b01_q10_protocol import (
@@ -12,10 +14,53 @@ from roborock.protocols.b01_q10_protocol import (
12
14
  decode_message,
13
15
  encode_mqtt_payload,
14
16
  )
17
+ from roborock.roborock_message import RoborockMessage
15
18
 
16
19
  _LOGGER = logging.getLogger(__name__)
17
20
 
18
21
 
22
+ class Q10RpcChannel(Protocol):
23
+ """Protocol for Q10 RPC channels."""
24
+
25
+ async def send_command(
26
+ self,
27
+ command: B01_Q10_DP,
28
+ params: ParamsType = None,
29
+ ) -> None:
30
+ """Send a command on the MQTT channel, without waiting for a response."""
31
+ ...
32
+
33
+
34
+ class B01Q10Channel(Channel, Q10RpcChannel):
35
+ """Unified B01 Q10 channel wrapping MQTT transport."""
36
+
37
+ def __init__(self, mqtt_channel: MqttChannel) -> None:
38
+ self._mqtt_channel = mqtt_channel
39
+
40
+ @property
41
+ def is_connected(self) -> bool:
42
+ return self._mqtt_channel.is_connected
43
+
44
+ @property
45
+ def is_local_connected(self) -> bool:
46
+ return False
47
+
48
+ async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
49
+ return await self._mqtt_channel.subscribe(callback)
50
+
51
+ async def subscribe_stream(self) -> AsyncGenerator[Q10Message, None]:
52
+ """Stream decoded Q10 messages received via MQTT."""
53
+ async for msg in stream_decoded_messages(self._mqtt_channel):
54
+ yield msg
55
+
56
+ async def send_command(
57
+ self,
58
+ command: B01_Q10_DP,
59
+ params: ParamsType = None,
60
+ ) -> None:
61
+ await send_command(self._mqtt_channel, command, params)
62
+
63
+
19
64
  async def stream_decoded_messages(
20
65
  mqtt_channel: MqttChannel,
21
66
  ) -> AsyncGenerator[Q10Message, None]:
@@ -59,3 +104,8 @@ async def send_command(
59
104
  ex,
60
105
  )
61
106
  raise
107
+
108
+
109
+ def create_b01_q10_channel(mqtt_channel: MqttChannel) -> B01Q10Channel:
110
+ """Create a B01Q10Channel wrapping MQTT transport."""
111
+ return B01Q10Channel(mqtt_channel)
@@ -1,19 +1,23 @@
1
1
  """Thin wrapper around the MQTT channel for Roborock B01 Q7 devices."""
2
2
 
3
- from __future__ import annotations
4
-
5
3
  import asyncio
6
4
  import json
7
5
  import logging
8
6
  from collections.abc import Callable
9
- from typing import TypeAlias, TypeVar
7
+ from typing import Protocol, TypeAlias, TypeVar
10
8
 
9
+ from roborock.data import HomeDataDevice, HomeDataProduct
10
+ from roborock.devices.transport.channel import Channel
11
11
  from roborock.devices.transport.mqtt_channel import MqttChannel
12
12
  from roborock.exceptions import RoborockException
13
13
  from roborock.protocols.b01_q7_protocol import (
14
+ B01_Q7_DPS,
14
15
  B01_VERSION,
16
+ CommandType,
15
17
  MapKey,
18
+ ParamsType,
16
19
  Q7RequestMessage,
20
+ create_map_key,
17
21
  decode_map_payload,
18
22
  decode_rpc_response,
19
23
  encode_mqtt_payload,
@@ -26,6 +30,30 @@ _T = TypeVar("_T")
26
30
  DecodedB01Response: TypeAlias = dict[str, object] | str
27
31
 
28
32
 
33
+ class Q7RpcChannel(Protocol):
34
+ """Protocol for Q7 RPC channels."""
35
+
36
+ async def send_command(
37
+ self,
38
+ command: CommandType,
39
+ params: ParamsType = None,
40
+ ) -> DecodedB01Response:
41
+ """Send a command and get a decoded response."""
42
+ ...
43
+
44
+
45
+ class Q7MapRpcChannel(Protocol):
46
+ """Protocol for Q7 map RPC channels."""
47
+
48
+ async def send_map_command(
49
+ self,
50
+ command: CommandType,
51
+ params: ParamsType = None,
52
+ ) -> bytes:
53
+ """Send a map command and get decoded bytes."""
54
+ ...
55
+
56
+
29
57
  def _matches_map_response(response_message: RoborockMessage, *, version: bytes | None) -> bytes | None:
30
58
  """Return raw map payload bytes for matching MAP_RESPONSE messages."""
31
59
  if (
@@ -120,39 +148,55 @@ async def send_decoded_command(
120
148
  raise RoborockException(f"B01 command timed out after {_TIMEOUT}s ({request_message})") from ex
121
149
  except RoborockException as ex:
122
150
  _LOGGER.warning(
123
- "Error sending B01 decoded command (%ss): %s",
151
+ "Error sending B01 decoded command (%s): %s",
124
152
  request_message,
125
153
  ex,
126
154
  )
127
155
  raise
128
156
  except Exception as ex:
129
157
  _LOGGER.exception(
130
- "Error sending B01 decoded command (%ss): %s",
158
+ "Error sending B01 decoded command (%s): %s",
131
159
  request_message,
132
160
  ex,
133
161
  )
134
162
  raise
135
163
 
136
164
 
137
- class MapRpcChannel:
138
- """RPC channel for map-related commands on B01/Q7 devices."""
165
+ class B01Q7Channel(Channel, Q7RpcChannel, Q7MapRpcChannel):
166
+ """Unified B01 Q7 channel wrapping MQTT transport."""
139
167
 
140
168
  def __init__(self, mqtt_channel: MqttChannel, map_key: MapKey) -> None:
141
169
  self._mqtt_channel = mqtt_channel
142
170
  self._map_key = map_key
143
171
 
144
- async def send_map_command(self, request_message: Q7RequestMessage) -> bytes:
145
- """Send a map upload command and return decoded SCMap bytes.
146
-
147
- This publishes the request and waits for a matching ``MAP_RESPONSE`` message
148
- with the correct protocol version. The raw ``MAP_RESPONSE`` payload bytes are
149
- then decoded/inflated via :func:`decode_map_payload` using this channel's
150
- ``map_key``, and the resulting SCMap bytes are returned.
151
-
152
- The returned value is the decoded map data bytes suitable for passing to the
153
- map parser library, not the raw MQTT ``MAP_RESPONSE`` payload bytes.
154
- """
172
+ @property
173
+ def is_connected(self) -> bool:
174
+ return self._mqtt_channel.is_connected
175
+
176
+ @property
177
+ def is_local_connected(self) -> bool:
178
+ return False
179
+
180
+ async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
181
+ return await self._mqtt_channel.subscribe(callback)
182
+
183
+ async def send_command(
184
+ self,
185
+ command: CommandType,
186
+ params: ParamsType = None,
187
+ ) -> DecodedB01Response:
188
+ return await send_decoded_command(
189
+ self._mqtt_channel,
190
+ Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
191
+ )
155
192
 
193
+ async def send_map_command(
194
+ self,
195
+ command: CommandType,
196
+ params: ParamsType = None,
197
+ ) -> bytes:
198
+ """Send a map upload command and return decoded SCMap bytes."""
199
+ request_message = Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params)
156
200
  try:
157
201
  raw_payload = await _send_command(
158
202
  self._mqtt_channel,
@@ -163,3 +207,17 @@ class MapRpcChannel:
163
207
  raise RoborockException(f"B01 map command timed out after {_TIMEOUT}s ({request_message})") from ex
164
208
 
165
209
  return decode_map_payload(raw_payload, map_key=self._map_key)
210
+
211
+
212
+ def create_b01_q7_channel(
213
+ device: HomeDataDevice,
214
+ product: HomeDataProduct,
215
+ mqtt_channel: MqttChannel,
216
+ ) -> B01Q7Channel:
217
+ """Create a B01Q7Channel for the given device."""
218
+ if device.sn is None or product.model is None:
219
+ raise RoborockException(
220
+ f"Device serial number and product model are required (sn: {device.sn}, model: {product.model})"
221
+ )
222
+ map_key = create_map_key(serial=device.sn, model=product.model)
223
+ return B01Q7Channel(mqtt_channel, map_key)
@@ -4,9 +4,8 @@ import asyncio
4
4
  import logging
5
5
 
6
6
  from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
7
- from roborock.devices.rpc.b01_q10_channel import stream_decoded_messages
7
+ from roborock.devices.rpc.b01_q10_channel import B01Q10Channel
8
8
  from roborock.devices.traits import Trait
9
- from roborock.devices.transport.mqtt_channel import MqttChannel
10
9
  from roborock.map.b01_q10_map_parser import Q10MapPacket, Q10TracePacket
11
10
  from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message
12
11
 
@@ -83,7 +82,7 @@ class Q10PropertiesApi(Trait):
83
82
  clean_history: CleanHistoryTrait
84
83
  """Trait for fetching the device clean-record history (``dpCleanRecord``)."""
85
84
 
86
- def __init__(self, channel: MqttChannel) -> None:
85
+ def __init__(self, channel: B01Q10Channel) -> None:
87
86
  """Initialize the B01Props API."""
88
87
  self._channel = channel
89
88
  self.command = CommandTrait(channel)
@@ -134,7 +133,7 @@ class Q10PropertiesApi(Trait):
134
133
 
135
134
  async def _subscribe_loop(self) -> None:
136
135
  """Persistent loop dispatching decoded messages to the read-model traits."""
137
- async for message in stream_decoded_messages(self._channel):
136
+ async for message in self._channel.subscribe_stream():
138
137
  self._handle_message(message)
139
138
 
140
139
  def _handle_message(self, message: Q10Message) -> None:
@@ -157,6 +156,6 @@ class Q10PropertiesApi(Trait):
157
156
  trait.update_from_dps(message.dps)
158
157
 
159
158
 
160
- def create(channel: MqttChannel) -> Q10PropertiesApi:
159
+ def create(channel: B01Q10Channel) -> Q10PropertiesApi:
161
160
  """Create traits for B01 devices."""
162
161
  return Q10PropertiesApi(channel)
@@ -1,8 +1,7 @@
1
1
  from typing import Any
2
2
 
3
3
  from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
4
- from roborock.devices.rpc.b01_q10_channel import send_command
5
- from roborock.devices.transport.mqtt_channel import MqttChannel
4
+ from roborock.devices.rpc.b01_q10_channel import Q10RpcChannel
6
5
  from roborock.protocols.b01_q10_protocol import ParamsType
7
6
 
8
7
 
@@ -15,9 +14,9 @@ class CommandTrait:
15
14
  available.
16
15
  """
17
16
 
18
- def __init__(self, channel: MqttChannel) -> None:
17
+ def __init__(self, rpc_channel: Q10RpcChannel) -> None:
19
18
  """Initialize the CommandTrait."""
20
- self._channel = channel
19
+ self._rpc_channel = rpc_channel
21
20
 
22
21
  async def send(self, command: B01_Q10_DP, params: ParamsType = None) -> Any:
23
22
  """Send a command to the device.
@@ -27,6 +26,6 @@ class CommandTrait:
27
26
  caller to ensure that any traits affected by the command are refreshed
28
27
  as needed.
29
28
  """
30
- if not self._channel:
29
+ if not self._rpc_channel:
31
30
  raise ValueError("Device trait in invalid state")
32
- return await send_command(self._channel, command, params=params)
31
+ return await self._rpc_channel.send_command(command, params=params)
@@ -3,8 +3,6 @@
3
3
  Potentially other devices may fall into this category in the future.
4
4
  """
5
5
 
6
- from __future__ import annotations
7
-
8
6
  from typing import Any
9
7
 
10
8
  from roborock import B01Props
@@ -18,11 +16,10 @@ from roborock.data.b01_q7.b01_q7_code_mappings import (
18
16
  SCWindMapping,
19
17
  WaterLevelMapping,
20
18
  )
21
- from roborock.devices.rpc.b01_q7_channel import MapRpcChannel, send_decoded_command
19
+ from roborock.devices.rpc.b01_q7_channel import Q7MapRpcChannel, Q7RpcChannel
22
20
  from roborock.devices.traits import Trait
23
- from roborock.devices.transport.mqtt_channel import MqttChannel
24
21
  from roborock.exceptions import RoborockException
25
- from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, CommandType, ParamsType, Q7RequestMessage, create_map_key
22
+ from roborock.protocols.b01_q7_protocol import CommandType, ParamsType
26
23
  from roborock.roborock_message import RoborockB01Props
27
24
  from roborock.roborock_typing import RoborockB01Q7Methods
28
25
 
@@ -53,10 +50,14 @@ class Q7PropertiesApi(Trait):
53
50
  """Trait for fetching parsed current map content."""
54
51
 
55
52
  def __init__(
56
- self, channel: MqttChannel, map_rpc_channel: MapRpcChannel, device: HomeDataDevice, product: HomeDataProduct
53
+ self,
54
+ rpc_channel: Q7RpcChannel,
55
+ map_rpc_channel: Q7MapRpcChannel,
56
+ device: HomeDataDevice,
57
+ product: HomeDataProduct,
57
58
  ) -> None:
58
59
  """Initialize the Q7 API."""
59
- self._channel = channel
60
+ self._rpc_channel = rpc_channel
60
61
  self._map_rpc_channel = map_rpc_channel
61
62
  self._device = device
62
63
  self._product = product
@@ -64,8 +65,8 @@ class Q7PropertiesApi(Trait):
64
65
  if not device.sn or not product.model:
65
66
  raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
66
67
 
67
- self.clean_summary = CleanSummaryTrait(channel)
68
- self.map = MapTrait(channel)
68
+ self.clean_summary = CleanSummaryTrait(rpc_channel)
69
+ self.map = MapTrait(rpc_channel)
69
70
  self.map_content = MapContentTrait(
70
71
  self._map_rpc_channel,
71
72
  self.map,
@@ -199,17 +200,23 @@ class Q7PropertiesApi(Trait):
199
200
 
200
201
  async def send(self, command: CommandType, params: ParamsType) -> Any:
201
202
  """Send a command to the device."""
202
- return await send_decoded_command(
203
- self._channel,
204
- Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
205
- )
203
+ return await self._rpc_channel.send_command(command, params)
206
204
 
207
205
 
208
- def create(product: HomeDataProduct, device: HomeDataDevice, channel: MqttChannel) -> Q7PropertiesApi:
206
+ def create(
207
+ product: HomeDataProduct,
208
+ device: HomeDataDevice,
209
+ rpc_channel: Q7RpcChannel,
210
+ map_rpc_channel: Q7MapRpcChannel,
211
+ ) -> Q7PropertiesApi:
209
212
  """Create traits for B01 Q7 devices."""
210
213
  if device.sn is None or product.model is None:
211
214
  raise RoborockException(
212
- f"Device serial number and product model are required (sn:: {device.sn}, model: {product.model})"
215
+ f"Device serial number and product model are required (sn: {device.sn}, model: {product.model})"
213
216
  )
214
- map_rpc_channel = MapRpcChannel(channel, map_key=create_map_key(serial=device.sn, model=product.model))
215
- return Q7PropertiesApi(channel, device=device, product=product, map_rpc_channel=map_rpc_channel)
217
+ return Q7PropertiesApi(
218
+ rpc_channel=rpc_channel,
219
+ map_rpc_channel=map_rpc_channel,
220
+ device=device,
221
+ product=product,
222
+ )
@@ -7,11 +7,9 @@ and a `record_list` whose items contain a JSON string in `detail`.
7
7
  import logging
8
8
 
9
9
  from roborock import CleanRecordDetail, CleanRecordList, CleanRecordSummary
10
- from roborock.devices.rpc.b01_q7_channel import send_decoded_command
10
+ from roborock.devices.rpc.b01_q7_channel import Q7RpcChannel
11
11
  from roborock.devices.traits import Trait
12
- from roborock.devices.transport.mqtt_channel import MqttChannel
13
12
  from roborock.exceptions import RoborockException
14
- from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
15
13
  from roborock.roborock_typing import RoborockB01Q7Methods
16
14
 
17
15
  __all__ = [
@@ -24,11 +22,11 @@ _LOGGER = logging.getLogger(__name__)
24
22
  class CleanSummaryTrait(CleanRecordSummary, Trait):
25
23
  """B01/Q7 clean summary + clean record access (via record list service)."""
26
24
 
27
- def __init__(self, channel: MqttChannel) -> None:
25
+ def __init__(self, channel: Q7RpcChannel) -> None:
28
26
  """Initialize the clean summary trait.
29
27
 
30
28
  Args:
31
- channel: MQTT channel used to communicate with the device.
29
+ channel: RPC channel used to communicate with the device.
32
30
  """
33
31
  super().__init__()
34
32
  self._channel = channel
@@ -46,9 +44,9 @@ class CleanSummaryTrait(CleanRecordSummary, Trait):
46
44
 
47
45
  async def _get_record_list(self) -> CleanRecordList:
48
46
  """Fetch the raw device clean record list (`service.get_record_list`)."""
49
- result = await send_decoded_command(
50
- self._channel,
51
- Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_RECORD_LIST, params={}),
47
+ result = await self._channel.send_command(
48
+ command=RoborockB01Q7Methods.GET_RECORD_LIST,
49
+ params={},
52
50
  )
53
51
 
54
52
  if not isinstance(result, dict):
@@ -1,11 +1,9 @@
1
1
  """Map trait for B01 Q7 devices."""
2
2
 
3
3
  from roborock.data import Q7MapList
4
- from roborock.devices.rpc.b01_q7_channel import send_decoded_command
4
+ from roborock.devices.rpc.b01_q7_channel import Q7RpcChannel
5
5
  from roborock.devices.traits import Trait
6
- from roborock.devices.transport.mqtt_channel import MqttChannel
7
6
  from roborock.exceptions import RoborockException
8
- from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
9
7
  from roborock.roborock_typing import RoborockB01Q7Methods
10
8
 
11
9
 
@@ -16,15 +14,15 @@ class MapTrait(Q7MapList, Trait):
16
14
  current map ID to fetch.
17
15
  """
18
16
 
19
- def __init__(self, channel: MqttChannel) -> None:
17
+ def __init__(self, channel: Q7RpcChannel) -> None:
20
18
  super().__init__()
21
19
  self._channel = channel
22
20
 
23
21
  async def refresh(self) -> None:
24
22
  """Refresh cached map list metadata from the device."""
25
- response = await send_decoded_command(
26
- self._channel,
27
- Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
23
+ response = await self._channel.send_command(
24
+ command=RoborockB01Q7Methods.GET_MAP_LIST,
25
+ params={},
28
26
  )
29
27
  if not isinstance(response, dict):
30
28
  raise RoborockException(
@@ -14,11 +14,10 @@ from dataclasses import dataclass
14
14
  from vacuum_map_parser_base.map_data import MapData
15
15
 
16
16
  from roborock.data import RoborockBase
17
- from roborock.devices.rpc.b01_q7_channel import MapRpcChannel
17
+ from roborock.devices.rpc.b01_q7_channel import Q7MapRpcChannel
18
18
  from roborock.devices.traits import Trait
19
19
  from roborock.exceptions import RoborockException
20
20
  from roborock.map.b01_map_parser import B01MapParser, B01MapParserConfig
21
- from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
22
21
  from roborock.roborock_typing import RoborockB01Q7Methods
23
22
 
24
23
  from .map import MapTrait
@@ -55,7 +54,7 @@ class MapContentTrait(MapContent, Trait):
55
54
 
56
55
  def __init__(
57
56
  self,
58
- map_rpc_channel: MapRpcChannel,
57
+ map_rpc_channel: Q7MapRpcChannel,
59
58
  map_trait: MapTrait,
60
59
  *,
61
60
  map_parser_config: B01MapParserConfig | None = None,
@@ -77,13 +76,11 @@ class MapContentTrait(MapContent, Trait):
77
76
  if (map_id := self._map_trait.current_map_id) is None:
78
77
  raise RoborockException("Unable to determine current map ID")
79
78
 
80
- request = Q7RequestMessage(
81
- dps=B01_Q7_DPS,
82
- command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
83
- params={"map_id": map_id},
84
- )
85
79
  async with self._map_command_lock:
86
- raw_payload = await self._map_rpc_channel.send_map_command(request)
80
+ raw_payload = await self._map_rpc_channel.send_map_command(
81
+ RoborockB01Q7Methods.UPLOAD_BY_MAPID,
82
+ {"map_id": map_id},
83
+ )
87
84
 
88
85
  try:
89
86
  parsed_data = self._map_parser.parse(raw_payload)