python-roborock 5.26.0__tar.gz → 5.28.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 (124) hide show
  1. {python_roborock-5.26.0 → python_roborock-5.28.0}/PKG-INFO +1 -1
  2. {python_roborock-5.26.0 → python_roborock-5.28.0}/pyproject.toml +1 -1
  3. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/v1/v1_code_mappings.py +25 -16
  4. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/v1/v1_containers.py +12 -0
  5. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/device_features.py +207 -12
  6. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/__init__.py +18 -9
  7. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/dust_collection_mode.py +6 -2
  8. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/smart_wash_params.py +6 -2
  9. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/wash_towel_mode.py +6 -2
  10. python_roborock-5.28.0/roborock/testing/__init__.py +101 -0
  11. python_roborock-5.28.0/roborock/testing/channel.py +126 -0
  12. python_roborock-5.28.0/roborock/testing/cloud.py +247 -0
  13. python_roborock-5.28.0/roborock/testing/simulator.py +110 -0
  14. python_roborock-5.28.0/roborock/testing/v1_simulator.py +395 -0
  15. {python_roborock-5.26.0 → python_roborock-5.28.0}/.gitignore +0 -0
  16. {python_roborock-5.26.0 → python_roborock-5.28.0}/LICENSE +0 -0
  17. {python_roborock-5.26.0 → python_roborock-5.28.0}/README.md +0 -0
  18. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/__init__.py +0 -0
  19. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/broadcast_protocol.py +0 -0
  20. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/callbacks.py +0 -0
  21. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/cli.py +0 -0
  22. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/const.py +0 -0
  23. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/__init__.py +0 -0
  24. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/b01_q10/__init__.py +0 -0
  25. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  26. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  27. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/b01_q7/__init__.py +0 -0
  28. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  29. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  30. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/code_mappings.py +0 -0
  31. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/containers.py +0 -0
  32. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/dyad/__init__.py +0 -0
  33. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  34. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/dyad/dyad_containers.py +0 -0
  35. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/mower/__init__.py +0 -0
  36. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/mower/mower_containers.py +0 -0
  37. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/v1/__init__.py +0 -0
  38. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  39. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/zeo/__init__.py +0 -0
  40. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  41. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/data/zeo/zeo_containers.py +0 -0
  42. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/README.md +0 -0
  43. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/__init__.py +0 -0
  44. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/cache.py +0 -0
  45. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/device.py +0 -0
  46. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/device_manager.py +0 -0
  47. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/file_cache.py +0 -0
  48. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/rpc/__init__.py +0 -0
  49. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/rpc/a01_channel.py +0 -0
  50. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  51. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  52. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/rpc/v1_channel.py +0 -0
  53. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/__init__.py +0 -0
  54. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/a01/__init__.py +0 -0
  55. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/__init__.py +0 -0
  56. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  57. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/button_light.py +0 -0
  58. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
  59. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
  60. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  61. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  62. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/consumable.py +0 -0
  63. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
  64. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
  65. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/map.py +0 -0
  66. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/network_info.py +0 -0
  67. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
  68. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  69. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  70. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q10/volume.py +0 -0
  71. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  72. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  73. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q7/map.py +0 -0
  74. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
  75. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/common.py +0 -0
  76. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/traits_mixin.py +0 -0
  77. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  78. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  79. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/command.py +0 -0
  80. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/common.py +0 -0
  81. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  82. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/device_features.py +0 -0
  83. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  84. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  85. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/home.py +0 -0
  86. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/led_status.py +0 -0
  87. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/map_content.py +0 -0
  88. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/maps.py +0 -0
  89. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/network_info.py +0 -0
  90. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/rooms.py +0 -0
  91. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/routines.py +0 -0
  92. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/status.py +0 -0
  93. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  94. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/traits/v1/volume.py +0 -0
  95. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/transport/__init__.py +0 -0
  96. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/transport/channel.py +0 -0
  97. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/transport/local_channel.py +0 -0
  98. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  99. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/diagnostics.py +0 -0
  100. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/exceptions.py +0 -0
  101. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/__init__.py +0 -0
  102. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/b01_grid_layers.py +0 -0
  103. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/b01_map_parser.py +0 -0
  104. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/b01_q10_map_parser.py +0 -0
  105. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/b01_q10_overlays.py +0 -0
  106. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/map_parser.py +0 -0
  107. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/proto/__init__.py +0 -0
  108. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/proto/b01_scmap.proto +0 -0
  109. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  110. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/mqtt/__init__.py +0 -0
  111. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/mqtt/health_manager.py +0 -0
  112. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/mqtt/roborock_session.py +0 -0
  113. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/mqtt/session.py +0 -0
  114. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/protocol.py +0 -0
  115. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/protocols/__init__.py +0 -0
  116. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/protocols/a01_protocol.py +0 -0
  117. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  118. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  119. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/protocols/v1_protocol.py +0 -0
  120. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/py.typed +0 -0
  121. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/roborock_message.py +0 -0
  122. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/roborock_typing.py +0 -0
  123. {python_roborock-5.26.0 → python_roborock-5.28.0}/roborock/util.py +0 -0
  124. {python_roborock-5.26.0 → python_roborock-5.28.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.26.0
3
+ Version: 5.28.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.26.0"
3
+ version = "5.28.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"
@@ -650,22 +650,31 @@ 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
669
678
 
670
679
 
671
680
  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,219 @@ 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.pearl_plus_dock,
678
+ RoborockDockTypeCode.shell_3s_dock,
679
+ RoborockDockTypeCode.o6_dock,
680
+ RoborockDockTypeCode.couple_dock,
681
+ }
682
+
683
+ _CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES = {
684
+ RoborockDockTypeCode.o5_dock,
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.o6_dock,
738
+ RoborockDockTypeCode.shell_e_dock,
739
+ RoborockDockTypeCode.shell_2e_dock,
740
+ }
741
+
742
+ _HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES = {
743
+ RoborockDockTypeCode.couple_dock,
744
+ }
745
+
746
+ _SPECIAL_WASH_TEMP_DOCK_TYPES = {
747
+ RoborockDockTypeCode.shell_2e_lite_dock,
748
+ }
749
+
750
+
751
+ @dataclass(frozen=True)
752
+ class RoborockDockFeatures:
753
+ """Capabilities derived from a V1 dock type.
754
+
755
+ This mirrors the Roborock app's DK capability model: feature availability
756
+ should be attached to the dock family instead of repeated per trait.
757
+ """
758
+
759
+ dock_type: RoborockDockTypeCode
760
+ has_am: bool | None = None
761
+
762
+ @classmethod
763
+ @cache
764
+ def from_dock_type(cls, dock_type: RoborockDockTypeCode | None, has_am: bool | None = None) -> Self:
765
+ """Return cached capabilities for a V1 dock type."""
766
+ return cls(dock_type or RoborockDockTypeCode.o0_dock, has_am=has_am)
767
+
768
+ def _matches_am_variant(self, dock_types: set[RoborockDockTypeCode]) -> bool:
769
+ return self.has_am is True and self.dock_type in dock_types
770
+
771
+ def _matches_non_am_variant(self, dock_types: set[RoborockDockTypeCode]) -> bool:
772
+ return self.has_am is not True and self.dock_type in dock_types
773
+
774
+ @property
775
+ def has_dock(self) -> bool:
776
+ return self.dock_type not in _NO_DOCK_TYPES
777
+
778
+ @property
779
+ def is_pure_collect(self) -> bool:
780
+ return self.dock_type in _PURE_COLLECT_DOCK_TYPES
781
+
782
+ @property
783
+ def is_pure_wash(self) -> bool:
784
+ return self.dock_type in _PURE_WASH_DOCK_TYPES
785
+
786
+ @property
787
+ def is_collect_wash(self) -> bool:
788
+ return self.dock_type in _COLLECT_WASH_DOCK_TYPES
789
+
790
+ @property
791
+ def is_collect_wash_dry(self) -> bool:
792
+ return self.has_dock and not (self.is_pure_collect or self.is_pure_wash or self.is_collect_wash)
793
+
794
+ @property
795
+ def is_collectable(self) -> bool:
796
+ return self.is_collect_wash or self.is_collect_wash_dry or self.is_pure_collect
797
+
798
+ @property
799
+ def is_washable(self) -> bool:
800
+ return self.is_pure_wash or self.is_collect_wash or self.is_collect_wash_dry
801
+
802
+ @property
803
+ def is_dryable(self) -> bool:
804
+ return self.is_collect_wash_dry
805
+
806
+ @property
807
+ def is_only_x_series(self) -> bool:
808
+ return self.dock_type in _ONLY_X_SERIES_DOCK_TYPES
809
+
810
+ @property
811
+ def is_cleaning_brush_supported(self) -> bool:
812
+ return self.is_only_x_series
813
+
814
+ @property
815
+ def is_clean_fluid_auto_delivery_supported(self) -> bool:
816
+ return self.dock_type in _CLEAN_FLUID_AUTO_DELIVERY_DOCK_TYPES or self._matches_am_variant(
817
+ _CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES
818
+ )
819
+
820
+ @property
821
+ def is_auto_sterilize_supported(self) -> bool:
822
+ return self.dock_type in _AUTO_STERILIZE_DOCK_TYPES
823
+
824
+ @property
825
+ def is_inner_charge_head(self) -> bool:
826
+ return self.dock_type in _INNER_CHARGE_HEAD_DOCK_TYPES
827
+
828
+ @property
829
+ def is_dust_bucket_supported(self) -> bool:
830
+ return self.dock_type in _DUST_BUCKET_DOCK_TYPES
831
+
832
+ @property
833
+ def is_clean_carousel_self_clean_supported(self) -> bool:
834
+ return self._matches_non_am_variant(_CLEAN_CAROUSEL_SELF_CLEAN_DOCK_TYPES)
835
+
836
+ @property
837
+ def is_water_updown_drain_supported(self) -> bool:
838
+ return self.dock_type in _WATER_UPDOWN_DRAIN_DOCK_TYPES or self._matches_am_variant(
839
+ _WATER_UPDOWN_DRAIN_AM_DOCK_TYPES
840
+ )
841
+
842
+ @property
843
+ def is_hatch_door_dock_cool_fan_supported(self) -> bool:
844
+ return self.dock_type in _HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES
845
+
846
+ @property
847
+ def is_special_support_wash_temp(self) -> bool:
848
+ return self.dock_type in _SPECIAL_WASH_TEMP_DOCK_TYPES
849
+
850
+
647
851
  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,
852
+ dock_type for dock_type in RoborockDockTypeCode if RoborockDockFeatures.from_dock_type(dock_type).is_washable
658
853
  ]
659
854
 
660
855
 
661
856
  def is_wash_n_fill_dock(dock_type: RoborockDockTypeCode) -> bool:
662
857
  """Check if the dock type is a wash and fill dock."""
663
- return dock_type in WASH_N_FILL_DOCK_TYPES
858
+ return RoborockDockFeatures.from_dock_type(dock_type).is_washable
664
859
 
665
860
 
666
861
  def is_valid_dock(dock_type: RoborockDockTypeCode) -> bool:
667
862
  """Check if device supports a dock."""
668
- return dock_type != RoborockDockTypeCode.no_dock
863
+ 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,
@@ -0,0 +1,101 @@
1
+ """Testing fakes and simulators for python-roborock.
2
+
3
+ This package provides stateful firmware simulators (e.g. `V1VacuumSimulator`),
4
+ fake transport channels (`FakeChannel`), and cloud orchestration simulators (`FakeRoborockCloud`)
5
+ to allow downstream consumers (such as Home Assistant integrations) to write high-fidelity
6
+ integration tests using the real client library classes instead of fragile top-level mocks.
7
+
8
+ Testing Architecture & Boundaries
9
+ ---------------------------------
10
+ We fake communication at two boundaries:
11
+ 1. **Network HTTP API Interception**: `FakeRoborockCloud.patch_device_manager()` routes
12
+ HTTP requests (such as discovery, login, home details) to custom mock endpoints using
13
+ `aioresponses` under the hood. No Python client methods are mocked; the real EAPI client
14
+ executes fully.
15
+ 2. **Plaintext RPC Message Interception**: Device communication is intercepted at the
16
+ plaintext JSON RPC level (Layer 2). The real client classes (`V1Channel`, `MqttChannel`)
17
+ run under test, but their transport calls are intercepted by our stateful simulators.
18
+
19
+ ┌────────────────────────────────────────────────────────┐
20
+ │ TESTED CLIENT (REAL CODE) │
21
+ │ │
22
+ │ RoborockDevice / Traits / V1RpcChannel / V1Channel │
23
+ └──────────────────────────┬─────────────────────────────┘
24
+
25
+ ROBOROCKMESSAGE PAYLOADS
26
+ (Plaintext JSON commands)
27
+
28
+ ┌──────────────────────────▼─────────────────────────────┐
29
+ │ SIMULATOR (TEST FAKE) │
30
+ │ │
31
+ │ FakeChannel (Intercepts publish/subscribe) │
32
+ │ RoborockDeviceSimulator (Stateful firmware simulator) │
33
+ └────────────────────────────────────────────────────────┘
34
+
35
+ Integration Usage Example
36
+ -------------------------
37
+ ```python
38
+ from roborock.testing import FakeRoborockCloud, V1VacuumSimulator
39
+
40
+ async def test_start_vacuum_service():
41
+ # Setup cloud state and add a simulated vacuum device
42
+ cloud = FakeRoborockCloud()
43
+ fake_device = V1VacuumSimulator(duid="living_room_s7", battery=100, state=RoborockStateCode.charging)
44
+ cloud.add_device(fake_device)
45
+
46
+ # Patch channels and API calls using our cloud context manager
47
+ with cloud.patch_device_manager():
48
+ # Create the real client manager (logins and discovers natively via mock HTTP)
49
+ manager = await create_device_manager(
50
+ user_params=UserParams(username="test_user", user_data=USER_DATA),
51
+ cache=InMemoryCache(),
52
+ )
53
+
54
+ # Fetch the discovered device client
55
+ devices = await manager.get_devices()
56
+ device = devices[0]
57
+
58
+ # Trigger client start command
59
+ await device.v1_properties.command.send("app_start")
60
+
61
+ # Assert against the simulated vacuum state
62
+ assert fake_device.state == RoborockStateCode.cleaning
63
+ ```
64
+ """
65
+
66
+ from roborock.testing.channel import FakeChannel
67
+ from roborock.testing.cloud import FakeRoborockCloud, FakeWebApiClient
68
+ from roborock.testing.simulator import (
69
+ DEFAULT_KEY_T,
70
+ DEFAULT_LOCAL_KEY,
71
+ DEFAULT_PRODUCT_ID,
72
+ RoborockDeviceSimulator,
73
+ )
74
+ from roborock.testing.v1_simulator import (
75
+ DEFAULT_APP_INIT,
76
+ DEFAULT_CLEAN_SUMMARY,
77
+ DEFAULT_CONSUMABLE,
78
+ DEFAULT_DND_TIMER,
79
+ DEFAULT_LAST_CLEAN_RECORD,
80
+ DEFAULT_NETWORK_INFO,
81
+ DEFAULT_STATUS,
82
+ V1VacuumSimulator,
83
+ )
84
+
85
+ __all__ = [
86
+ "DEFAULT_APP_INIT",
87
+ "DEFAULT_CLEAN_SUMMARY",
88
+ "DEFAULT_CONSUMABLE",
89
+ "DEFAULT_DND_TIMER",
90
+ "DEFAULT_KEY_T",
91
+ "DEFAULT_LAST_CLEAN_RECORD",
92
+ "DEFAULT_LOCAL_KEY",
93
+ "DEFAULT_NETWORK_INFO",
94
+ "DEFAULT_PRODUCT_ID",
95
+ "DEFAULT_STATUS",
96
+ "FakeChannel",
97
+ "FakeRoborockCloud",
98
+ "FakeWebApiClient",
99
+ "RoborockDeviceSimulator",
100
+ "V1VacuumSimulator",
101
+ ]