python-roborock 5.27.0__tar.gz → 5.28.1__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 (124) hide show
  1. {python_roborock-5.27.0 → python_roborock-5.28.1}/PKG-INFO +1 -1
  2. {python_roborock-5.27.0 → python_roborock-5.28.1}/pyproject.toml +1 -1
  3. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/v1/v1_code_mappings.py +26 -16
  4. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/v1/v1_containers.py +12 -0
  5. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/device_features.py +209 -12
  6. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/__init__.py +18 -9
  7. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/dust_collection_mode.py +6 -2
  8. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/smart_wash_params.py +6 -2
  9. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/wash_towel_mode.py +6 -2
  10. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/testing/channel.py +27 -5
  11. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/testing/cloud.py +44 -1
  12. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/testing/simulator.py +2 -8
  13. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/testing/v1_simulator.py +1 -1
  14. {python_roborock-5.27.0 → python_roborock-5.28.1}/.gitignore +0 -0
  15. {python_roborock-5.27.0 → python_roborock-5.28.1}/LICENSE +0 -0
  16. {python_roborock-5.27.0 → python_roborock-5.28.1}/README.md +0 -0
  17. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/__init__.py +0 -0
  18. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/broadcast_protocol.py +0 -0
  19. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/callbacks.py +0 -0
  20. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/cli.py +0 -0
  21. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/const.py +0 -0
  22. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/__init__.py +0 -0
  23. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/b01_q10/__init__.py +0 -0
  24. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  25. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  26. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/b01_q7/__init__.py +0 -0
  27. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  28. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  29. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/code_mappings.py +0 -0
  30. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/containers.py +0 -0
  31. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/dyad/__init__.py +0 -0
  32. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  33. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/dyad/dyad_containers.py +0 -0
  34. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/mower/__init__.py +0 -0
  35. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/mower/mower_containers.py +0 -0
  36. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/v1/__init__.py +0 -0
  37. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/v1/v1_clean_modes.py +0 -0
  38. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/zeo/__init__.py +0 -0
  39. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  40. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/data/zeo/zeo_containers.py +0 -0
  41. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/README.md +0 -0
  42. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/__init__.py +0 -0
  43. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/cache.py +0 -0
  44. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/device.py +0 -0
  45. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/device_manager.py +0 -0
  46. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/file_cache.py +0 -0
  47. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/rpc/__init__.py +0 -0
  48. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/rpc/a01_channel.py +0 -0
  49. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  50. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  51. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/rpc/v1_channel.py +0 -0
  52. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/__init__.py +0 -0
  53. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/a01/__init__.py +0 -0
  54. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/__init__.py +0 -0
  55. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  56. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/button_light.py +0 -0
  57. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
  58. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
  59. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/command.py +0 -0
  60. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/common.py +0 -0
  61. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/consumable.py +0 -0
  62. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
  63. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
  64. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/map.py +0 -0
  65. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/network_info.py +0 -0
  66. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/remote.py +0 -0
  67. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/status.py +0 -0
  68. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  69. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q10/volume.py +0 -0
  70. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  71. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  72. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q7/map.py +0 -0
  73. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/b01/q7/map_content.py +0 -0
  74. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/common.py +0 -0
  75. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/traits_mixin.py +0 -0
  76. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/child_lock.py +0 -0
  77. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/clean_summary.py +0 -0
  78. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/command.py +0 -0
  79. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/common.py +0 -0
  80. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/consumeable.py +0 -0
  81. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/device_features.py +0 -0
  82. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  83. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  84. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/home.py +0 -0
  85. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/led_status.py +0 -0
  86. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/map_content.py +0 -0
  87. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/maps.py +0 -0
  88. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/network_info.py +0 -0
  89. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/rooms.py +0 -0
  90. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/routines.py +0 -0
  91. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/status.py +0 -0
  92. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  93. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/traits/v1/volume.py +0 -0
  94. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/transport/__init__.py +0 -0
  95. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/transport/channel.py +0 -0
  96. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/transport/local_channel.py +0 -0
  97. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/devices/transport/mqtt_channel.py +0 -0
  98. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/diagnostics.py +0 -0
  99. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/exceptions.py +0 -0
  100. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/__init__.py +0 -0
  101. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/b01_grid_layers.py +0 -0
  102. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/b01_map_parser.py +0 -0
  103. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/b01_q10_map_parser.py +0 -0
  104. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/b01_q10_overlays.py +0 -0
  105. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/map_parser.py +0 -0
  106. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/proto/__init__.py +0 -0
  107. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/proto/b01_scmap.proto +0 -0
  108. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  109. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/mqtt/__init__.py +0 -0
  110. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/mqtt/health_manager.py +0 -0
  111. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/mqtt/roborock_session.py +0 -0
  112. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/mqtt/session.py +0 -0
  113. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/protocol.py +0 -0
  114. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/protocols/__init__.py +0 -0
  115. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/protocols/a01_protocol.py +0 -0
  116. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/protocols/b01_q10_protocol.py +0 -0
  117. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/protocols/b01_q7_protocol.py +0 -0
  118. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/protocols/v1_protocol.py +0 -0
  119. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/py.typed +0 -0
  120. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/roborock_message.py +0 -0
  121. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/roborock_typing.py +0 -0
  122. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/testing/__init__.py +0 -0
  123. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/util.py +0 -0
  124. {python_roborock-5.27.0 → python_roborock-5.28.1}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 5.27.0
3
+ Version: 5.28.1
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.27.0"
3
+ version = "5.28.1"
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"
@@ -650,22 +650,32 @@ class RoborockDockErrorCode(RoborockEnum):
650
650
 
651
651
  class RoborockDockTypeCode(RoborockEnum):
652
652
  unknown = -9999
653
- no_dock = 0
654
- auto_empty_dock = 1
655
- empty_wash_fill_dock = 3
656
- auto_empty_dock_pure = 5
657
- s7_max_ultra_dock = 6
658
- s8_dock = 7
659
- p10_dock = 8
660
- p10_pro_dock = 9
661
- s8_maxv_ultra_dock = 10
662
- qrevo_master_dock = 14
663
- qrevo_s_dock = 15
664
- saros_r10_dock = 16
665
- qrevo_curv_dock = 17
666
- saros_10_dock = 18
667
- qrevo_s5v_dock = 22
668
- saros_20_dock = 27
653
+ o0_dock = 0
654
+ o1_dock = 1
655
+ o2_dock = 2
656
+ o3_dock = 3
657
+ oc_dock = 5
658
+ o3_plus_dock = 6
659
+ o4_dock = 7
660
+ pearl_dock = 8
661
+ pearl_plus_dock = 9
662
+ o5_dock = 10
663
+ shell_2s_dock = 11
664
+ couple_dock = 13
665
+ shell_3_dock = 14
666
+ shell_2c_dock = 15
667
+ shell_3s_dock = 16
668
+ k1_dock = 17
669
+ o6_dock = 18
670
+ k1c_dock = 19
671
+ k1s_dock = 20
672
+ shell_e_dock = 21
673
+ shell_2e_dock = 22
674
+ shell_3c_dock = 23
675
+ type_27_dock = 27
676
+ k1c_lite_dock = 28
677
+ shell_2e_lite_dock = 30
678
+ shell_2e_heat_dock = 40
669
679
 
670
680
 
671
681
  class RoborockDockDustCollectionModeCode(RoborockEnum):
@@ -239,6 +239,12 @@ class Status(RoborockBase):
239
239
  return map_flag
240
240
  return None
241
241
 
242
+ @property
243
+ def has_am(self) -> bool | None:
244
+ if self.dss is None:
245
+ return None
246
+ return (self.dss & 3) == 2
247
+
242
248
  @property
243
249
  def clear_water_box_status(self) -> ClearWaterBoxStatus | None:
244
250
  if self.dss:
@@ -376,6 +382,12 @@ class StatusV2(RoborockBase):
376
382
  return map_flag
377
383
  return None
378
384
 
385
+ @property
386
+ def has_am(self) -> bool | None:
387
+ if self.dss is None:
388
+ return None
389
+ return (self.dss & 3) == 2
390
+
379
391
  @property
380
392
  def clear_water_box_status(self) -> ClearWaterBoxStatus | None:
381
393
  if self.dss:
@@ -1,5 +1,6 @@
1
1
  from dataclasses import dataclass, field, fields
2
2
  from enum import IntEnum, StrEnum
3
+ from functools import cache
3
4
  from typing import Any, Self
4
5
 
5
6
  from roborock.data.code_mappings import RoborockProductNickname
@@ -644,25 +645,221 @@ class DeviceFeatures(RoborockBase):
644
645
  return [k for k, v in vars(self).items() if v]
645
646
 
646
647
 
648
+ _NO_DOCK_TYPES = {
649
+ RoborockDockTypeCode.unknown,
650
+ RoborockDockTypeCode.o0_dock,
651
+ }
652
+
653
+ _PURE_COLLECT_DOCK_TYPES = {
654
+ RoborockDockTypeCode.o1_dock,
655
+ RoborockDockTypeCode.oc_dock,
656
+ }
657
+
658
+ _PURE_WASH_DOCK_TYPES = {
659
+ RoborockDockTypeCode.o2_dock,
660
+ }
661
+
662
+ _COLLECT_WASH_DOCK_TYPES = {
663
+ RoborockDockTypeCode.o3_dock,
664
+ }
665
+
666
+ _ONLY_X_SERIES_DOCK_TYPES = {
667
+ RoborockDockTypeCode.o2_dock,
668
+ RoborockDockTypeCode.o3_dock,
669
+ RoborockDockTypeCode.o3_plus_dock,
670
+ RoborockDockTypeCode.o4_dock,
671
+ RoborockDockTypeCode.o5_dock,
672
+ RoborockDockTypeCode.o6_dock,
673
+ }
674
+
675
+ _CLEAN_FLUID_AUTO_DELIVERY_DOCK_TYPES = {
676
+ RoborockDockTypeCode.o4_dock,
677
+ RoborockDockTypeCode.o5_dock,
678
+ RoborockDockTypeCode.pearl_plus_dock,
679
+ RoborockDockTypeCode.shell_3s_dock,
680
+ RoborockDockTypeCode.o6_dock,
681
+ RoborockDockTypeCode.couple_dock,
682
+ }
683
+
684
+ _CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES = {
685
+ RoborockDockTypeCode.shell_2s_dock,
686
+ RoborockDockTypeCode.shell_3_dock,
687
+ RoborockDockTypeCode.shell_2c_dock,
688
+ RoborockDockTypeCode.k1_dock,
689
+ RoborockDockTypeCode.k1s_dock,
690
+ RoborockDockTypeCode.shell_e_dock,
691
+ RoborockDockTypeCode.shell_2e_dock,
692
+ RoborockDockTypeCode.shell_3c_dock,
693
+ }
694
+
695
+ _AUTO_STERILIZE_DOCK_TYPES = {
696
+ RoborockDockTypeCode.shell_3s_dock,
697
+ }
698
+
699
+ _INNER_CHARGE_HEAD_DOCK_TYPES = {
700
+ RoborockDockTypeCode.o0_dock,
701
+ RoborockDockTypeCode.o1_dock,
702
+ RoborockDockTypeCode.oc_dock,
703
+ RoborockDockTypeCode.o2_dock,
704
+ RoborockDockTypeCode.o3_dock,
705
+ RoborockDockTypeCode.o3_plus_dock,
706
+ RoborockDockTypeCode.o4_dock,
707
+ }
708
+
709
+ _DUST_BUCKET_DOCK_TYPES = {
710
+ RoborockDockTypeCode.o3_dock,
711
+ RoborockDockTypeCode.o3_plus_dock,
712
+ RoborockDockTypeCode.o4_dock,
713
+ }
714
+
715
+ _CLEAN_CAROUSEL_SELF_CLEAN_DOCK_TYPES = {
716
+ RoborockDockTypeCode.shell_3_dock,
717
+ RoborockDockTypeCode.k1_dock,
718
+ RoborockDockTypeCode.couple_dock,
719
+ RoborockDockTypeCode.shell_3s_dock,
720
+ RoborockDockTypeCode.k1s_dock,
721
+ RoborockDockTypeCode.shell_3c_dock,
722
+ }
723
+
724
+ _WATER_UPDOWN_DRAIN_DOCK_TYPES = {
725
+ RoborockDockTypeCode.o4_dock,
726
+ RoborockDockTypeCode.pearl_plus_dock,
727
+ RoborockDockTypeCode.couple_dock,
728
+ }
729
+
730
+ _WATER_UPDOWN_DRAIN_AM_DOCK_TYPES = {
731
+ RoborockDockTypeCode.o5_dock,
732
+ RoborockDockTypeCode.shell_2s_dock,
733
+ RoborockDockTypeCode.shell_3_dock,
734
+ RoborockDockTypeCode.shell_2c_dock,
735
+ RoborockDockTypeCode.shell_3s_dock,
736
+ RoborockDockTypeCode.k1_dock,
737
+ RoborockDockTypeCode.k1s_dock,
738
+ RoborockDockTypeCode.o6_dock,
739
+ RoborockDockTypeCode.shell_e_dock,
740
+ RoborockDockTypeCode.shell_2e_dock,
741
+ RoborockDockTypeCode.shell_3c_dock,
742
+ }
743
+
744
+ _HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES = {
745
+ RoborockDockTypeCode.couple_dock,
746
+ }
747
+
748
+ _SPECIAL_WASH_TEMP_DOCK_TYPES = {
749
+ RoborockDockTypeCode.shell_2e_lite_dock,
750
+ }
751
+
752
+
753
+ @dataclass(frozen=True)
754
+ class RoborockDockFeatures:
755
+ """Capabilities derived from a V1 dock type.
756
+
757
+ This mirrors the Roborock app's DK capability model: feature availability
758
+ should be attached to the dock family instead of repeated per trait.
759
+ """
760
+
761
+ dock_type: RoborockDockTypeCode
762
+ has_am: bool | None = None
763
+
764
+ @classmethod
765
+ @cache
766
+ def from_dock_type(cls, dock_type: RoborockDockTypeCode | None, has_am: bool | None = None) -> Self:
767
+ """Return cached capabilities for a V1 dock type."""
768
+ return cls(dock_type or RoborockDockTypeCode.o0_dock, has_am=has_am)
769
+
770
+ def _matches_am_variant(self, dock_types: set[RoborockDockTypeCode]) -> bool:
771
+ return self.has_am is True and self.dock_type in dock_types
772
+
773
+ def _matches_non_am_variant(self, dock_types: set[RoborockDockTypeCode]) -> bool:
774
+ return self.has_am is not True and self.dock_type in dock_types
775
+
776
+ @property
777
+ def has_dock(self) -> bool:
778
+ return self.dock_type not in _NO_DOCK_TYPES
779
+
780
+ @property
781
+ def is_pure_collect(self) -> bool:
782
+ return self.dock_type in _PURE_COLLECT_DOCK_TYPES
783
+
784
+ @property
785
+ def is_pure_wash(self) -> bool:
786
+ return self.dock_type in _PURE_WASH_DOCK_TYPES
787
+
788
+ @property
789
+ def is_collect_wash(self) -> bool:
790
+ return self.dock_type in _COLLECT_WASH_DOCK_TYPES
791
+
792
+ @property
793
+ def is_collect_wash_dry(self) -> bool:
794
+ return self.has_dock and not (self.is_pure_collect or self.is_pure_wash or self.is_collect_wash)
795
+
796
+ @property
797
+ def is_collectable(self) -> bool:
798
+ return self.is_collect_wash or self.is_collect_wash_dry or self.is_pure_collect
799
+
800
+ @property
801
+ def is_washable(self) -> bool:
802
+ return self.is_pure_wash or self.is_collect_wash or self.is_collect_wash_dry
803
+
804
+ @property
805
+ def is_dryable(self) -> bool:
806
+ return self.is_collect_wash_dry
807
+
808
+ @property
809
+ def is_only_x_series(self) -> bool:
810
+ return self.dock_type in _ONLY_X_SERIES_DOCK_TYPES
811
+
812
+ @property
813
+ def is_cleaning_brush_supported(self) -> bool:
814
+ return self.is_only_x_series
815
+
816
+ @property
817
+ def is_clean_fluid_auto_delivery_supported(self) -> bool:
818
+ return self.dock_type in _CLEAN_FLUID_AUTO_DELIVERY_DOCK_TYPES or self._matches_am_variant(
819
+ _CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES
820
+ )
821
+
822
+ @property
823
+ def is_auto_sterilize_supported(self) -> bool:
824
+ return self.dock_type in _AUTO_STERILIZE_DOCK_TYPES
825
+
826
+ @property
827
+ def is_inner_charge_head(self) -> bool:
828
+ return self.dock_type in _INNER_CHARGE_HEAD_DOCK_TYPES
829
+
830
+ @property
831
+ def is_dust_bucket_supported(self) -> bool:
832
+ return self.dock_type in _DUST_BUCKET_DOCK_TYPES
833
+
834
+ @property
835
+ def is_clean_carousel_self_clean_supported(self) -> bool:
836
+ return self._matches_non_am_variant(_CLEAN_CAROUSEL_SELF_CLEAN_DOCK_TYPES)
837
+
838
+ @property
839
+ def is_water_updown_drain_supported(self) -> bool:
840
+ return self.dock_type in _WATER_UPDOWN_DRAIN_DOCK_TYPES or self._matches_am_variant(
841
+ _WATER_UPDOWN_DRAIN_AM_DOCK_TYPES
842
+ )
843
+
844
+ @property
845
+ def is_hatch_door_dock_cool_fan_supported(self) -> bool:
846
+ return self.dock_type in _HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES
847
+
848
+ @property
849
+ def is_special_support_wash_temp(self) -> bool:
850
+ return self.dock_type in _SPECIAL_WASH_TEMP_DOCK_TYPES
851
+
852
+
647
853
  WASH_N_FILL_DOCK_TYPES = [
648
- RoborockDockTypeCode.empty_wash_fill_dock,
649
- RoborockDockTypeCode.s8_dock,
650
- RoborockDockTypeCode.p10_dock,
651
- RoborockDockTypeCode.p10_pro_dock,
652
- RoborockDockTypeCode.s8_maxv_ultra_dock,
653
- RoborockDockTypeCode.qrevo_s_dock,
654
- RoborockDockTypeCode.saros_r10_dock,
655
- RoborockDockTypeCode.qrevo_curv_dock,
656
- RoborockDockTypeCode.qrevo_s5v_dock,
657
- RoborockDockTypeCode.saros_20_dock,
854
+ dock_type for dock_type in RoborockDockTypeCode if RoborockDockFeatures.from_dock_type(dock_type).is_washable
658
855
  ]
659
856
 
660
857
 
661
858
  def is_wash_n_fill_dock(dock_type: RoborockDockTypeCode) -> bool:
662
859
  """Check if the dock type is a wash and fill dock."""
663
- return dock_type in WASH_N_FILL_DOCK_TYPES
860
+ return RoborockDockFeatures.from_dock_type(dock_type).is_washable
664
861
 
665
862
 
666
863
  def is_valid_dock(dock_type: RoborockDockTypeCode) -> bool:
667
864
  """Check if device supports a dock."""
668
- return dock_type != RoborockDockTypeCode.no_dock
865
+ return RoborockDockFeatures.from_dock_type(dock_type).has_dock
@@ -42,8 +42,8 @@ optional traits:
42
42
  - `requires_feature` - The string name of the device feature that must be supported
43
43
  for this trait to be enabled. See `DeviceFeaturesTrait` for a list of
44
44
  available features.
45
- - `requires_dock_type` - If set, this is a function that accepts a `RoborockDockTypeCode`
46
- and returns a boolean indicating whether the trait is supported for that dock type.
45
+ - `requires_dock_features` - If set, this is a function that accepts a `RoborockDockFeatures`
46
+ and returns a boolean indicating whether the trait is supported for that dock.
47
47
 
48
48
  Additionally, DeviceFeaturesTrait has a method `is_field_supported` that is used to
49
49
  check individual trait field values. This is a more fine grained version to allow
@@ -59,6 +59,7 @@ from typing import Any, get_args
59
59
 
60
60
  from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase
61
61
  from roborock.data.v1.v1_code_mappings import RoborockDockTypeCode
62
+ from roborock.device_features import RoborockDockFeatures
62
63
  from roborock.devices.cache import DeviceCache
63
64
  from roborock.devices.traits import Trait
64
65
  from roborock.exceptions import RoborockException
@@ -262,9 +263,10 @@ class PropertiesApi(Trait):
262
263
  await self.device_features.refresh()
263
264
  # Dock type also acts like a device feature for some traits.
264
265
  dock_type = await self._dock_type()
266
+ dock_features = RoborockDockFeatures.from_dock_type(dock_type, has_am=self.status.has_am)
265
267
 
266
268
  # Initialize traits with special arguments before the generic loop
267
- if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_type):
269
+ if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_features):
268
270
  wash_towel_mode = WashTowelModeTrait(self.device_features)
269
271
  wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode) # type: ignore[assignment]
270
272
  self.wash_towel_mode = wash_towel_mode
@@ -280,7 +282,7 @@ class PropertiesApi(Trait):
280
282
 
281
283
  # Union args may not be in declared order
282
284
  item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
283
- if not self._is_supported(item_type, item.name, dock_type):
285
+ if not self._is_supported(item_type, item.name, dock_features):
284
286
  _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
285
287
  continue
286
288
  _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
@@ -288,11 +290,11 @@ class PropertiesApi(Trait):
288
290
  setattr(self, item.name, trait)
289
291
  trait._rpc_channel = self._get_rpc_channel(trait)
290
292
 
291
- def _is_supported(self, trait_type: type[V1TraitMixin], name: str, dock_type: RoborockDockTypeCode) -> bool:
293
+ def _is_supported(self, trait_type: type[V1TraitMixin], name: str, dock_features: RoborockDockFeatures) -> bool:
292
294
  """Check if a trait is supported by the device."""
293
295
 
294
- if (requires_dock_type := getattr(trait_type, "requires_dock_type", None)) is not None:
295
- return requires_dock_type(dock_type)
296
+ if (requires_dock_features := getattr(trait_type, "requires_dock_features", None)) is not None:
297
+ return requires_dock_features(dock_features)
296
298
 
297
299
  if (feature_name := getattr(trait_type, "requires_feature", None)) is None:
298
300
  _LOGGER.debug("Optional trait missing 'requires_feature' attribute %s, skipping", name)
@@ -307,16 +309,23 @@ class PropertiesApi(Trait):
307
309
  if dock_type is not None:
308
310
  _LOGGER.debug("Using cached dock type: %s", dock_type)
309
311
  try:
310
- return RoborockDockTypeCode(dock_type)
312
+ dock_type = RoborockDockTypeCode(dock_type)
311
313
  except ValueError:
312
314
  _LOGGER.debug("Cached dock type %s is invalid, refreshing", dock_type)
315
+ else:
316
+ if self.status.dss is None:
317
+ await self.status.refresh()
318
+ if self.status.dock_type is not None:
319
+ dock_type = self.status.dock_type
320
+ await self._set_cached_trait_data("dock_type", dock_type)
321
+ return dock_type
313
322
 
314
323
  _LOGGER.debug("Starting dock type discovery")
315
324
  await self.status.refresh()
316
325
  _LOGGER.debug("Fetched dock type: %s", self.status.dock_type)
317
326
  if self.status.dock_type is None:
318
327
  # Explicitly set so we reuse cached value next type
319
- dock_type = RoborockDockTypeCode.no_dock
328
+ dock_type = RoborockDockTypeCode.o0_dock
320
329
  else:
321
330
  dock_type = self.status.dock_type
322
331
  await self._set_cached_trait_data("dock_type", dock_type)
@@ -1,14 +1,18 @@
1
1
  """Trait for dust collection mode."""
2
2
 
3
3
  from roborock.data import DustCollectionMode
4
- from roborock.device_features import is_valid_dock
4
+ from roborock.device_features import RoborockDockFeatures
5
5
  from roborock.devices.traits.v1 import common
6
6
  from roborock.roborock_typing import RoborockCommand
7
7
 
8
8
 
9
+ def _supports_dust_collection_mode(dock_features: RoborockDockFeatures) -> bool:
10
+ return dock_features.is_collectable
11
+
12
+
9
13
  class DustCollectionModeTrait(DustCollectionMode, common.V1TraitMixin):
10
14
  """Trait for dust collection mode."""
11
15
 
12
16
  command = RoborockCommand.GET_DUST_COLLECTION_MODE
13
17
  converter = common.DefaultConverter(DustCollectionMode)
14
- requires_dock_type = is_valid_dock
18
+ requires_dock_features = _supports_dust_collection_mode
@@ -1,14 +1,18 @@
1
1
  """Trait for smart wash parameters."""
2
2
 
3
3
  from roborock.data import SmartWashParams
4
- from roborock.device_features import is_wash_n_fill_dock
4
+ from roborock.device_features import RoborockDockFeatures
5
5
  from roborock.devices.traits.v1 import common
6
6
  from roborock.roborock_typing import RoborockCommand
7
7
 
8
8
 
9
+ def _supports_smart_wash_params(dock_features: RoborockDockFeatures) -> bool:
10
+ return dock_features.is_washable
11
+
12
+
9
13
  class SmartWashParamsTrait(SmartWashParams, common.V1TraitMixin):
10
14
  """Trait for smart wash parameters."""
11
15
 
12
16
  command = RoborockCommand.GET_SMART_WASH_PARAMS
13
17
  converter = common.DefaultConverter(SmartWashParams)
14
- requires_dock_type = is_wash_n_fill_dock
18
+ requires_dock_features = _supports_smart_wash_params
@@ -4,18 +4,22 @@ from functools import cached_property
4
4
  from typing import Self
5
5
 
6
6
  from roborock.data import WashTowelMode, WashTowelModes, get_wash_towel_modes
7
- from roborock.device_features import is_wash_n_fill_dock
7
+ from roborock.device_features import RoborockDockFeatures
8
8
  from roborock.devices.traits.v1 import common
9
9
  from roborock.devices.traits.v1.device_features import DeviceFeaturesTrait
10
10
  from roborock.roborock_typing import RoborockCommand
11
11
 
12
12
 
13
+ def _supports_wash_towel_mode(dock_features: RoborockDockFeatures) -> bool:
14
+ return dock_features.is_washable
15
+
16
+
13
17
  class WashTowelModeTrait(WashTowelMode, common.V1TraitMixin):
14
18
  """Trait for wash towel mode."""
15
19
 
16
20
  command = RoborockCommand.GET_WASH_TOWEL_MODE
17
21
  converter = common.DefaultConverter(WashTowelMode)
18
- requires_dock_type = is_wash_n_fill_dock
22
+ requires_dock_features = _supports_wash_towel_mode
19
23
 
20
24
  def __init__(
21
25
  self,
@@ -5,7 +5,7 @@ subscription, and publishing logic at the message boundary. It acts as an
5
5
  in-memory replacement for `MqttChannel` and `LocalChannel` during testing.
6
6
  """
7
7
 
8
- from collections.abc import Callable
8
+ from collections.abc import Awaitable, Callable
9
9
  from typing import Any
10
10
  from unittest.mock import AsyncMock, MagicMock
11
11
 
@@ -37,6 +37,9 @@ class FakeChannel(Channel):
37
37
  publish (useful for low-level RPC request/response testing).
38
38
  - **Push unsolicited messages**: Call ``channel.notify_subscribers(msg)``
39
39
  to simulate the device broadcasting a state change.
40
+ - **Intercept published messages**: Register a handler/callback via
41
+ ``channel.publish_handler = my_handler`` (e.g. stateful simulator)
42
+ to reactively process commands.
40
43
  """
41
44
 
42
45
  subscribe: Any
@@ -49,6 +52,11 @@ class FakeChannel(Channel):
49
52
  self._is_connected = False
50
53
  self._is_local = is_local
51
54
 
55
+ # A callback to intercept published messages (e.g., bound simulator handler).
56
+ # Can be either synchronous or asynchronous: Callable[[RoborockMessage], Awaitable[Any]].
57
+ # By default, routes to self._default_publish_handler to handle the response_queue.
58
+ self.publish_handler: Callable[[RoborockMessage], Awaitable[Any]] | None = self._default_publish_handler
59
+
52
60
  # Set this to an exception instance to make the next publish raise it.
53
61
  # This is a convenience shortcut; callers can also replace
54
62
  # ``publish.side_effect`` directly for more control.
@@ -96,14 +104,16 @@ class FakeChannel(Channel):
96
104
  async def _publish(self, message: RoborockMessage) -> None:
97
105
  """Default publish implementation.
98
106
 
99
- Records the message in ``published_messages`` and, if
100
- ``response_queue`` is non-empty, pops the first response and
101
- delivers it to all current subscribers (simulating a
102
- request/response round-trip).
107
+ Records the message in ``published_messages`` and executes ``publish_handler``.
103
108
  """
104
109
  self.published_messages.append(message)
105
110
  if self.publish_side_effect:
106
111
  raise self.publish_side_effect
112
+ if self.publish_handler:
113
+ await self.publish_handler(message)
114
+
115
+ async def _default_publish_handler(self, message: RoborockMessage) -> None:
116
+ """Default handler that pops canned responses from response_queue."""
107
117
  if self.response_queue:
108
118
  response = self.response_queue.pop(0)
109
119
  self.notify_subscribers(response)
@@ -124,3 +134,15 @@ class FakeChannel(Channel):
124
134
  """
125
135
  for subscriber in list(self.subscribers):
126
136
  subscriber(message)
137
+
138
+ def inject_error(self, exception: Exception) -> None:
139
+ """Inject a transient failure into all channel operations (publish, subscribe, connect)."""
140
+ self.publish.side_effect = exception
141
+ self.subscribe.side_effect = exception
142
+ self.connect.side_effect = exception
143
+
144
+ def clear_error(self) -> None:
145
+ """Restore default success behaviors on all channel operations."""
146
+ self.publish.side_effect = self._publish
147
+ self.subscribe.side_effect = self._subscribe
148
+ self.connect.side_effect = self._connect
@@ -6,6 +6,7 @@ to simulate physical devices connected to the Roborock Cloud.
6
6
  """
7
7
 
8
8
  import contextlib
9
+ import datetime
9
10
  import re
10
11
  from typing import Any
11
12
  from unittest.mock import AsyncMock, patch
@@ -173,6 +174,44 @@ class FakeWebApiClient:
173
174
  callback=get_homes_callback,
174
175
  )
175
176
 
177
+ def get_default_home_data(self) -> HomeData:
178
+ """Construct the default HomeData using simulated devices registered in the cloud."""
179
+ devices = [d.device_info for d in self.cloud.simulated_devices.values()]
180
+ products = [d.product for d in self.cloud.simulated_devices.values()]
181
+ return HomeData(
182
+ id=self.cloud.home_id,
183
+ name=self.cloud.home_name,
184
+ devices=devices,
185
+ products=products,
186
+ received_devices=[],
187
+ rooms=[],
188
+ )
189
+
190
+ def set_homes_response(
191
+ self,
192
+ home_data: HomeData | None = None,
193
+ status: int = 200,
194
+ ) -> None:
195
+ """Easily set the faked response payload for the homes endpoint using a HomeData dataclass.
196
+
197
+ If home_data is None, it defaults to the active get_default_home_data() state.
198
+ """
199
+ self.homes_status = status
200
+ if status != 200:
201
+ self.homes_payload_override = None
202
+ return
203
+
204
+ if home_data is None:
205
+ home_data = self.get_default_home_data()
206
+
207
+ self.homes_payload_override = {
208
+ "api": None,
209
+ "code": 200,
210
+ "result": home_data.as_dict(),
211
+ "status": "ok",
212
+ "success": True,
213
+ }
214
+
176
215
 
177
216
  class FakeRoborockCloud:
178
217
  """A central state object representing the Roborock Cloud environment under test."""
@@ -234,7 +273,9 @@ class FakeRoborockCloud:
234
273
  with aioresponses() as mocked:
235
274
  self.web_api.mock_requests(mocked)
236
275
 
237
- # Patch Channel factories and rate limiters
276
+ # Mock sleep logic to speed up tests.
277
+ sleep_time = datetime.timedelta(seconds=0.001)
278
+ # Patch Channel factories, rate limiters, and backoff sleep intervals
238
279
  with (
239
280
  patch(
240
281
  "roborock.web_api.RoborockApiClient._login_limiter.try_acquire_async",
@@ -243,5 +284,7 @@ class FakeRoborockCloud:
243
284
  patch("roborock.web_api.RoborockApiClient._home_data_limiter.try_acquire", return_value=True),
244
285
  patch("roborock.devices.device_manager.create_v1_channel", side_effect=mock_create_v1_channel),
245
286
  patch("roborock.devices.device_manager.create_mqtt_channel", side_effect=mock_create_mqtt_channel),
287
+ patch("roborock.devices.device.MIN_BACKOFF_INTERVAL", sleep_time),
288
+ patch("roborock.devices.device.MAX_BACKOFF_INTERVAL", sleep_time),
246
289
  ):
247
290
  yield
@@ -73,26 +73,20 @@ class RoborockDeviceSimulator:
73
73
 
74
74
  # MQTT channel is always present — all protocols use it.
75
75
  self.mqtt_channel = FakeChannel(is_local=False)
76
- self.mqtt_channel.publish.side_effect = self._handle_mqtt_publish
76
+ self.mqtt_channel.publish_handler = self._handle_mqtt_publish
77
77
 
78
78
  # Local channel is only used by V1 devices. A01/B01 (MQTT-only)
79
79
  # simulators should pass has_local_channel=False.
80
80
  self.local_channel: FakeChannel | None = None
81
81
  if has_local_channel:
82
82
  self.local_channel = FakeChannel(is_local=True)
83
- self.local_channel.publish.side_effect = self._handle_local_publish
83
+ self.local_channel.publish_handler = self._handle_local_publish
84
84
 
85
85
  async def _handle_local_publish(self, message: RoborockMessage) -> None:
86
86
  assert self.local_channel is not None
87
- self.local_channel.published_messages.append(message)
88
- if self.local_channel.publish_side_effect:
89
- raise self.local_channel.publish_side_effect
90
87
  await self._handle_publish(message, self.local_channel)
91
88
 
92
89
  async def _handle_mqtt_publish(self, message: RoborockMessage) -> None:
93
- self.mqtt_channel.published_messages.append(message)
94
- if self.mqtt_channel.publish_side_effect:
95
- raise self.mqtt_channel.publish_side_effect
96
90
  await self._handle_publish(message, self.mqtt_channel)
97
91
 
98
92
  async def _handle_publish(self, message: RoborockMessage, channel: FakeChannel) -> None:
@@ -81,7 +81,7 @@ DEFAULT_STATUS = StatusV2(
81
81
  home_sec_enable_password=0,
82
82
  adbumper_status=[0, 0, 0],
83
83
  water_shortage_status=0,
84
- dock_type=RoborockDockTypeCode.s8_dock,
84
+ dock_type=RoborockDockTypeCode.o4_dock,
85
85
  dust_collection_status=0,
86
86
  auto_dust_collection=1,
87
87
  avoid_count=19,