python-roborock 2.53.1__tar.gz → 2.58.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 (76) hide show
  1. python_roborock-2.58.0/.gitignore +21 -0
  2. {python_roborock-2.53.1 → python_roborock-2.58.0}/PKG-INFO +19 -25
  3. python_roborock-2.58.0/pyproject.toml +100 -0
  4. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/api.py +1 -1
  5. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/clean_modes.py +27 -0
  6. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/cli.py +192 -20
  7. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/code_mappings.py +34 -0
  8. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/containers.py +93 -7
  9. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/device_features.py +84 -1
  10. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/a01_channel.py +2 -4
  11. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/cache.py +8 -1
  12. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/device.py +21 -1
  13. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/device_manager.py +8 -2
  14. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/traits_mixin.py +1 -1
  15. python_roborock-2.58.0/roborock/devices/traits/v1/__init__.py +163 -0
  16. python_roborock-2.58.0/roborock/devices/traits/v1/child_lock.py +20 -0
  17. python_roborock-2.58.0/roborock/devices/traits/v1/command.py +21 -0
  18. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/common.py +5 -1
  19. python_roborock-2.58.0/roborock/devices/traits/v1/device_features.py +47 -0
  20. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/do_not_disturb.py +22 -0
  21. python_roborock-2.58.0/roborock/devices/traits/v1/flow_led_status.py +20 -0
  22. python_roborock-2.58.0/roborock/devices/traits/v1/home.py +174 -0
  23. python_roborock-2.58.0/roborock/devices/traits/v1/led_status.py +34 -0
  24. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/map_content.py +1 -0
  25. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/maps.py +9 -0
  26. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/rooms.py +1 -13
  27. python_roborock-2.58.0/roborock/devices/traits/v1/valley_electricity_timer.py +40 -0
  28. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/v1_channel.py +3 -3
  29. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/v1_rpc_channel.py +0 -1
  30. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/exceptions.py +8 -0
  31. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/protocols/v1_protocol.py +3 -3
  32. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/roborock_future.py +2 -3
  33. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/version_1_apis/roborock_local_client_v1.py +1 -3
  34. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/web_api.py +12 -3
  35. python_roborock-2.53.1/pyproject.toml +0 -82
  36. python_roborock-2.53.1/roborock/devices/traits/v1/__init__.py +0 -96
  37. {python_roborock-2.53.1 → python_roborock-2.58.0}/LICENSE +0 -0
  38. {python_roborock-2.53.1 → python_roborock-2.58.0}/README.md +0 -0
  39. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/__init__.py +0 -0
  40. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/b01_containers.py +0 -0
  41. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/broadcast_protocol.py +0 -0
  42. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/callbacks.py +0 -0
  43. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/cloud_api.py +0 -0
  44. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/command_cache.py +0 -0
  45. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/const.py +0 -0
  46. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/README.md +0 -0
  47. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/__init__.py +0 -0
  48. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/b01_channel.py +0 -0
  49. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/channel.py +0 -0
  50. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/local_channel.py +0 -0
  51. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/mqtt_channel.py +0 -0
  52. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/__init__.py +0 -0
  53. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/a01/__init__.py +0 -0
  54. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/b01/__init__.py +0 -0
  55. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  56. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  57. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/status.py +0 -0
  58. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/devices/traits/v1/volume.py +0 -0
  59. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/map/__init__.py +0 -0
  60. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/map/map_parser.py +0 -0
  61. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/mqtt/__init__.py +0 -0
  62. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/mqtt/roborock_session.py +0 -0
  63. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/mqtt/session.py +0 -0
  64. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/protocol.py +0 -0
  65. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/protocols/a01_protocol.py +0 -0
  66. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/protocols/b01_protocol.py +0 -0
  67. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/py.typed +0 -0
  68. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/roborock_message.py +0 -0
  69. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/roborock_typing.py +0 -0
  70. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/util.py +0 -0
  71. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/version_1_apis/__init__.py +0 -0
  72. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  73. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  74. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/version_a01_apis/__init__.py +0 -0
  75. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  76. {python_roborock-2.53.1 → python_roborock-2.58.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
@@ -0,0 +1,21 @@
1
+ dist
2
+ venv
3
+ .venv
4
+ .idea
5
+ roborock/__pycache__
6
+ *.pyc
7
+ .coverage
8
+
9
+ # Sphinx documentation
10
+ docs/_build/
11
+
12
+ # mkdocs documentation
13
+ /site
14
+ /docs/build/
15
+ .DS_Store
16
+
17
+ # gemini-cli settings
18
+ .gemini/
19
+
20
+ # GitHub App credentials
21
+ gha-creds-*.json
@@ -1,35 +1,30 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 2.53.1
3
+ Version: 2.58.0
4
4
  Summary: A package to control Roborock vacuums.
5
- Home-page: https://github.com/humbertogontijo/python-roborock
6
- License: GPL-3.0-only
7
- Keywords: roborock,vacuum,homeassistant
8
- Author: humbertogontijo
9
- Author-email: humbertogontijo@users.noreply.github.com
10
- Requires-Python: >=3.11,<4.0
5
+ Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
6
+ Project-URL: Documentation, https://python-roborock.readthedocs.io/
7
+ Author: Lash-L, allenporter
8
+ Author-email: humbertogontijo <humbertogontijo@users.noreply.github.com>
9
+ License-Expression: GPL-3.0-only
10
+ License-File: LICENSE
11
+ Keywords: homeassistant,roborock,vacuum
11
12
  Classifier: Development Status :: 5 - Production/Stable
12
13
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
14
14
  Classifier: Natural Language :: English
15
15
  Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
16
  Classifier: Topic :: Software Development :: Libraries
20
- Requires-Dist: aiohttp (>=3.8.2,<4.0.0)
21
- Requires-Dist: aiomqtt (>=2.3.2,<3.0.0)
22
- Requires-Dist: async-timeout
23
- Requires-Dist: click (>=8)
24
- Requires-Dist: click-shell (>=2.1,<3.0)
25
- Requires-Dist: construct (>=2.10.57,<3.0.0)
26
- Requires-Dist: paho-mqtt (>=1.6.1,<3.0.0)
27
- Requires-Dist: pycryptodome (>=3.18,<4.0)
28
- Requires-Dist: pycryptodomex (>=3.18,<4.0) ; sys_platform == "darwin"
29
- Requires-Dist: pyrate-limiter (>=3.7.0,<4.0.0)
17
+ Requires-Python: <4,>=3.11
18
+ Requires-Dist: aiohttp<4,>=3.8.2
19
+ Requires-Dist: aiomqtt<3,>=2.3.2
20
+ Requires-Dist: click-shell~=2.1
21
+ Requires-Dist: click>=8
22
+ Requires-Dist: construct<3,>=2.10.57
23
+ Requires-Dist: paho-mqtt<3.0.0,>=1.6.1
24
+ Requires-Dist: pycryptodomex~=3.18; sys_platform == 'darwin'
25
+ Requires-Dist: pycryptodome~=3.18
26
+ Requires-Dist: pyrate-limiter<4,>=3.7.0
30
27
  Requires-Dist: vacuum-map-parser-roborock
31
- Project-URL: Documentation, https://python-roborock.readthedocs.io/
32
- Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
33
28
  Description-Content-Type: text/markdown
34
29
 
35
30
  # Roborock
@@ -108,4 +103,3 @@ Please note this may not immediately contain the latest devices.
108
103
  ## Credits
109
104
 
110
105
  Thanks @rovo89 for https://gist.github.com/rovo89/dff47ed19fca0dfdda77503e66c2b7c7 And thanks @PiotrMachowski for https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor
111
-
@@ -0,0 +1,100 @@
1
+ [project]
2
+ name = "python-roborock"
3
+ version = "2.58.0"
4
+ description = "A package to control Roborock vacuums."
5
+ authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
6
+ requires-python = ">=3.11, <4"
7
+ readme = "README.md"
8
+ license = "GPL-3.0-only"
9
+ keywords = [
10
+ "roborock",
11
+ "vacuum",
12
+ "homeassistant",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "Natural Language :: English",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = [
22
+ "click>=8",
23
+ "aiohttp>=3.8.2,<4",
24
+ "pycryptodome~=3.18",
25
+ "pycryptodomex~=3.18 ; sys_platform == 'darwin'",
26
+ "paho-mqtt>=1.6.1,<3.0.0",
27
+ "construct>=2.10.57,<3",
28
+ "vacuum-map-parser-roborock",
29
+ "pyrate-limiter>=3.7.0,<4",
30
+ "aiomqtt>=2.3.2,<3",
31
+ "click-shell~=2.1",
32
+ ]
33
+
34
+ [project.urls]
35
+ Repository = "https://github.com/humbertogontijo/python-roborock"
36
+ Documentation = "https://python-roborock.readthedocs.io/"
37
+
38
+ [project.scripts]
39
+ roborock = "roborock.cli:main"
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest-asyncio>=1.1.0",
44
+ "pytest",
45
+ "pre-commit>=3.5,<5.0",
46
+ "mypy",
47
+ "ruff==0.14.0",
48
+ "codespell",
49
+ "pyshark>=0.6,<0.7",
50
+ "aioresponses>=0.7.7,<0.8",
51
+ "freezegun>=1.5.1,<2",
52
+ "pytest-timeout>=2.3.1,<3",
53
+ "syrupy>=4.9.1,<5",
54
+ "pdoc>=15.0.4,<16",
55
+ ]
56
+
57
+ [tool.hatch.build.targets.sdist]
58
+ include = ["roborock"]
59
+
60
+ [tool.hatch.build.targets.wheel]
61
+ include = ["roborock"]
62
+
63
+ [build-system]
64
+ requires = ["hatchling"]
65
+ build-backend = "hatchling.build"
66
+
67
+ [tool.semantic_release]
68
+ branch = "main"
69
+ version_toml = ["pyproject.toml:tool.poetry.version"]
70
+ build_command = "pip install poetry && poetry build"
71
+
72
+ [tool.semantic_release.commit_parser_options]
73
+ allowed_tags = [
74
+ "chore",
75
+ "docs",
76
+ "feat",
77
+ "fix",
78
+ "refactor"
79
+ ]
80
+ major_tags= ["refactor"]
81
+
82
+ [tool.ruff]
83
+ lint.ignore = ["F403", "E741"]
84
+ lint.select=["E", "F", "UP", "I"]
85
+ line-length = 120
86
+
87
+ [tool.ruff.lint.per-file-ignores]
88
+ "*/__init__.py" = ["F401"]
89
+
90
+ [tool.pytest.ini_options]
91
+ asyncio_mode = "auto"
92
+ asyncio_default_fixture_loop_scope = "function"
93
+ timeout = 30
94
+ log_format = "%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s"
95
+
96
+ [tool.uv]
97
+ dev-dependencies = [
98
+ "pyyaml>=6.0.3",
99
+ "pyshark>=0.6",
100
+ ]
@@ -83,7 +83,7 @@ class RoborockClient(ABC):
83
83
  if response == "unknown_method":
84
84
  raise UnknownMethodError("Unknown method")
85
85
  return response
86
- except (asyncio.TimeoutError, asyncio.CancelledError):
86
+ except (TimeoutError, asyncio.CancelledError):
87
87
  raise RoborockTimeout(f"id={request_id} Timeout after {self.queue_timeout} seconds") from None
88
88
  finally:
89
89
  self._waiting_queue.pop(request_id, None)
@@ -45,6 +45,12 @@ class WaterModes(RoborockModeEnum):
45
45
  CUSTOM = ("custom_water_flow", 207)
46
46
  EXTREME = ("extreme", 208)
47
47
  SMART_MODE = ("smart_mode", 209)
48
+ PURE_WATER_FLOW_START = ("slight", 221)
49
+ PURE_WATER_FLOW_SMALL = ("low", 225)
50
+ PURE_WATER_FLOW_MIDDLE = ("medium", 235)
51
+ PURE_WATER_FLOW_LARGE = ("moderate", 245)
52
+ PURE_WATER_SUPER_BEGIN = ("high", 248)
53
+ PURE_WATER_FLOW_END = ("extreme", 250)
48
54
 
49
55
 
50
56
  class WashTowelModes(RoborockModeEnum):
@@ -112,6 +118,18 @@ def get_clean_routes(features: DeviceFeatures, region: str) -> list[CleanRoutes]
112
118
 
113
119
  def get_water_modes(features: DeviceFeatures) -> list[WaterModes]:
114
120
  """Get the valid water modes for the device - also known as 'water flow' or 'water level'"""
121
+ # If the device supports water slide mode, it uses a completely different set of modes. Technically, it can even
122
+ # support values in between. But for now we will just support the main values.
123
+ if features.is_water_slide_mode_supported:
124
+ return [
125
+ WaterModes.PURE_WATER_FLOW_START,
126
+ WaterModes.PURE_WATER_FLOW_SMALL,
127
+ WaterModes.PURE_WATER_FLOW_MIDDLE,
128
+ WaterModes.PURE_WATER_FLOW_LARGE,
129
+ WaterModes.PURE_WATER_SUPER_BEGIN,
130
+ WaterModes.PURE_WATER_FLOW_END,
131
+ ]
132
+
115
133
  supported_modes = [WaterModes.OFF]
116
134
  if features.is_mop_shake_module_supported:
117
135
  # For mops that have the vibrating mop pad, they do mild standard intense
@@ -131,6 +149,15 @@ def get_water_modes(features: DeviceFeatures) -> list[WaterModes]:
131
149
  return supported_modes
132
150
 
133
151
 
152
+ def is_mode_customized(clean_mode: VacuumModes, water_mode: WaterModes, mop_mode: CleanRoutes) -> bool:
153
+ """Check if any of the cleaning modes are set to a custom value."""
154
+ return (
155
+ clean_mode == VacuumModes.CUSTOMIZED
156
+ or water_mode == WaterModes.CUSTOMIZED
157
+ or mop_mode == CleanRoutes.CUSTOMIZED
158
+ )
159
+
160
+
134
161
  def is_smart_mode_set(water_mode: WaterModes, clean_mode: VacuumModes, mop_mode: CleanRoutes) -> bool:
135
162
  """Check if the smart mode is set for the given water mode and clean mode"""
136
163
  return (
@@ -41,8 +41,9 @@ from pyshark import FileCapture # type: ignore
41
41
  from pyshark.capture.live_capture import LiveCapture, UnknownInterfaceException # type: ignore
42
42
  from pyshark.packet.packet import Packet # type: ignore
43
43
 
44
- from roborock import SHORT_MODEL_TO_ENUM, DeviceFeatures, RoborockCommand, RoborockException
45
- from roborock.containers import DeviceData, HomeData, NetworkInfo, RoborockBase, UserData
44
+ from roborock import SHORT_MODEL_TO_ENUM, RoborockCommand
45
+ from roborock.containers import CombinedMapInfo, DeviceData, HomeData, NetworkInfo, RoborockBase, UserData
46
+ from roborock.device_features import DeviceFeatures
46
47
  from roborock.devices.cache import Cache, CacheData
47
48
  from roborock.devices.device import RoborockDevice
48
49
  from roborock.devices.device_manager import DeviceManager, create_device_manager, create_home_data_api
@@ -50,6 +51,7 @@ from roborock.devices.traits import Trait
50
51
  from roborock.devices.traits.v1 import V1TraitMixin
51
52
  from roborock.devices.traits.v1.consumeable import ConsumableAttribute
52
53
  from roborock.devices.traits.v1.map_content import MapContentTrait
54
+ from roborock.exceptions import RoborockException, RoborockUnsupportedFeature
53
55
  from roborock.protocol import MessageParser
54
56
  from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
55
57
  from roborock.web_api import RoborockApiClient
@@ -116,6 +118,7 @@ class ConnectionCache(RoborockBase):
116
118
  email: str
117
119
  home_data: HomeData | None = None
118
120
  network_info: dict[str, NetworkInfo] | None = None
121
+ home_cache: dict[int, CombinedMapInfo] | None = None
119
122
 
120
123
 
121
124
  class DeviceConnectionManager:
@@ -258,14 +261,21 @@ class RoborockContext(Cache):
258
261
 
259
262
  async def get(self) -> CacheData:
260
263
  """Get cached value."""
264
+ _LOGGER.debug("Getting cache data")
261
265
  connection_cache = self.cache_data()
262
- return CacheData(home_data=connection_cache.home_data, network_info=connection_cache.network_info or {})
266
+ return CacheData(
267
+ home_data=connection_cache.home_data,
268
+ network_info=connection_cache.network_info or {},
269
+ home_cache=connection_cache.home_cache,
270
+ )
263
271
 
264
272
  async def set(self, value: CacheData) -> None:
265
273
  """Set value in the cache."""
274
+ _LOGGER.debug("Setting cache data")
266
275
  connection_cache = self.cache_data()
267
276
  connection_cache.home_data = value.home_data
268
277
  connection_cache.network_info = value.network_info
278
+ connection_cache.home_cache = value.home_cache
269
279
  self.update(connection_cache)
270
280
 
271
281
 
@@ -392,14 +402,21 @@ async def _v1_trait(context: RoborockContext, device_id: str, display_func: Call
392
402
  device = await device_manager.get_device(device_id)
393
403
  if device.v1_properties is None:
394
404
  raise RoborockException(f"Device {device.name} does not support V1 protocol")
395
-
405
+ await device.v1_properties.discover_features()
396
406
  trait = display_func(device.v1_properties)
397
407
  await trait.refresh()
398
408
  return trait
399
409
 
400
410
 
401
411
  async def _display_v1_trait(context: RoborockContext, device_id: str, display_func: Callable[[], Trait]) -> None:
402
- trait = await _v1_trait(context, device_id, display_func)
412
+ try:
413
+ trait = await _v1_trait(context, device_id, display_func)
414
+ except RoborockUnsupportedFeature:
415
+ click.echo("Feature not supported by device")
416
+ return
417
+ except RoborockException as e:
418
+ click.echo(f"Error: {e}")
419
+ return
403
420
  click.echo(dump_json(trait.as_dict()))
404
421
 
405
422
 
@@ -523,6 +540,116 @@ async def reset_consumable(ctx, device_id: str, consumable: str):
523
540
  click.echo(f"Reset {consumable} for device {device_id}")
524
541
 
525
542
 
543
+ @session.command()
544
+ @click.option("--device_id", required=True)
545
+ @click.option("--enabled", type=bool, help="Enable (True) or disable (False) the child lock.")
546
+ @click.pass_context
547
+ @async_command
548
+ async def child_lock(ctx, device_id: str, enabled: bool | None):
549
+ """Get device child lock status."""
550
+ context: RoborockContext = ctx.obj
551
+ try:
552
+ trait = await _v1_trait(context, device_id, lambda v1: v1.child_lock)
553
+ except RoborockUnsupportedFeature:
554
+ click.echo("Feature not supported by device")
555
+ return
556
+ if enabled is not None:
557
+ if enabled:
558
+ await trait.enable()
559
+ else:
560
+ await trait.disable()
561
+ click.echo(f"Set child lock to {enabled} for device {device_id}")
562
+ await trait.refresh()
563
+
564
+ click.echo(dump_json(trait.as_dict()))
565
+
566
+
567
+ @session.command()
568
+ @click.option("--device_id", required=True)
569
+ @click.option("--enabled", type=bool, help="Enable (True) or disable (False) the DND status.")
570
+ @click.pass_context
571
+ @async_command
572
+ async def dnd(ctx, device_id: str, enabled: bool | None):
573
+ """Get Do Not Disturb Timer status."""
574
+ context: RoborockContext = ctx.obj
575
+ try:
576
+ trait = await _v1_trait(context, device_id, lambda v1: v1.dnd)
577
+ except RoborockUnsupportedFeature:
578
+ click.echo("Feature not supported by device")
579
+ return
580
+ if enabled is not None:
581
+ if enabled:
582
+ await trait.enable()
583
+ else:
584
+ await trait.disable()
585
+ click.echo(f"Set DND to {enabled} for device {device_id}")
586
+ await trait.refresh()
587
+
588
+ click.echo(dump_json(trait.as_dict()))
589
+
590
+
591
+ @session.command()
592
+ @click.option("--device_id", required=True)
593
+ @click.option("--enabled", required=False, type=bool, help="Enable (True) or disable (False) the Flow LED.")
594
+ @click.pass_context
595
+ @async_command
596
+ async def flow_led_status(ctx, device_id: str, enabled: bool | None):
597
+ """Get device Flow LED status."""
598
+ context: RoborockContext = ctx.obj
599
+ try:
600
+ trait = await _v1_trait(context, device_id, lambda v1: v1.flow_led_status)
601
+ except RoborockUnsupportedFeature:
602
+ click.echo("Feature not supported by device")
603
+ return
604
+ if enabled is not None:
605
+ if enabled:
606
+ await trait.enable()
607
+ else:
608
+ await trait.disable()
609
+ click.echo(f"Set Flow LED to {enabled} for device {device_id}")
610
+ await trait.refresh()
611
+
612
+ click.echo(dump_json(trait.as_dict()))
613
+
614
+
615
+ @session.command()
616
+ @click.option("--device_id", required=True)
617
+ @click.option("--enabled", required=False, type=bool, help="Enable (True) or disable (False) the LED.")
618
+ @click.pass_context
619
+ @async_command
620
+ async def led_status(ctx, device_id: str, enabled: bool | None):
621
+ """Get device LED status."""
622
+ context: RoborockContext = ctx.obj
623
+ try:
624
+ trait = await _v1_trait(context, device_id, lambda v1: v1.led_status)
625
+ except RoborockUnsupportedFeature:
626
+ click.echo("Feature not supported by device")
627
+ return
628
+ if enabled is not None:
629
+ if enabled:
630
+ await trait.enable()
631
+ else:
632
+ await trait.disable()
633
+ click.echo(f"Set LED Status to {enabled} for device {device_id}")
634
+ await trait.refresh()
635
+
636
+ click.echo(dump_json(trait.as_dict()))
637
+
638
+
639
+ @session.command()
640
+ @click.option("--device_id", required=True)
641
+ @click.option("--enabled", required=True, type=bool, help="Enable (True) or disable (False) the child lock.")
642
+ @click.pass_context
643
+ @async_command
644
+ async def set_child_lock(ctx, device_id: str, enabled: bool):
645
+ """Set the child lock status."""
646
+ context: RoborockContext = ctx.obj
647
+ trait = await _v1_trait(context, device_id, lambda v1: v1.child_lock)
648
+ await trait.set_child_lock(enabled)
649
+ status = "enabled" if enabled else "disabled"
650
+ click.echo(f"Child lock {status} for device {device_id}")
651
+
652
+
526
653
  @session.command()
527
654
  @click.option("--device_id", required=True)
528
655
  @click.pass_context
@@ -533,6 +660,52 @@ async def rooms(ctx, device_id: str):
533
660
  await _display_v1_trait(context, device_id, lambda v1: v1.rooms)
534
661
 
535
662
 
663
+ @session.command()
664
+ @click.option("--device_id", required=True)
665
+ @click.pass_context
666
+ @async_command
667
+ async def features(ctx, device_id: str):
668
+ """Get device room mapping info."""
669
+ context: RoborockContext = ctx.obj
670
+ await _display_v1_trait(context, device_id, lambda v1: v1.device_features)
671
+
672
+
673
+ @session.command()
674
+ @click.option("--device_id", required=True)
675
+ @click.option("--refresh", is_flag=True, default=False, help="Refresh status before discovery.")
676
+ @click.pass_context
677
+ @async_command
678
+ async def home(ctx, device_id: str, refresh: bool):
679
+ """Discover and cache home layout (maps and rooms)."""
680
+ context: RoborockContext = ctx.obj
681
+ device_manager = await context.get_device_manager()
682
+ device = await device_manager.get_device(device_id)
683
+ if device.v1_properties is None:
684
+ raise RoborockException(f"Device {device.name} does not support V1 protocol")
685
+
686
+ # Ensure we have the latest status before discovery
687
+ await device.v1_properties.status.refresh()
688
+
689
+ home_trait = device.v1_properties.home
690
+ await home_trait.discover_home()
691
+ if refresh:
692
+ await home_trait.refresh()
693
+
694
+ # Display the discovered home cache
695
+ if home_trait.home_cache:
696
+ cache_summary = {
697
+ map_flag: {
698
+ "name": map_data.name,
699
+ "room_count": len(map_data.rooms),
700
+ "rooms": [{"segment_id": room.segment_id, "name": room.name} for room in map_data.rooms],
701
+ }
702
+ for map_flag, map_data in home_trait.home_cache.items()
703
+ }
704
+ click.echo(dump_json(cache_summary))
705
+ else:
706
+ click.echo("No maps discovered")
707
+
708
+
536
709
  @click.command()
537
710
  @click.option("--device_id", required=True)
538
711
  @click.option("--cmd", required=True)
@@ -541,21 +714,14 @@ async def rooms(ctx, device_id: str):
541
714
  @async_command
542
715
  async def command(ctx, cmd, device_id, params):
543
716
  context: RoborockContext = ctx.obj
544
- cache_data = await context.get_devices()
545
-
546
- home_data = cache_data.home_data
547
- devices = home_data.get_all_devices()
548
- device = next(device for device in devices if device.duid == device_id)
549
- model = next(
550
- (product.model for product in home_data.products if device is not None and product.id == device.product_id),
551
- None,
552
- )
553
- if model is None:
554
- raise RoborockException(f"Could not find model for device {device.name}")
555
- device_info = DeviceData(device=device, model=model)
556
- mqtt_client = RoborockMqttClientV1(cache_data.user_data, device_info)
557
- await mqtt_client.send_command(cmd, json.loads(params) if params is not None else None)
558
- await mqtt_client.async_release()
717
+ device_manager = await context.get_device_manager()
718
+ device = await device_manager.get_device(device_id)
719
+ if device.v1_properties is None:
720
+ raise RoborockException(f"Device {device.name} does not support V1 protocol")
721
+ command_trait: Trait = device.v1_properties.command
722
+ result = await command_trait.send(cmd, json.loads(params) if params is not None else None)
723
+ if result:
724
+ click.echo(dump_json(result))
559
725
 
560
726
 
561
727
  @click.command()
@@ -780,6 +946,12 @@ cli.add_command(map_data)
780
946
  cli.add_command(consumables)
781
947
  cli.add_command(reset_consumable)
782
948
  cli.add_command(rooms)
949
+ cli.add_command(home)
950
+ cli.add_command(features)
951
+ cli.add_command(child_lock)
952
+ cli.add_command(dnd)
953
+ cli.add_command(flow_led_status)
954
+ cli.add_command(led_status)
783
955
 
784
956
 
785
957
  def main():
@@ -768,6 +768,40 @@ class RoborockStartType(RoborockEnum):
768
768
  smart_watch = 821
769
769
 
770
770
 
771
+ class RoborockDssCodes(RoborockEnum):
772
+ @classmethod
773
+ def _missing_(cls: type[RoborockEnum], key) -> RoborockEnum:
774
+ # If the calculated value is not provided, then it should be viewed as okay.
775
+ # As the math will sometimes result in you getting numbers that don't matter.
776
+ return cls.okay # type: ignore
777
+
778
+
779
+ class ClearWaterBoxStatus(RoborockDssCodes):
780
+ """Status of the clear water box."""
781
+
782
+ okay = 0
783
+ out_of_water = 1
784
+ out_of_water_2 = 38
785
+ refill_error = 48
786
+
787
+
788
+ class DirtyWaterBoxStatus(RoborockDssCodes):
789
+ """Status of the dirty water box."""
790
+
791
+ okay = 0
792
+ full_not_installed = 1
793
+ full_not_installed_2 = 39
794
+ drain_error = 49
795
+
796
+
797
+ class DustBagStatus(RoborockDssCodes):
798
+ """Status of the dust bag."""
799
+
800
+ okay = 0
801
+ not_installed = 1
802
+ full = 34
803
+
804
+
771
805
  class DyadSelfCleanMode(RoborockEnum):
772
806
  self_clean = 1
773
807
  self_clean_and_dry = 2