python-roborock 5.23.1__tar.gz → 5.25.0__tar.gz

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