python-roborock 5.24.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 (120) hide show
  1. {python_roborock-5.24.0 → python_roborock-5.26.0}/PKG-INFO +1 -1
  2. {python_roborock-5.24.0 → python_roborock-5.26.0}/pyproject.toml +1 -1
  3. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/__init__.py +1 -0
  4. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +64 -0
  5. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/b01_q10/b01_q10_containers.py +7 -1
  6. python_roborock-5.26.0/roborock/data/mower/__init__.py +3 -0
  7. python_roborock-5.26.0/roborock/data/mower/mower_containers.py +43 -0
  8. python_roborock-5.26.0/roborock/data/v1/v1_clean_modes.py +361 -0
  9. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/device_manager.py +13 -2
  10. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/rpc/b01_q10_channel.py +51 -1
  11. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/rpc/b01_q7_channel.py +76 -18
  12. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/__init__.py +4 -5
  13. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/command.py +5 -6
  14. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/__init__.py +24 -17
  15. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/clean_summary.py +6 -8
  16. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/map.py +5 -7
  17. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q7/map_content.py +6 -9
  18. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/status.py +36 -4
  19. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/roborock_message.py +39 -0
  20. python_roborock-5.24.0/roborock/data/v1/v1_clean_modes.py +0 -192
  21. {python_roborock-5.24.0 → python_roborock-5.26.0}/.gitignore +0 -0
  22. {python_roborock-5.24.0 → python_roborock-5.26.0}/LICENSE +0 -0
  23. {python_roborock-5.24.0 → python_roborock-5.26.0}/README.md +0 -0
  24. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/__init__.py +0 -0
  25. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/broadcast_protocol.py +0 -0
  26. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/callbacks.py +0 -0
  27. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/cli.py +0 -0
  28. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/const.py +0 -0
  29. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/b01_q10/__init__.py +0 -0
  30. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/b01_q7/__init__.py +0 -0
  31. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  32. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  33. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/code_mappings.py +0 -0
  34. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/containers.py +0 -0
  35. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/dyad/__init__.py +0 -0
  36. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  37. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/dyad/dyad_containers.py +0 -0
  38. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/v1/__init__.py +0 -0
  39. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  40. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/v1/v1_containers.py +0 -0
  41. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/zeo/__init__.py +0 -0
  42. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  43. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/data/zeo/zeo_containers.py +0 -0
  44. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/device_features.py +0 -0
  45. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/README.md +0 -0
  46. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/__init__.py +0 -0
  47. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/cache.py +0 -0
  48. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/device.py +0 -0
  49. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/file_cache.py +0 -0
  50. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/rpc/__init__.py +0 -0
  51. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/rpc/a01_channel.py +0 -0
  52. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/rpc/v1_channel.py +0 -0
  53. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/__init__.py +0 -0
  54. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/a01/__init__.py +0 -0
  55. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/__init__.py +0 -0
  56. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/button_light.py +0 -0
  57. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
  58. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
  59. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  60. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/consumable.py +0 -0
  61. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
  62. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
  63. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/map.py +0 -0
  64. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/network_info.py +0 -0
  65. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
  66. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  67. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  68. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/b01/q10/volume.py +0 -0
  69. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/common.py +0 -0
  70. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/traits_mixin.py +0 -0
  71. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/__init__.py +0 -0
  72. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  73. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  74. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/command.py +0 -0
  75. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/common.py +0 -0
  76. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  77. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/device_features.py +0 -0
  78. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  79. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  80. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  81. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/home.py +0 -0
  82. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/led_status.py +0 -0
  83. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/map_content.py +0 -0
  84. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/maps.py +0 -0
  85. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/network_info.py +0 -0
  86. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/rooms.py +0 -0
  87. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/routines.py +0 -0
  88. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  89. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  90. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/volume.py +0 -0
  91. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  92. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/transport/__init__.py +0 -0
  93. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/transport/channel.py +0 -0
  94. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/transport/local_channel.py +0 -0
  95. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  96. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/diagnostics.py +0 -0
  97. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/exceptions.py +0 -0
  98. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/__init__.py +0 -0
  99. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/b01_grid_layers.py +0 -0
  100. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/b01_map_parser.py +0 -0
  101. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/b01_q10_map_parser.py +0 -0
  102. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/b01_q10_overlays.py +0 -0
  103. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/map_parser.py +0 -0
  104. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/proto/__init__.py +0 -0
  105. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/proto/b01_scmap.proto +0 -0
  106. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  107. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/mqtt/__init__.py +0 -0
  108. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/mqtt/health_manager.py +0 -0
  109. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/mqtt/roborock_session.py +0 -0
  110. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/mqtt/session.py +0 -0
  111. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/protocol.py +0 -0
  112. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/protocols/__init__.py +0 -0
  113. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/protocols/a01_protocol.py +0 -0
  114. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  115. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  116. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/protocols/v1_protocol.py +0 -0
  117. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/py.typed +0 -0
  118. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/roborock_typing.py +0 -0
  119. {python_roborock-5.24.0 → python_roborock-5.26.0}/roborock/util.py +0 -0
  120. {python_roborock-5.24.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.24.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.24.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"
@@ -5,5 +5,6 @@ from .b01_q10 import *
5
5
  from .code_mappings import *
6
6
  from .containers import *
7
7
  from .dyad import *
8
+ from .mower import *
8
9
  from .v1 import *
9
10
  from .zeo import *
@@ -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):
@@ -0,0 +1,3 @@
1
+ """Dataclasses for Roborock mower devices."""
2
+
3
+ from .mower_containers import *
@@ -0,0 +1,43 @@
1
+ """Data containers for Roborock mower devices."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from roborock.data.containers import RoborockBase
7
+ from roborock.roborock_message import RoborockMowerDataProtocol
8
+
9
+
10
+ @dataclass
11
+ class MowerStatus(RoborockBase):
12
+ """Core mower status backed by mower DPS updates."""
13
+
14
+ error_code: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.ERROR_CODE})
15
+ battery: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BATTERY})
16
+ mow_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_TYPE})
17
+ mow_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_STATE})
18
+ mapping_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_TYPE})
19
+ mapping_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_STATE})
20
+ ota_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OTA_STATE})
21
+ charge_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_STATE})
22
+ dock_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.DOCK_STATE})
23
+ charge_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_TYPE})
24
+ pend_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.PEND_TYPE})
25
+ remote_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.REMOTE_STATE})
26
+ mow_start_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_START_TYPE})
27
+ mow_eff_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_EFF_MODE})
28
+ mow_height: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_HEIGHT})
29
+ mow_direction_angle: int | None = field(
30
+ default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_DIRECTION_ANGLE}
31
+ )
32
+ mow_pattern: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PATTERN})
33
+ mow_conf_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_CONF_MODE})
34
+ offline_status: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OFFLINE_STATUS})
35
+ mow_progress: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PROGRESS})
36
+ blade_lifespan: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BLADE_LIFESPAN})
37
+ fc_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.FC_STATE})
38
+ gps_coordinate: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.GPS_COORDINATE})
39
+ off_dock_no_task_status: int | None = field(
40
+ default=None, metadata={"dps": RoborockMowerDataProtocol.OFF_DOCK_NO_TASK_STATUS}
41
+ )
42
+ afs_status: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.AFS_STATUS})
43
+ network_channel: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.NETWORK_CHANNEL})
@@ -0,0 +1,361 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from enum import StrEnum
5
+ from typing import TypeVar
6
+
7
+ from ...exceptions import RoborockUnsupportedFeature
8
+ from ..code_mappings import RoborockModeEnum
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from roborock.device_features import DeviceFeatures
12
+
13
+
14
+ class VacuumModes(RoborockModeEnum):
15
+ GENTLE = ("gentle", 105)
16
+ OFF = ("off", 105)
17
+ QUIET = ("quiet", 101)
18
+ BALANCED = ("balanced", 102)
19
+ TURBO = ("turbo", 103)
20
+ MAX = ("max", 104)
21
+ MAX_PLUS = ("max_plus", 108)
22
+ OFF_RAISE_MAIN_BRUSH = ("off_raise_main_brush", 109)
23
+ CUSTOMIZED = ("custom", 106)
24
+ SMART_MODE = ("smart_mode", 110)
25
+
26
+
27
+ class CleanRoutes(RoborockModeEnum):
28
+ STANDARD = ("standard", 300)
29
+ DEEP = ("deep", 301)
30
+ DEEP_PLUS = ("deep_plus", 303)
31
+ FAST = ("fast", 304)
32
+ DEEP_PLUS_CN = ("deep_plus", 305)
33
+ SMART_MODE = ("smart_mode", 306)
34
+ CUSTOMIZED = ("custom", 302)
35
+
36
+
37
+ class VacuumModesOld(RoborockModeEnum):
38
+ QUIET = ("quiet", 38)
39
+ BALANCED = ("balanced", 60)
40
+ TURBO = ("turbo", 75)
41
+ MAX = ("max", 100)
42
+
43
+
44
+ class WaterModes(RoborockModeEnum):
45
+ OFF = ("off", 200)
46
+ LOW = ("low", 201)
47
+ MILD = ("mild", 201)
48
+ MEDIUM = ("medium", 202)
49
+ STANDARD = ("standard", 202)
50
+ HIGH = ("high", 203)
51
+ INTENSE = ("intense", 203)
52
+ MIN = ("min", 205)
53
+ MAX = ("max", 206)
54
+ CUSTOMIZED = ("custom", 204)
55
+ CUSTOM = ("custom_water_flow", 207)
56
+ EXTREME = ("extreme", 208)
57
+ SMART_MODE = ("smart_mode", 209)
58
+ PURE_WATER_FLOW_START = ("slight", 221)
59
+ PURE_WATER_FLOW_SMALL = ("low", 225)
60
+ PURE_WATER_FLOW_MIDDLE = ("medium", 235)
61
+ PURE_WATER_FLOW_LARGE = ("moderate", 245)
62
+ PURE_WATER_SUPER_BEGIN = ("high", 248)
63
+ PURE_WATER_FLOW_END = ("extreme", 250)
64
+
65
+
66
+ class WashTowelModes(RoborockModeEnum):
67
+ SMART = ("smart", 10)
68
+ LIGHT = ("light", 0)
69
+ BALANCED = ("balanced", 1)
70
+ DEEP = ("deep", 2)
71
+ SUPER_DEEP = ("super_deep", 8)
72
+
73
+
74
+ class CleaningMode(StrEnum):
75
+ """High-level cleaning intent derived from the lower-level motor settings.
76
+
77
+ Prefer this abstraction when you want to present or switch between the
78
+ user-facing cleaning behaviors exposed by the app. The lower-level
79
+ `VacuumModes`, `WaterModes`, and `CleanRoutes` enums are still useful for
80
+ fine-grained selection.
81
+ """
82
+
83
+ VACUUM = "vacuum"
84
+ VAC_AND_MOP = "vac_and_mop"
85
+ MOP = "mop"
86
+ CUSTOM = "custom"
87
+ SMART_MODE = "smart_mode"
88
+
89
+
90
+ WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes] = {
91
+ 200: WaterModes.OFF,
92
+ 221: WaterModes.PURE_WATER_FLOW_START,
93
+ 225: WaterModes.PURE_WATER_FLOW_SMALL,
94
+ 235: WaterModes.PURE_WATER_FLOW_MIDDLE,
95
+ 245: WaterModes.PURE_WATER_FLOW_LARGE,
96
+ 248: WaterModes.PURE_WATER_SUPER_BEGIN,
97
+ 250: WaterModes.PURE_WATER_FLOW_END,
98
+ }
99
+
100
+ ModeEnumT = TypeVar("ModeEnumT", bound=RoborockModeEnum)
101
+
102
+
103
+ def get_wash_towel_modes(features: DeviceFeatures) -> list[WashTowelModes]:
104
+ """Get the valid wash towel modes for the device"""
105
+ modes = [WashTowelModes.LIGHT, WashTowelModes.BALANCED, WashTowelModes.DEEP]
106
+ if features.is_super_deep_wash_supported and not features.is_dirty_replenish_clean_supported:
107
+ modes.append(WashTowelModes.SUPER_DEEP)
108
+ elif features.is_dirty_replenish_clean_supported:
109
+ modes.append(WashTowelModes.SMART)
110
+ return modes
111
+
112
+
113
+ def get_clean_modes(features: DeviceFeatures) -> list[VacuumModes]:
114
+ """Get the valid clean modes for the device - also known as 'fan power' or 'suction mode'"""
115
+ modes = [VacuumModes.QUIET, VacuumModes.BALANCED, VacuumModes.TURBO, VacuumModes.MAX]
116
+ if features.is_max_plus_mode_supported or features.is_none_pure_clean_mop_with_max_plus:
117
+ # If the vacuum has max plus mode supported
118
+ modes.append(VacuumModes.MAX_PLUS)
119
+ if features.is_pure_clean_mop_supported:
120
+ # If the vacuum is capable of 'pure mop clean' aka no vacuum
121
+ if features.is_support_main_brush_up_down_supported:
122
+ modes.append(VacuumModes.OFF_RAISE_MAIN_BRUSH)
123
+ else:
124
+ modes.append(VacuumModes.OFF)
125
+ else:
126
+ # If not, we can add gentle
127
+ modes.append(VacuumModes.GENTLE)
128
+ if features.is_smart_clean_mode_set_supported:
129
+ modes.append(VacuumModes.SMART_MODE)
130
+ if features.is_customized_clean_supported:
131
+ modes.append(VacuumModes.CUSTOMIZED)
132
+ return modes
133
+
134
+
135
+ def get_clean_routes(features: DeviceFeatures, region: str) -> list[CleanRoutes]:
136
+ """The routes that the vacuum will take while mopping"""
137
+ if features.is_none_pure_clean_mop_with_max_plus:
138
+ return [CleanRoutes.FAST, CleanRoutes.STANDARD]
139
+ supported = [CleanRoutes.STANDARD, CleanRoutes.DEEP]
140
+ if features.is_careful_slow_mop_supported:
141
+ if not (
142
+ features.is_corner_clean_mode_supported
143
+ and features.is_clean_route_deep_slow_plus_supported
144
+ and region == "cn"
145
+ ):
146
+ # for some reason there is a china specific deep plus mode
147
+ supported.append(CleanRoutes.DEEP_PLUS_CN)
148
+ else:
149
+ supported.append(CleanRoutes.DEEP_PLUS)
150
+
151
+ if features.is_clean_route_fast_mode_supported:
152
+ supported.append(CleanRoutes.FAST)
153
+ if features.is_smart_clean_mode_set_supported:
154
+ supported.append(CleanRoutes.SMART_MODE)
155
+ if features.is_customized_clean_supported:
156
+ supported.append(CleanRoutes.CUSTOMIZED)
157
+
158
+ return supported
159
+
160
+
161
+ def get_water_modes(features: DeviceFeatures) -> list[WaterModes]:
162
+ """Get the valid water modes for the device - also known as 'water flow' or 'water level'"""
163
+ # Water slide mode supports a separate set of water flow codes.
164
+ if features.is_water_slide_mode_supported:
165
+ return list(WATER_SLIDE_MODE_MAPPING.values())
166
+
167
+ supported_modes = [WaterModes.OFF]
168
+ if features.is_mop_shake_module_supported:
169
+ # For mops that have the vibrating mop pad, they do mild standard intense
170
+ supported_modes.extend([WaterModes.MILD, WaterModes.STANDARD, WaterModes.INTENSE])
171
+ else:
172
+ supported_modes.extend([WaterModes.LOW, WaterModes.MEDIUM, WaterModes.HIGH])
173
+ if features.is_custom_water_box_distance_supported:
174
+ # This is for devices that allow you to set a custom water flow from 0-100
175
+ supported_modes.append(WaterModes.CUSTOM)
176
+ if features.is_mop_shake_module_supported and features.is_mop_shake_water_max_supported:
177
+ supported_modes.append(WaterModes.EXTREME)
178
+ if features.is_smart_clean_mode_set_supported:
179
+ supported_modes.append(WaterModes.SMART_MODE)
180
+ if features.is_customized_clean_supported:
181
+ supported_modes.append(WaterModes.CUSTOMIZED)
182
+
183
+ return supported_modes
184
+
185
+
186
+ def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
187
+ """Get water mode mapping by supported feature set.
188
+
189
+ WaterModes contains aliases for multiple codes that share the same value
190
+ string (e.g. low can be 201 or 225). For water slide mode devices we need
191
+ explicit code mapping to preserve those slide-specific codes.
192
+ """
193
+ if features.is_water_slide_mode_supported:
194
+ return {code: mode.value for code, mode in WATER_SLIDE_MODE_MAPPING.items()}
195
+ return {mode.code: mode.value for mode in get_water_modes(features)}
196
+
197
+
198
+ def get_cleaning_mode_options(features: DeviceFeatures) -> list[CleaningMode]:
199
+ """Return the supported high-level cleaning modes for the device.
200
+
201
+ These options are the preferred user-facing choices because they bundle the
202
+ correct fan, water, and mop-route settings together for the device. Callers
203
+ should generally present these instead of mixing lower-level mode enums
204
+ unless they explicitly need fine-grained control.
205
+ """
206
+ if not features.is_support_water_mode:
207
+ return []
208
+
209
+ supported_water_modes = get_water_modes(features)
210
+ options = [CleaningMode.VACUUM, CleaningMode.VAC_AND_MOP]
211
+ if features.is_pure_clean_mop_supported:
212
+ options.append(CleaningMode.MOP)
213
+ if features.is_customized_clean_supported and WaterModes.CUSTOMIZED in supported_water_modes:
214
+ options.append(CleaningMode.CUSTOM)
215
+ if features.is_smart_clean_mode_set_supported and WaterModes.SMART_MODE in supported_water_modes:
216
+ options.append(CleaningMode.SMART_MODE)
217
+ return options
218
+
219
+
220
+ def get_mop_only_vacuum_mode(features: DeviceFeatures) -> VacuumModes:
221
+ """Determine the vacuum mode to use when you just want to mop.
222
+
223
+ There are three cases that must be handled:
224
+ 1. The device does not support only mopping.
225
+ 2. The device supports raising the vacuum brush while mopping
226
+ 3. All other cases.
227
+ """
228
+ if not features.is_pure_clean_mop_supported:
229
+ raise RoborockUnsupportedFeature("Mop-only cleaning is not supported")
230
+ if features.is_support_main_brush_up_down_supported:
231
+ return VacuumModes.OFF_RAISE_MAIN_BRUSH
232
+ return VacuumModes.OFF
233
+
234
+
235
+ def _get_default_mopping_water_mode(features: DeviceFeatures) -> WaterModes:
236
+ """Pick a sensible default water mode when mopping for the device."""
237
+ # Water-slide devices use a disjoint set of water codes; pick a mid-flow
238
+ # slide code instead of the standard 202, which they don't accept.
239
+ if features.is_water_slide_mode_supported:
240
+ return WaterModes.PURE_WATER_FLOW_MIDDLE
241
+ return WaterModes.STANDARD
242
+
243
+
244
+ def _get_clean_motor_mode_params(
245
+ mode: CleaningMode,
246
+ features: DeviceFeatures,
247
+ ) -> tuple[VacuumModes, WaterModes, CleanRoutes]:
248
+ """Return (fan_power, water_box_mode, mop_mode) enums for the high-level mode."""
249
+ if mode == CleaningMode.VACUUM:
250
+ return (VacuumModes.BALANCED, WaterModes.OFF, CleanRoutes.STANDARD)
251
+ if mode == CleaningMode.VAC_AND_MOP:
252
+ return (VacuumModes.BALANCED, _get_default_mopping_water_mode(features), CleanRoutes.STANDARD)
253
+ if mode == CleaningMode.MOP:
254
+ return (
255
+ get_mop_only_vacuum_mode(features),
256
+ _get_default_mopping_water_mode(features),
257
+ CleanRoutes.STANDARD,
258
+ )
259
+ if mode == CleaningMode.CUSTOM:
260
+ return (VacuumModes.CUSTOMIZED, WaterModes.CUSTOMIZED, CleanRoutes.CUSTOMIZED)
261
+ if mode == CleaningMode.SMART_MODE:
262
+ return (VacuumModes.SMART_MODE, WaterModes.SMART_MODE, CleanRoutes.SMART_MODE)
263
+ raise RoborockUnsupportedFeature(f"Cleaning mode {mode.value!r} is not supported")
264
+
265
+
266
+ def resolve_cleaning_mode(cleaning_mode: str | CleaningMode) -> CleaningMode:
267
+ """Resolve a string or enum into a CleaningMode value."""
268
+ if isinstance(cleaning_mode, CleaningMode):
269
+ return cleaning_mode
270
+ try:
271
+ return CleaningMode(cleaning_mode)
272
+ except ValueError as err:
273
+ raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode!r} is not supported") from err
274
+
275
+
276
+ def get_cleaning_mode_parameters(cleaning_mode: CleaningMode, features: DeviceFeatures) -> list[dict[str, int]]:
277
+ """Get the RPC payload for switching the high-level cleaning mode."""
278
+ if cleaning_mode not in get_cleaning_mode_options(features):
279
+ raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode.value!r} is not supported")
280
+
281
+ fan_power, water_box_mode, mop_mode = _get_clean_motor_mode_params(cleaning_mode, features)
282
+ params: dict[str, int] = {"fan_power": fan_power.code, "water_box_mode": water_box_mode.code}
283
+ if features.is_clean_route_setting_supported:
284
+ params["mop_mode"] = mop_mode.code
285
+ return [params]
286
+
287
+
288
+ def _resolve_mode_code(value: int | ModeEnumT | None, mode_cls: type[ModeEnumT]) -> ModeEnumT | None:
289
+ """Resolve a raw code or enum into a RoborockModeEnum."""
290
+ if value is None:
291
+ return None
292
+ if isinstance(value, mode_cls):
293
+ return value
294
+ return mode_cls.from_code_optional(int(value))
295
+
296
+
297
+ def _resolve_clean_mode(value: int | VacuumModes | None, features: DeviceFeatures) -> VacuumModes | None:
298
+ """Resolve a vacuum mode code, accounting for feature-specific code aliases."""
299
+ if value is None or isinstance(value, VacuumModes):
300
+ return value
301
+ if value == VacuumModes.OFF.code:
302
+ if features.is_pure_clean_mop_supported:
303
+ return get_mop_only_vacuum_mode(features)
304
+ return VacuumModes.GENTLE
305
+ return VacuumModes.from_code_optional(value)
306
+
307
+
308
+ def get_current_cleaning_mode(
309
+ clean_mode: int | VacuumModes | None,
310
+ water_mode: int | WaterModes | None,
311
+ mop_mode: int | CleanRoutes | None,
312
+ features: DeviceFeatures,
313
+ ) -> CleaningMode | None:
314
+ """Classify the current high-level cleaning mode from individual mode codes."""
315
+ if not features.is_support_water_mode:
316
+ return None
317
+ clean_mode_enum = _resolve_clean_mode(clean_mode, features)
318
+ water_mode_enum = _resolve_mode_code(water_mode, WaterModes)
319
+ mop_mode_enum = _resolve_mode_code(mop_mode, CleanRoutes)
320
+ if clean_mode_enum is None or water_mode_enum is None:
321
+ return None
322
+
323
+ if is_smart_mode_set(water_mode_enum, clean_mode_enum, mop_mode_enum):
324
+ return CleaningMode.SMART_MODE
325
+ if is_mode_customized(clean_mode_enum, water_mode_enum, mop_mode_enum):
326
+ return CleaningMode.CUSTOM
327
+ if water_mode_enum != WaterModes.OFF:
328
+ try:
329
+ if clean_mode_enum == get_mop_only_vacuum_mode(features):
330
+ return CleaningMode.MOP
331
+ except RoborockUnsupportedFeature:
332
+ pass
333
+ if water_mode_enum == WaterModes.OFF:
334
+ return CleaningMode.VACUUM
335
+ return CleaningMode.VAC_AND_MOP
336
+
337
+
338
+ def is_mode_customized(
339
+ clean_mode: VacuumModes | None,
340
+ water_mode: WaterModes | None,
341
+ mop_mode: CleanRoutes | None,
342
+ ) -> bool:
343
+ """Check if any of the cleaning modes are set to a custom value."""
344
+ return (
345
+ clean_mode == VacuumModes.CUSTOMIZED
346
+ or water_mode == WaterModes.CUSTOMIZED
347
+ or mop_mode == CleanRoutes.CUSTOMIZED
348
+ )
349
+
350
+
351
+ def is_smart_mode_set(
352
+ water_mode: WaterModes | None,
353
+ clean_mode: VacuumModes | None,
354
+ mop_mode: CleanRoutes | None,
355
+ ) -> bool:
356
+ """Check if the smart mode is set for the given water mode and clean mode"""
357
+ return (
358
+ water_mode == WaterModes.SMART_MODE
359
+ or clean_mode == VacuumModes.SMART_MODE
360
+ or mop_mode == CleanRoutes.SMART_MODE
361
+ )
@@ -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)