python-roborock 5.36.0__tar.gz → 5.37.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 (127) hide show
  1. {python_roborock-5.36.0 → python_roborock-5.37.1}/PKG-INFO +1 -1
  2. {python_roborock-5.36.0 → python_roborock-5.37.1}/pyproject.toml +1 -1
  3. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/v1/v1_code_mappings.py +17 -0
  4. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/device_features.py +110 -0
  5. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/device_manager.py +1 -0
  6. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/rpc/v1_channel.py +52 -0
  7. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/__init__.py +17 -0
  8. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/common.py +1 -1
  9. python_roborock-5.37.1/roborock/devices/traits/v1/obstacle_photos.py +104 -0
  10. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/protocols/v1_protocol.py +45 -0
  11. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/roborock_typing.py +1 -0
  12. {python_roborock-5.36.0 → python_roborock-5.37.1}/.gitignore +0 -0
  13. {python_roborock-5.36.0 → python_roborock-5.37.1}/LICENSE +0 -0
  14. {python_roborock-5.36.0 → python_roborock-5.37.1}/README.md +0 -0
  15. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/__init__.py +0 -0
  16. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/broadcast_protocol.py +0 -0
  17. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/callbacks.py +0 -0
  18. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/cli.py +0 -0
  19. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/const.py +0 -0
  20. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/__init__.py +0 -0
  21. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/b01_q10/__init__.py +0 -0
  22. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  23. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  24. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/b01_q7/__init__.py +0 -0
  25. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  26. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  27. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/code_mappings.py +0 -0
  28. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/containers.py +0 -0
  29. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/dyad/__init__.py +0 -0
  30. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  31. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/dyad/dyad_containers.py +0 -0
  32. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/mower/__init__.py +0 -0
  33. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/mower/mower_containers.py +0 -0
  34. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/v1/__init__.py +0 -0
  35. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/v1/v1_clean_modes.py +0 -0
  36. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/v1/v1_containers.py +0 -0
  37. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/zeo/__init__.py +0 -0
  38. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  39. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/data/zeo/zeo_containers.py +0 -0
  40. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/README.md +0 -0
  41. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/__init__.py +0 -0
  42. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/cache.py +0 -0
  43. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/device.py +0 -0
  44. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/file_cache.py +0 -0
  45. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/rpc/__init__.py +0 -0
  46. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/rpc/a01_channel.py +0 -0
  47. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  48. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  49. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/__init__.py +0 -0
  50. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/a01/__init__.py +0 -0
  51. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/__init__.py +0 -0
  52. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  53. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/button_light.py +0 -0
  54. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
  55. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
  56. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/command.py +0 -0
  57. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/common.py +0 -0
  58. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/consumable.py +0 -0
  59. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
  60. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
  61. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/map.py +0 -0
  62. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/network_info.py +0 -0
  63. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/remote.py +0 -0
  64. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/status.py +0 -0
  65. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  66. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q10/volume.py +0 -0
  67. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  68. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  69. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q7/map.py +0 -0
  70. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/b01/q7/map_content.py +0 -0
  71. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/common.py +0 -0
  72. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/traits_mixin.py +0 -0
  73. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/child_lock.py +0 -0
  74. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/clean_summary.py +0 -0
  75. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/command.py +0 -0
  76. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/consumeable.py +0 -0
  77. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/device_features.py +0 -0
  78. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  79. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  80. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  81. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/home.py +0 -0
  82. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/led_status.py +0 -0
  83. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/map_content.py +0 -0
  84. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/maps.py +0 -0
  85. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/network_info.py +0 -0
  86. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/rooms.py +0 -0
  87. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/routines.py +0 -0
  88. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  89. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/status.py +0 -0
  90. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  91. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/volume.py +0 -0
  92. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  93. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/transport/__init__.py +0 -0
  94. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/transport/channel.py +0 -0
  95. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/transport/local_channel.py +0 -0
  96. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/devices/transport/mqtt_channel.py +0 -0
  97. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/diagnostics.py +0 -0
  98. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/exceptions.py +0 -0
  99. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/__init__.py +0 -0
  100. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/b01_grid_layers.py +0 -0
  101. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/b01_map_parser.py +0 -0
  102. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/b01_q10_map_parser.py +0 -0
  103. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/b01_q10_overlays.py +0 -0
  104. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/b01_q10_render.py +0 -0
  105. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/map_parser.py +0 -0
  106. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/proto/__init__.py +0 -0
  107. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/proto/b01_scmap.proto +0 -0
  108. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  109. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/mqtt/__init__.py +0 -0
  110. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/mqtt/health_manager.py +0 -0
  111. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/mqtt/roborock_session.py +0 -0
  112. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/mqtt/session.py +0 -0
  113. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/protocol.py +0 -0
  114. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/protocols/__init__.py +0 -0
  115. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/protocols/a01_protocol.py +0 -0
  116. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/protocols/b01_q10_protocol.py +0 -0
  117. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/protocols/b01_q7_protocol.py +0 -0
  118. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/py.typed +0 -0
  119. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/roborock_message.py +0 -0
  120. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/testing/__init__.py +0 -0
  121. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/testing/b01_q10_simulator.py +0 -0
  122. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/testing/channel.py +0 -0
  123. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/testing/cloud.py +0 -0
  124. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/testing/simulator.py +0 -0
  125. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/testing/v1_simulator.py +0 -0
  126. {python_roborock-5.36.0 → python_roborock-5.37.1}/roborock/util.py +0 -0
  127. {python_roborock-5.36.0 → python_roborock-5.37.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.36.0
3
+ Version: 5.37.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.36.0"
3
+ version = "5.37.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"
@@ -365,10 +365,27 @@ class RoborockDockTypeCode(RoborockEnum):
365
365
  shell_e_dock = 21
366
366
  shell_2e_dock = 22
367
367
  shell_3c_dock = 23
368
+ hera_dock = 24
369
+ k1s_pro_dock = 26
368
370
  type_27_dock = 27
369
371
  k1c_lite_dock = 28
372
+ k1r_dock = 29
370
373
  shell_2e_lite_dock = 30
374
+ shell_4rc_dock = 31
375
+ shell_4r_dock = 32
376
+ shell_4p_dock = 33
377
+ shell_4s_dock = 34
378
+ o7_dock = 35
379
+ k1d_dock = 37
380
+ shell_3p_dock = 38
381
+ shell_4d_dock = 39
371
382
  shell_2e_heat_dock = 40
383
+ f1_dock = 41
384
+ f1r_dock = 42
385
+ f2r_dock = 43
386
+ f1s_dock = 44
387
+ o7h_dock = 45
388
+ f1c_dock = 46
372
389
 
373
390
 
374
391
  class RoborockDockDustCollectionModeCode(RoborockEnum):
@@ -539,6 +539,12 @@ class DeviceFeatures(RoborockBase):
539
539
  ]
540
540
  }
541
541
  )
542
+ is_ai_recognition_setting_supported: bool = field(
543
+ metadata={"product_features": [ProductFeatures.AIRECOGNITION_SETTING]}
544
+ )
545
+ is_ai_recognition_obstacle_supported: bool = field(
546
+ metadata={"product_features": [ProductFeatures.AIRECOGNITION_OBSTACLE]}
547
+ )
542
548
  is_pure_clean_mop_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_PURECLEANMOP]})
543
549
  is_new_remote_view_supported: bool = field(metadata={"product_features": [ProductFeatures.REMOTE_BACK]})
544
550
  is_max_plus_mode_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_MAXPLUS]})
@@ -670,6 +676,8 @@ _ONLY_X_SERIES_DOCK_TYPES = {
670
676
  RoborockDockTypeCode.o4_dock,
671
677
  RoborockDockTypeCode.o5_dock,
672
678
  RoborockDockTypeCode.o6_dock,
679
+ RoborockDockTypeCode.o7_dock,
680
+ RoborockDockTypeCode.o7h_dock,
673
681
  }
674
682
 
675
683
  _CLEAN_FLUID_AUTO_DELIVERY_DOCK_TYPES = {
@@ -679,6 +687,10 @@ _CLEAN_FLUID_AUTO_DELIVERY_DOCK_TYPES = {
679
687
  RoborockDockTypeCode.shell_3s_dock,
680
688
  RoborockDockTypeCode.o6_dock,
681
689
  RoborockDockTypeCode.couple_dock,
690
+ RoborockDockTypeCode.type_27_dock,
691
+ RoborockDockTypeCode.o7_dock,
692
+ RoborockDockTypeCode.f1s_dock,
693
+ RoborockDockTypeCode.o7h_dock,
682
694
  }
683
695
 
684
696
  _CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES = {
@@ -690,6 +702,7 @@ _CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES = {
690
702
  RoborockDockTypeCode.shell_e_dock,
691
703
  RoborockDockTypeCode.shell_2e_dock,
692
704
  RoborockDockTypeCode.shell_3c_dock,
705
+ RoborockDockTypeCode.k1c_dock,
693
706
  }
694
707
 
695
708
  _AUTO_STERILIZE_DOCK_TYPES = {
@@ -719,6 +732,18 @@ _CLEAN_CAROUSEL_SELF_CLEAN_DOCK_TYPES = {
719
732
  RoborockDockTypeCode.shell_3s_dock,
720
733
  RoborockDockTypeCode.k1s_dock,
721
734
  RoborockDockTypeCode.shell_3c_dock,
735
+ RoborockDockTypeCode.k1s_pro_dock,
736
+ RoborockDockTypeCode.type_27_dock,
737
+ RoborockDockTypeCode.k1r_dock,
738
+ RoborockDockTypeCode.shell_4rc_dock,
739
+ RoborockDockTypeCode.shell_4p_dock,
740
+ RoborockDockTypeCode.shell_4s_dock,
741
+ RoborockDockTypeCode.k1d_dock,
742
+ RoborockDockTypeCode.shell_3p_dock,
743
+ RoborockDockTypeCode.f1_dock,
744
+ RoborockDockTypeCode.f2r_dock,
745
+ RoborockDockTypeCode.f1s_dock,
746
+ RoborockDockTypeCode.f1c_dock,
722
747
  }
723
748
 
724
749
  _WATER_UPDOWN_DRAIN_DOCK_TYPES = {
@@ -739,6 +764,19 @@ _WATER_UPDOWN_DRAIN_AM_DOCK_TYPES = {
739
764
  RoborockDockTypeCode.shell_e_dock,
740
765
  RoborockDockTypeCode.shell_2e_dock,
741
766
  RoborockDockTypeCode.shell_3c_dock,
767
+ RoborockDockTypeCode.k1c_dock,
768
+ RoborockDockTypeCode.hera_dock,
769
+ RoborockDockTypeCode.k1s_pro_dock,
770
+ RoborockDockTypeCode.type_27_dock,
771
+ RoborockDockTypeCode.shell_4rc_dock,
772
+ RoborockDockTypeCode.shell_4r_dock,
773
+ RoborockDockTypeCode.shell_4p_dock,
774
+ RoborockDockTypeCode.shell_4s_dock,
775
+ RoborockDockTypeCode.o7_dock,
776
+ RoborockDockTypeCode.shell_4d_dock,
777
+ RoborockDockTypeCode.f1r_dock,
778
+ RoborockDockTypeCode.f1s_dock,
779
+ RoborockDockTypeCode.o7h_dock,
742
780
  }
743
781
 
744
782
  _HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES = {
@@ -749,6 +787,56 @@ _SPECIAL_WASH_TEMP_DOCK_TYPES = {
749
787
  RoborockDockTypeCode.shell_2e_lite_dock,
750
788
  }
751
789
 
790
+ _HIGH_TMP_WATER_DOCK_TYPES = {
791
+ RoborockDockTypeCode.k1r_dock,
792
+ RoborockDockTypeCode.shell_4rc_dock,
793
+ RoborockDockTypeCode.shell_2e_heat_dock,
794
+ RoborockDockTypeCode.f1_dock,
795
+ RoborockDockTypeCode.f1r_dock,
796
+ RoborockDockTypeCode.f2r_dock,
797
+ RoborockDockTypeCode.f1c_dock,
798
+ }
799
+
800
+ _DOUBLE_SERIAL_COMMUNICATION_DOCK_TYPES = {
801
+ RoborockDockTypeCode.k1s_pro_dock,
802
+ RoborockDockTypeCode.type_27_dock,
803
+ RoborockDockTypeCode.k1r_dock,
804
+ RoborockDockTypeCode.shell_4r_dock,
805
+ RoborockDockTypeCode.shell_4p_dock,
806
+ RoborockDockTypeCode.shell_4s_dock,
807
+ RoborockDockTypeCode.o7_dock,
808
+ RoborockDockTypeCode.k1d_dock,
809
+ RoborockDockTypeCode.shell_3p_dock,
810
+ RoborockDockTypeCode.f1_dock,
811
+ RoborockDockTypeCode.f1r_dock,
812
+ RoborockDockTypeCode.f2r_dock,
813
+ RoborockDockTypeCode.f1s_dock,
814
+ RoborockDockTypeCode.o7h_dock,
815
+ RoborockDockTypeCode.f1c_dock,
816
+ }
817
+
818
+ _DOUBLE_SERIAL_COMMUNICATION_AM_DOCK_TYPES = {
819
+ RoborockDockTypeCode.shell_4rc_dock,
820
+ RoborockDockTypeCode.shell_4d_dock,
821
+ }
822
+
823
+ _NO_WATER_FLOW_CAROUSEL_DOCK_TYPES = {
824
+ RoborockDockTypeCode.f1r_dock,
825
+ RoborockDockTypeCode.f2r_dock,
826
+ RoborockDockTypeCode.f1s_dock,
827
+ }
828
+
829
+ _SIMPLE_WASH_MODE_DOCK_TYPES = {
830
+ RoborockDockTypeCode.f1_dock,
831
+ RoborockDockTypeCode.f1r_dock,
832
+ RoborockDockTypeCode.f2r_dock,
833
+ RoborockDockTypeCode.f1c_dock,
834
+ }
835
+
836
+ _STEAM_WASH_DOCK_TYPES = {
837
+ RoborockDockTypeCode.o7h_dock,
838
+ }
839
+
752
840
 
753
841
  @dataclass(frozen=True)
754
842
  class RoborockDockFeatures:
@@ -849,6 +937,28 @@ class RoborockDockFeatures:
849
937
  def is_special_support_wash_temp(self) -> bool:
850
938
  return self.dock_type in _SPECIAL_WASH_TEMP_DOCK_TYPES
851
939
 
940
+ @property
941
+ def is_high_tmp_water_supported(self) -> bool:
942
+ return self.dock_type in _HIGH_TMP_WATER_DOCK_TYPES
943
+
944
+ @property
945
+ def is_double_serial_communication_supported(self) -> bool:
946
+ return self.dock_type in _DOUBLE_SERIAL_COMMUNICATION_DOCK_TYPES or self._matches_am_variant(
947
+ _DOUBLE_SERIAL_COMMUNICATION_AM_DOCK_TYPES
948
+ )
949
+
950
+ @property
951
+ def is_no_water_flow_carousel(self) -> bool:
952
+ return self.dock_type in _NO_WATER_FLOW_CAROUSEL_DOCK_TYPES
953
+
954
+ @property
955
+ def is_simple_wash_mode_supported(self) -> bool:
956
+ return self.dock_type in _SIMPLE_WASH_MODE_DOCK_TYPES
957
+
958
+ @property
959
+ def is_steam_wash_supported(self) -> bool:
960
+ return self.dock_type in _STEAM_WASH_DOCK_TYPES
961
+
852
962
 
853
963
  WASH_N_FILL_DOCK_TYPES = [
854
964
  dock_type for dock_type in RoborockDockTypeCode if RoborockDockFeatures.from_dock_type(dock_type).is_washable
@@ -246,6 +246,7 @@ async def create_device_manager(
246
246
  channel.rpc_channel,
247
247
  channel.mqtt_rpc_channel,
248
248
  channel.map_rpc_channel,
249
+ channel.blob_rpc_channel,
249
250
  channel.add_dps_listener,
250
251
  web_api,
251
252
  device_cache=device_cache,
@@ -29,6 +29,7 @@ from roborock.protocols.v1_protocol import (
29
29
  ResponseMessage,
30
30
  SecurityData,
31
31
  V1RpcChannel,
32
+ create_blob_response_decoder,
32
33
  create_map_response_decoder,
33
34
  create_security_data,
34
35
  decode_data_protocol_message,
@@ -161,6 +162,50 @@ class RpcChannel(V1RpcChannel):
161
162
  return result
162
163
 
163
164
 
165
+ class BlobRpcChannel(V1RpcChannel):
166
+ """RPC channel that adds the security envelope required for blob requests."""
167
+
168
+ def __init__(
169
+ self,
170
+ rpc_channel: V1RpcChannel,
171
+ raw_blob_rpc_channel: V1RpcChannel,
172
+ security_data: SecurityData,
173
+ ) -> None:
174
+ """Initialize the blob RPC channel."""
175
+ self._rpc_channel = rpc_channel
176
+ self._raw_blob_rpc_channel = raw_blob_rpc_channel
177
+ self._security_data = security_data
178
+
179
+ async def send_command(
180
+ self,
181
+ method: CommandType,
182
+ *,
183
+ response_type: type[_T] | None = None,
184
+ params: ParamsType = None,
185
+ ) -> _T | Any:
186
+ """Send a blob command after adding the device security parameters."""
187
+ public_key = await self._rpc_channel.send_command(RoborockCommand.GET_RANDOM_PKEY)
188
+ if not isinstance(public_key, dict) or not isinstance(public_key.get("pub_key"), dict):
189
+ raise RoborockException("get_random_pkey response did not contain a public key")
190
+ security = self._security_data.to_dict()["security"]
191
+ blob_params = {
192
+ "security": {
193
+ "pub_key": public_key["pub_key"],
194
+ "cipher_suite": 0,
195
+ },
196
+ "endpoint": security["endpoint"],
197
+ "nonce": security["nonce"],
198
+ "data_filter": params,
199
+ }
200
+ if response_type is not None:
201
+ return await self._raw_blob_rpc_channel.send_command(
202
+ method,
203
+ response_type=response_type,
204
+ params=blob_params,
205
+ )
206
+ return await self._raw_blob_rpc_channel.send_command(method, params=blob_params)
207
+
208
+
164
209
  class V1Channel(Channel):
165
210
  """Unified V1 protocol channel with automatic MQTT/local connection handling.
166
211
 
@@ -245,6 +290,13 @@ class V1Channel(Channel):
245
290
  decoder = create_map_response_decoder(security_data=self._security_data)
246
291
  return RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger)
247
292
 
293
+ @property
294
+ def blob_rpc_channel(self) -> V1RpcChannel:
295
+ """Return the blob RPC channel used for fetching binary content."""
296
+ decoder = create_blob_response_decoder()
297
+ raw_blob_rpc_channel = RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger)
298
+ return BlobRpcChannel(self.rpc_channel, raw_blob_rpc_channel, self._security_data)
299
+
248
300
  def _create_local_rpc_strategy(self) -> RpcStrategy | None:
249
301
  """Create the RPC strategy for local transport."""
250
302
  if self._local_channel is None or not self.is_local_connected:
@@ -83,6 +83,7 @@ from . import (
83
83
  map_content,
84
84
  maps,
85
85
  network_info,
86
+ obstacle_photos,
86
87
  rooms,
87
88
  routines,
88
89
  smart_wash_params,
@@ -105,6 +106,7 @@ from .led_status import LedStatusTrait
105
106
  from .map_content import MapContentTrait
106
107
  from .maps import MapsTrait
107
108
  from .network_info import NetworkInfoTrait
109
+ from .obstacle_photos import ObstaclePhotoTrait
108
110
  from .rooms import RoomsTrait
109
111
  from .routines import RoutinesTrait
110
112
  from .smart_wash_params import SmartWashParamsTrait
@@ -131,6 +133,7 @@ __all__ = [
131
133
  "map_content",
132
134
  "maps",
133
135
  "network_info",
136
+ "obstacle_photos",
134
137
  "rooms",
135
138
  "routines",
136
139
  "smart_wash_params",
@@ -171,6 +174,7 @@ class PropertiesApi(Trait):
171
174
  dust_collection_mode: DustCollectionModeTrait | None = None
172
175
  wash_towel_mode: WashTowelModeTrait | None = None
173
176
  smart_wash_params: SmartWashParamsTrait | None = None
177
+ obstacle_photos: ObstaclePhotoTrait | None = None
174
178
 
175
179
  def __init__(
176
180
  self,
@@ -180,6 +184,7 @@ class PropertiesApi(Trait):
180
184
  rpc_channel: V1RpcChannel,
181
185
  mqtt_rpc_channel: V1RpcChannel,
182
186
  map_rpc_channel: V1RpcChannel,
187
+ blob_rpc_channel: V1RpcChannel,
183
188
  add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]],
184
189
  web_api: UserWebApiClient,
185
190
  device_cache: DeviceCache,
@@ -191,6 +196,7 @@ class PropertiesApi(Trait):
191
196
  self._rpc_channel = rpc_channel
192
197
  self._mqtt_rpc_channel = mqtt_rpc_channel
193
198
  self._map_rpc_channel = map_rpc_channel
199
+ self._blob_rpc_channel = blob_rpc_channel
194
200
  self._web_api = web_api
195
201
  self._device_cache = device_cache
196
202
  self._region = region
@@ -229,6 +235,8 @@ class PropertiesApi(Trait):
229
235
  # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
230
236
  if hasattr(trait, "mqtt_rpc_channel"):
231
237
  return self._mqtt_rpc_channel
238
+ elif hasattr(trait, "blob_rpc_channel"):
239
+ return self._blob_rpc_channel
232
240
  elif hasattr(trait, "map_rpc_channel"):
233
241
  return self._map_rpc_channel
234
242
  else:
@@ -272,6 +280,11 @@ class PropertiesApi(Trait):
272
280
  wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode) # type: ignore[assignment]
273
281
  self.wash_towel_mode = wash_towel_mode
274
282
 
283
+ if self.obstacle_photos is None and self._is_supported(ObstaclePhotoTrait, "obstacle_photos", dock_features):
284
+ obstacle_photos = ObstaclePhotoTrait(self._rpc_channel)
285
+ obstacle_photos._rpc_channel = self._get_rpc_channel(obstacle_photos)
286
+ self.obstacle_photos = obstacle_photos
287
+
275
288
  # Dynamically create any traits that need to be populated
276
289
  for item in fields(self):
277
290
  if (trait := getattr(self, item.name, None)) is not None:
@@ -283,6 +296,8 @@ class PropertiesApi(Trait):
283
296
 
284
297
  # Union args may not be in declared order
285
298
  item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
299
+ if item_type is ObstaclePhotoTrait:
300
+ continue
286
301
  if not self._is_supported(item_type, item.name, dock_features):
287
302
  _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
288
303
  continue
@@ -369,6 +384,7 @@ def create(
369
384
  rpc_channel: V1RpcChannel,
370
385
  mqtt_rpc_channel: V1RpcChannel,
371
386
  map_rpc_channel: V1RpcChannel,
387
+ blob_rpc_channel: V1RpcChannel,
372
388
  add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]],
373
389
  web_api: UserWebApiClient,
374
390
  device_cache: DeviceCache,
@@ -383,6 +399,7 @@ def create(
383
399
  rpc_channel,
384
400
  mqtt_rpc_channel,
385
401
  map_rpc_channel,
402
+ blob_rpc_channel,
386
403
  add_dps_listener,
387
404
  web_api,
388
405
  device_cache,
@@ -16,7 +16,7 @@ from roborock.roborock_typing import RoborockCommand
16
16
  _LOGGER = logging.getLogger(__name__)
17
17
 
18
18
 
19
- V1ResponseData = dict | list | int | str
19
+ V1ResponseData = dict | list | int | str | bytes
20
20
 
21
21
 
22
22
  class V1TraitDataConverter(ABC):
@@ -0,0 +1,104 @@
1
+ """Trait for fetching obstacle photos from V1 vacuums."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from roborock.data import RoborockBase
6
+ from roborock.devices.traits.v1 import common
7
+ from roborock.exceptions import RoborockException
8
+ from roborock.protocols.v1_protocol import V1RpcChannel
9
+ from roborock.roborock_typing import RoborockCommand
10
+
11
+ _PHOTO_TYPE_SMALL = 1
12
+ _PHOTO_DATA_BLOCK_TYPE = 3
13
+ _MAP_OBJECT_PHOTO_ENABLED_BIT = 10
14
+ _TYPE_SIZE = 2
15
+ _HEADER_SIZE_SIZE = 2
16
+ _PAYLOAD_SIZE_SIZE = 4
17
+ _MIN_BLOCK_HEADER_SIZE = _TYPE_SIZE + _HEADER_SIZE_SIZE + _PAYLOAD_SIZE_SIZE
18
+ _IMAGE_HEADERS = (b"\x89PNG\r\n\x1a\n", b"\xff\xd8\xff")
19
+
20
+
21
+ @dataclass
22
+ class ObstaclePhoto(RoborockBase):
23
+ """Obstacle photo content."""
24
+
25
+ photo_id: str
26
+ image_content: bytes
27
+
28
+
29
+ class ObstaclePhotoConverter(common.V1TraitDataConverter):
30
+ """Convert a decrypted get_photo payload to an obstacle photo."""
31
+
32
+ def convert(self, response: common.V1ResponseData) -> ObstaclePhoto:
33
+ """Parse the response from the device into an obstacle photo."""
34
+ if not isinstance(response, bytes):
35
+ raise ValueError(f"Unexpected ObstaclePhotoTrait response format: {type(response)}")
36
+ return ObstaclePhoto(photo_id="", image_content=parse_photo_data(response))
37
+
38
+
39
+ def parse_photo_data(response: bytes) -> bytes:
40
+ """Parse the get_photo response payload and return image bytes.
41
+
42
+ Roborock's app parses get_photo as a sequence of little-endian typed blocks.
43
+ Block type 3 contains the image bytes.
44
+ """
45
+ offset = 0
46
+ while offset + _MIN_BLOCK_HEADER_SIZE <= len(response):
47
+ block_type = int.from_bytes(response[offset : offset + _TYPE_SIZE], "little")
48
+ header_size = int.from_bytes(
49
+ response[offset + _TYPE_SIZE : offset + _TYPE_SIZE + _HEADER_SIZE_SIZE],
50
+ "little",
51
+ )
52
+ payload_size = int.from_bytes(
53
+ response[offset + _TYPE_SIZE + _HEADER_SIZE_SIZE : offset + _MIN_BLOCK_HEADER_SIZE],
54
+ "little",
55
+ )
56
+ next_offset = offset + header_size + payload_size
57
+ if header_size < _MIN_BLOCK_HEADER_SIZE or next_offset > len(response):
58
+ raise RoborockException("Invalid obstacle photo payload")
59
+
60
+ if block_type == _PHOTO_DATA_BLOCK_TYPE:
61
+ image_content = response[offset + header_size : next_offset]
62
+ if not image_content.startswith(_IMAGE_HEADERS):
63
+ raise RoborockException("Obstacle photo payload is not a supported image")
64
+ return image_content
65
+
66
+ offset = next_offset
67
+
68
+ raise RoborockException("Obstacle photo payload does not contain photo data")
69
+
70
+
71
+ class ObstaclePhotoTrait(RoborockBase, common.V1TraitMixin):
72
+ """Trait for fetching obstacle photos."""
73
+
74
+ command = RoborockCommand.GET_PHOTO
75
+ converter = ObstaclePhotoConverter()
76
+ blob_rpc_channel = True
77
+ requires_feature = "is_ai_recognition_obstacle_supported"
78
+
79
+ def __init__(self, standard_rpc_channel: V1RpcChannel) -> None:
80
+ """Initialize the obstacle photo trait."""
81
+ super().__init__()
82
+ self._standard_rpc_channel = standard_rpc_channel
83
+
84
+ async def get_enabled(self) -> bool:
85
+ """Return whether map object photo capture is enabled on the vacuum."""
86
+ response = await self._standard_rpc_channel.send_command(RoborockCommand.GET_CAMERA_STATUS)
87
+ if not isinstance(response, list) or not response or not isinstance(response[0], int):
88
+ raise RoborockException("get_camera_status response did not contain camera status")
89
+ return bool((response[0] >> _MAP_OBJECT_PHOTO_ENABLED_BIT) & 1)
90
+
91
+ async def get_photo(self, photo_id: str) -> ObstaclePhoto:
92
+ """Fetch the small obstacle photo for a map photo ID.
93
+
94
+ Photo IDs are available as ``Obstacle.details.photo_name`` in
95
+ ``ParsedMapData.map_data.obstacles_with_photo`` and
96
+ ``ParsedMapData.map_data.ignored_obstacles_with_photo``.
97
+ """
98
+ response = await self.rpc_channel.send_command(
99
+ self.command,
100
+ params={"img_id": photo_id, "type": _PHOTO_TYPE_SMALL},
101
+ )
102
+ photo = self.converter.convert(response)
103
+ photo.photo_id = photo_id
104
+ return photo
@@ -3,10 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import base64
6
+ import gzip
6
7
  import json
7
8
  import logging
8
9
  import secrets
9
10
  import struct
11
+ import zlib
10
12
  from collections.abc import Callable
11
13
  from dataclasses import dataclass, field
12
14
  from enum import StrEnum
@@ -24,6 +26,7 @@ _LOGGER = logging.getLogger(__name__)
24
26
  __all__ = [
25
27
  "SecurityData",
26
28
  "create_security_data",
29
+ "create_blob_response_decoder",
27
30
  "decode_data_protocol_message",
28
31
  "decode_rpc_response",
29
32
  "V1RpcChannel",
@@ -261,6 +264,48 @@ class MapResponse:
261
264
  """The map data, decrypted and decompressed."""
262
265
 
263
266
 
267
+ @dataclass
268
+ class BlobResponse:
269
+ """Data structure for V1 blob responses."""
270
+
271
+ request_id: int
272
+ """The request ID of the blob response."""
273
+
274
+ data: bytes
275
+ """The blob data, decompressed."""
276
+
277
+
278
+ def create_blob_response_decoder() -> Callable[[RoborockMessage], BlobResponse | None]:
279
+ """Create a decoder for V1 blob response messages.
280
+
281
+ Obstacle photos are acknowledged through the normal RPC response with
282
+ ``["ok"]`` and delivered later as a protocol-301 blob frame. The frame starts
283
+ with ``ROBOROCK``, stores the RPC request id at bytes 8-11, the header size
284
+ at bytes 16-17, and the gzip payload length at bytes 20-23.
285
+ """
286
+
287
+ def _decode_blob_response(message: RoborockMessage) -> BlobResponse | None:
288
+ """Decode a V1 blob response message."""
289
+ if message.protocol != RoborockMessageProtocol.MAP_RESPONSE:
290
+ return None
291
+ payload = message.payload
292
+ if not payload or not payload.startswith(b"ROBOROCK") or len(payload) < 24:
293
+ return None
294
+ request_id = int.from_bytes(payload[8:12], "little")
295
+ header_size = int.from_bytes(payload[16:18], "little")
296
+ payload_size = int.from_bytes(payload[20:24], "little")
297
+ end_offset = header_size + payload_size
298
+ if header_size < 24 or end_offset > len(payload):
299
+ raise RoborockException("Invalid V1 blob response format")
300
+ try:
301
+ data = Utils.decompress(payload[header_size:end_offset])
302
+ except (gzip.BadGzipFile, EOFError, zlib.error) as err:
303
+ raise RoborockException("Failed to decode blob message payload") from err
304
+ return BlobResponse(request_id=request_id, data=data)
305
+
306
+ return _decode_blob_response
307
+
308
+
264
309
  def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse | None]:
265
310
  """Create a decoder for V1 map response messages."""
266
311
 
@@ -117,6 +117,7 @@ class RoborockCommand(str, Enum):
117
117
  GET_NETWORK_INFO = "get_network_info"
118
118
  GET_OFFLINE_MAP_STATUS = "get_offline_map_status"
119
119
  GET_PERSIST = "get_persist_map"
120
+ GET_PHOTO = "get_photo"
120
121
  GET_PROP = "get_prop"
121
122
  GET_RANDOM_PKEY = "get_random_pkey"
122
123
  GET_RECOVER_MAP = "get_recover_map"