python-roborock 3.0.0__tar.gz → 3.2.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 (94) hide show
  1. {python_roborock-3.0.0 → python_roborock-3.2.0}/PKG-INFO +1 -1
  2. {python_roborock-3.0.0 → python_roborock-3.2.0}/pyproject.toml +16 -9
  3. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/cli.py +11 -0
  4. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/v1/v1_containers.py +11 -5
  5. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/device.py +40 -1
  6. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/device_manager.py +1 -0
  7. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/__init__.py +30 -7
  8. python_roborock-3.0.0/roborock/devices/traits/v1/clean_record.py → python_roborock-3.2.0/roborock/devices/traits/v1/clean_summary.py +34 -21
  9. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/device_features.py +5 -0
  10. python_roborock-3.2.0/roborock/devices/traits/v1/network_info.py +57 -0
  11. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/version_1_apis/roborock_client_v1.py +1 -1
  12. python_roborock-3.0.0/roborock/devices/traits/v1/clean_summary.py +0 -29
  13. {python_roborock-3.0.0 → python_roborock-3.2.0}/.gitignore +0 -0
  14. {python_roborock-3.0.0 → python_roborock-3.2.0}/LICENSE +0 -0
  15. {python_roborock-3.0.0 → python_roborock-3.2.0}/README.md +0 -0
  16. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/__init__.py +0 -0
  17. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/api.py +0 -0
  18. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/broadcast_protocol.py +0 -0
  19. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/callbacks.py +0 -0
  20. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/cloud_api.py +0 -0
  21. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/command_cache.py +0 -0
  22. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/const.py +0 -0
  23. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/__init__.py +0 -0
  24. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/b01_q10/__init__.py +0 -0
  25. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  26. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  27. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/b01_q7/__init__.py +0 -0
  28. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  29. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  30. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/code_mappings.py +0 -0
  31. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/containers.py +0 -0
  32. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/dyad/__init__.py +0 -0
  33. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  34. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/dyad/dyad_containers.py +0 -0
  35. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/v1/__init__.py +0 -0
  36. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  37. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  38. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/zeo/__init__.py +0 -0
  39. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  40. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/data/zeo/zeo_containers.py +0 -0
  41. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/device_features.py +0 -0
  42. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/README.md +0 -0
  43. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/__init__.py +0 -0
  44. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/a01_channel.py +0 -0
  45. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/b01_channel.py +0 -0
  46. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/cache.py +0 -0
  47. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/channel.py +0 -0
  48. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/local_channel.py +0 -0
  49. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/mqtt_channel.py +0 -0
  50. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/__init__.py +0 -0
  51. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/a01/__init__.py +0 -0
  52. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/b01/__init__.py +0 -0
  53. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/traits_mixin.py +0 -0
  54. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  55. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/command.py +0 -0
  56. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/common.py +0 -0
  57. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  58. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  59. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  60. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  61. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/home.py +0 -0
  62. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/led_status.py +0 -0
  63. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/map_content.py +0 -0
  64. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/maps.py +0 -0
  65. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/rooms.py +0 -0
  66. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  67. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/status.py +0 -0
  68. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  69. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/volume.py +0 -0
  70. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  71. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/v1_channel.py +0 -0
  72. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/devices/v1_rpc_channel.py +0 -0
  73. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/exceptions.py +0 -0
  74. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/map/__init__.py +0 -0
  75. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/map/map_parser.py +0 -0
  76. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/mqtt/__init__.py +0 -0
  77. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/mqtt/roborock_session.py +0 -0
  78. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/mqtt/session.py +0 -0
  79. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/protocol.py +0 -0
  80. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/protocols/a01_protocol.py +0 -0
  81. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/protocols/b01_protocol.py +0 -0
  82. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/protocols/v1_protocol.py +0 -0
  83. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/py.typed +0 -0
  84. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/roborock_future.py +0 -0
  85. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/roborock_message.py +0 -0
  86. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/roborock_typing.py +0 -0
  87. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/util.py +0 -0
  88. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/version_1_apis/__init__.py +0 -0
  89. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  90. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  91. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/version_a01_apis/__init__.py +0 -0
  92. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  93. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  94. {python_roborock-3.0.0 → python_roborock-3.2.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/humbertogontijo/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 = "3.0.0"
3
+ version = "3.2.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"
@@ -44,7 +44,7 @@ dev = [
44
44
  "pytest",
45
45
  "pre-commit>=3.5,<5.0",
46
46
  "mypy",
47
- "ruff==0.14.0",
47
+ "ruff==0.14.1",
48
48
  "codespell",
49
49
  "pyshark>=0.6,<0.7",
50
50
  "aioresponses>=0.7.7,<0.8",
@@ -52,6 +52,8 @@ dev = [
52
52
  "pytest-timeout>=2.3.1,<3",
53
53
  "syrupy>=4.9.1,<5",
54
54
  "pdoc>=15.0.4,<16",
55
+ "pyyaml>=6.0.3",
56
+ "pyshark>=0.6",
55
57
  ]
56
58
 
57
59
  [tool.hatch.build.targets.sdist]
@@ -67,7 +69,18 @@ build-backend = "hatchling.build"
67
69
  [tool.semantic_release]
68
70
  branch = "main"
69
71
  version_toml = ["pyproject.toml:project.version"]
70
- build_command = "pip install uv && uv build"
72
+ build_command = "pip install uv && uv lock --upgrade-package python-roborock && git add uv.lock && uv build"
73
+ changelog_file = 'CHANGELOG.md'
74
+ commit = true
75
+
76
+ [tool.semantic_release.branches.main]
77
+ match = "main"
78
+ prerelease = false
79
+
80
+ [tool.semantic_release.branches.temp-main-branch]
81
+ match = "temp-main-branch"
82
+ prerelease = false
83
+
71
84
 
72
85
  [tool.semantic_release.commit_parser_options]
73
86
  allowed_tags = [
@@ -92,9 +105,3 @@ asyncio_mode = "auto"
92
105
  asyncio_default_fixture_loop_scope = "function"
93
106
  timeout = 30
94
107
  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
- ]
@@ -731,6 +731,16 @@ async def home(ctx, device_id: str, refresh: bool):
731
731
  click.echo("No maps discovered")
732
732
 
733
733
 
734
+ @session.command()
735
+ @click.option("--device_id", required=True)
736
+ @click.pass_context
737
+ @async_command
738
+ async def network_info(ctx, device_id: str):
739
+ """Get device network information."""
740
+ context: RoborockContext = ctx.obj
741
+ await _display_v1_trait(context, device_id, lambda v1: v1.network_info)
742
+
743
+
734
744
  @click.command()
735
745
  @click.option("--device_id", required=True)
736
746
  @click.option("--cmd", required=True)
@@ -979,6 +989,7 @@ cli.add_command(child_lock)
979
989
  cli.add_command(dnd)
980
990
  cli.add_command(flow_led_status)
981
991
  cli.add_command(led_status)
992
+ cli.add_command(network_info)
982
993
 
983
994
 
984
995
  def main():
@@ -156,15 +156,15 @@ class Status(RoborockBase):
156
156
 
157
157
  @property
158
158
  def error_code_name(self) -> str | None:
159
- return self.error_code.name if self.error_code else None
159
+ return self.error_code.name if self.error_code is not None else None
160
160
 
161
161
  @property
162
162
  def state_name(self) -> str | None:
163
- return self.state.name if self.state else None
163
+ return self.state.name if self.state is not None else None
164
164
 
165
165
  @property
166
166
  def water_box_mode_name(self) -> str | None:
167
- return self.water_box_mode.name if self.water_box_mode else None
167
+ return self.water_box_mode.name if self.water_box_mode is not None else None
168
168
 
169
169
  @property
170
170
  def fan_power_options(self) -> list[str]:
@@ -174,11 +174,11 @@ class Status(RoborockBase):
174
174
 
175
175
  @property
176
176
  def fan_power_name(self) -> str | None:
177
- return self.fan_power.name if self.fan_power else None
177
+ return self.fan_power.name if self.fan_power is not None else None
178
178
 
179
179
  @property
180
180
  def mop_mode_name(self) -> str | None:
181
- return self.mop_mode.name if self.mop_mode else None
181
+ return self.mop_mode.name if self.mop_mode is not None else None
182
182
 
183
183
  def get_fan_speed_code(self, fan_speed: str) -> int:
184
184
  if self.fan_power is None:
@@ -449,6 +449,12 @@ class CleanRecord(RoborockBase):
449
449
  return _attr_repr(self)
450
450
 
451
451
 
452
+ class CleanSummaryWithDetail(CleanSummary):
453
+ """CleanSummary with the last CleanRecord included."""
454
+
455
+ last_clean_record: CleanRecord | None = None
456
+
457
+
452
458
  @dataclass
453
459
  class Consumable(RoborockBase):
454
460
  main_brush_work_time: int | None = None
@@ -6,7 +6,8 @@ until the API is stable.
6
6
 
7
7
  import logging
8
8
  from abc import ABC
9
- from collections.abc import Callable
9
+ from collections.abc import Callable, Mapping
10
+ from typing import Any, TypeVar, cast
10
11
 
11
12
  from roborock.data import HomeDataDevice, HomeDataProduct
12
13
  from roborock.roborock_message import RoborockMessage
@@ -113,3 +114,41 @@ class RoborockDevice(ABC, TraitsMixin):
113
114
  def _on_message(self, message: RoborockMessage) -> None:
114
115
  """Handle incoming messages from the device."""
115
116
  _LOGGER.debug("Received message from device: %s", message)
117
+
118
+ def diagnostic_data(self) -> dict[str, Any]:
119
+ """Return diagnostics information about the device."""
120
+ extra: dict[str, Any] = {}
121
+ if self.v1_properties:
122
+ extra["traits"] = _redact_data(self.v1_properties.as_dict())
123
+ return {
124
+ "device": _redact_data(self.device_info.as_dict()),
125
+ "product": _redact_data(self.product.as_dict()),
126
+ **extra,
127
+ }
128
+
129
+
130
+ T = TypeVar("T")
131
+
132
+ REDACT_KEYS = {"duid", "localKey", "mac", "bssid", "sn", "ip"}
133
+ REDACTED = "**REDACTED**"
134
+
135
+
136
+ def _redact_data(data: T) -> T | dict[str, Any]:
137
+ """Redact sensitive data in a dict."""
138
+ if not isinstance(data, (Mapping, list)):
139
+ return data
140
+
141
+ if isinstance(data, list):
142
+ return cast(T, [_redact_data(item) for item in data])
143
+
144
+ redacted = {**data}
145
+
146
+ for key, value in redacted.items():
147
+ if key in REDACT_KEYS:
148
+ redacted[key] = REDACTED
149
+ elif isinstance(value, dict):
150
+ redacted[key] = _redact_data(value)
151
+ elif isinstance(value, list):
152
+ redacted[key] = [_redact_data(item) for item in value]
153
+
154
+ return redacted
@@ -157,6 +157,7 @@ async def create_device_manager(
157
157
  case DeviceVersion.V1:
158
158
  channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache)
159
159
  trait = v1.create(
160
+ device.duid,
160
161
  product,
161
162
  home_data,
162
163
  channel.rpc_channel,
@@ -34,7 +34,7 @@ import logging
34
34
  from dataclasses import dataclass, field, fields
35
35
  from typing import Any, get_args
36
36
 
37
- from roborock.data.containers import HomeData, HomeDataProduct
37
+ from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase
38
38
  from roborock.data.v1.v1_code_mappings import RoborockDockTypeCode
39
39
  from roborock.devices.cache import Cache
40
40
  from roborock.devices.traits import Trait
@@ -42,7 +42,6 @@ from roborock.devices.v1_rpc_channel import V1RpcChannel
42
42
  from roborock.map.map_parser import MapParserConfig
43
43
 
44
44
  from .child_lock import ChildLockTrait
45
- from .clean_record import CleanRecordTrait
46
45
  from .clean_summary import CleanSummaryTrait
47
46
  from .command import CommandTrait
48
47
  from .common import V1TraitMixin
@@ -55,6 +54,7 @@ from .home import HomeTrait
55
54
  from .led_status import LedStatusTrait
56
55
  from .map_content import MapContentTrait
57
56
  from .maps import MapsTrait
57
+ from .network_info import NetworkInfoTrait
58
58
  from .rooms import RoomsTrait
59
59
  from .smart_wash_params import SmartWashParamsTrait
60
60
  from .status import StatusTrait
@@ -70,7 +70,6 @@ __all__ = [
70
70
  "StatusTrait",
71
71
  "DoNotDisturbTrait",
72
72
  "CleanSummaryTrait",
73
- "CleanRecordTrait",
74
73
  "SoundVolumeTrait",
75
74
  "MapsTrait",
76
75
  "MapContentTrait",
@@ -85,6 +84,7 @@ __all__ = [
85
84
  "DustCollectionModeTrait",
86
85
  "WashTowelModeTrait",
87
86
  "SmartWashParamsTrait",
87
+ "NetworkInfoTrait",
88
88
  ]
89
89
 
90
90
 
@@ -100,7 +100,6 @@ class PropertiesApi(Trait):
100
100
  command: CommandTrait
101
101
  dnd: DoNotDisturbTrait
102
102
  clean_summary: CleanSummaryTrait
103
- clean_record: CleanRecordTrait
104
103
  sound_volume: SoundVolumeTrait
105
104
  rooms: RoomsTrait
106
105
  maps: MapsTrait
@@ -108,6 +107,7 @@ class PropertiesApi(Trait):
108
107
  consumables: ConsumableTrait
109
108
  home: HomeTrait
110
109
  device_features: DeviceFeaturesTrait
110
+ network_info: NetworkInfoTrait
111
111
 
112
112
  # Optional features that may not be supported on all devices
113
113
  child_lock: ChildLockTrait | None = None
@@ -120,6 +120,7 @@ class PropertiesApi(Trait):
120
120
 
121
121
  def __init__(
122
122
  self,
123
+ device_uid: str,
123
124
  product: HomeDataProduct,
124
125
  home_data: HomeData,
125
126
  rpc_channel: V1RpcChannel,
@@ -129,20 +130,20 @@ class PropertiesApi(Trait):
129
130
  map_parser_config: MapParserConfig | None = None,
130
131
  ) -> None:
131
132
  """Initialize the V1TraitProps."""
133
+ self._device_uid = device_uid
132
134
  self._rpc_channel = rpc_channel
133
135
  self._mqtt_rpc_channel = mqtt_rpc_channel
134
136
  self._map_rpc_channel = map_rpc_channel
135
137
  self._cache = cache
136
138
 
137
139
  self.status = StatusTrait(product)
138
- self.clean_summary = CleanSummaryTrait()
139
- self.clean_record = CleanRecordTrait(self.clean_summary)
140
140
  self.consumables = ConsumableTrait()
141
141
  self.rooms = RoomsTrait(home_data)
142
142
  self.maps = MapsTrait(self.status)
143
143
  self.map_content = MapContentTrait(map_parser_config)
144
144
  self.home = HomeTrait(self.status, self.maps, self.rooms, cache)
145
145
  self.device_features = DeviceFeaturesTrait(product.product_nickname, cache)
146
+ self.network_info = NetworkInfoTrait(device_uid, cache)
146
147
 
147
148
  # Dynamically create any traits that need to be populated
148
149
  for item in fields(self):
@@ -246,8 +247,21 @@ class PropertiesApi(Trait):
246
247
  _LOGGER.debug("Updating cached trait data: %s", cache_data.trait_data)
247
248
  await self._cache.set(cache_data)
248
249
 
250
+ def as_dict(self) -> dict[str, Any]:
251
+ """Return the trait data as a dictionary."""
252
+ result: dict[str, Any] = {}
253
+ for item in fields(self):
254
+ trait = getattr(self, item.name, None)
255
+ if trait is None or not isinstance(trait, RoborockBase):
256
+ continue
257
+ data = trait.as_dict()
258
+ if data: # Don't omit unset traits
259
+ result[item.name] = data
260
+ return result
261
+
249
262
 
250
263
  def create(
264
+ device_uid: str,
251
265
  product: HomeDataProduct,
252
266
  home_data: HomeData,
253
267
  rpc_channel: V1RpcChannel,
@@ -257,4 +271,13 @@ def create(
257
271
  map_parser_config: MapParserConfig | None = None,
258
272
  ) -> PropertiesApi:
259
273
  """Create traits for V1 devices."""
260
- return PropertiesApi(product, home_data, rpc_channel, mqtt_rpc_channel, map_rpc_channel, cache, map_parser_config)
274
+ return PropertiesApi(
275
+ device_uid,
276
+ product,
277
+ home_data,
278
+ rpc_channel,
279
+ mqtt_rpc_channel,
280
+ map_rpc_channel,
281
+ cache,
282
+ map_parser_config,
283
+ )
@@ -1,44 +1,57 @@
1
- """Trait for getting the last clean record."""
2
-
3
1
  import logging
4
2
  from typing import Self
5
3
 
6
- from roborock.data import CleanRecord
4
+ from roborock.data import CleanRecord, CleanSummaryWithDetail
7
5
  from roborock.devices.traits.v1 import common
8
6
  from roborock.roborock_typing import RoborockCommand
9
7
  from roborock.util import unpack_list
10
8
 
11
- from .clean_summary import CleanSummaryTrait
12
-
13
9
  _LOGGER = logging.getLogger(__name__)
14
10
 
15
11
 
16
- class CleanRecordTrait(CleanRecord, common.V1TraitMixin):
17
- """Trait for getting the last clean record."""
12
+ class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
13
+ """Trait for managing the clean summary of Roborock devices."""
18
14
 
19
- command = RoborockCommand.GET_CLEAN_RECORD
20
-
21
- def __init__(self, clean_summary_trait: CleanSummaryTrait) -> None:
22
- """Initialize the clean record trait."""
23
- super().__init__()
24
- self._clean_summary_trait = clean_summary_trait
15
+ command = RoborockCommand.GET_CLEAN_SUMMARY
25
16
 
26
17
  async def refresh(self) -> Self:
27
- """Get the last clean record.
18
+ """Refresh the clean summary data and last clean record.
28
19
 
29
20
  Assumes that the clean summary has already been fetched.
30
21
  """
31
- if not self._clean_summary_trait.records:
22
+ await super().refresh()
23
+ if not self.records:
32
24
  _LOGGER.debug("No clean records available in clean summary.")
25
+ self.last_clean_record = None
33
26
  return self
34
- last_record_id = self._clean_summary_trait.records[-1]
35
- response = await self.rpc_channel.send_command(self.command, params=[last_record_id])
36
- new_self = self._parse_response(response)
37
- self._update_trait_values(new_self)
27
+ last_record_id = self.records[-1]
28
+ self.last_clean_record = await self.get_clean_record(last_record_id)
38
29
  return self
39
30
 
40
31
  @classmethod
41
- def _parse_type_response(cls, response: common.V1ResponseData) -> CleanRecord:
32
+ def _parse_type_response(cls, response: common.V1ResponseData) -> Self:
33
+ """Parse the response from the device into a CleanSummary."""
34
+ if isinstance(response, dict):
35
+ return cls.from_dict(response)
36
+ elif isinstance(response, list):
37
+ clean_time, clean_area, clean_count, records = unpack_list(response, 4)
38
+ return cls(
39
+ clean_time=clean_time,
40
+ clean_area=clean_area,
41
+ clean_count=clean_count,
42
+ records=records,
43
+ )
44
+ elif isinstance(response, int):
45
+ return cls(clean_time=response)
46
+ raise ValueError(f"Unexpected clean summary format: {response!r}")
47
+
48
+ async def get_clean_record(self, record_id: int) -> CleanRecord:
49
+ """Load a specific clean record by ID."""
50
+ response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
51
+ return self._parse_clean_record_response(response)
52
+
53
+ @classmethod
54
+ def _parse_clean_record_response(cls, response: common.V1ResponseData) -> CleanRecord:
42
55
  """Parse the response from the device into a CleanRecord."""
43
56
  if isinstance(response, dict):
44
57
  return CleanRecord.from_dict(response)
@@ -47,7 +60,7 @@ class CleanRecordTrait(CleanRecord, common.V1TraitMixin):
47
60
  records = [CleanRecord.from_dict(rec) for rec in response]
48
61
  final_record = records[-1]
49
62
  try:
50
- # This code is semi-presumptions - so it is put in a try finally to be safe.
63
+ # This code is semi-presumptuous - so it is put in a try finally to be safe.
51
64
  final_record.begin = records[0].begin
52
65
  final_record.begin_datetime = records[0].begin_datetime
53
66
  final_record.start_type = records[0].start_type
@@ -1,3 +1,4 @@
1
+ from dataclasses import fields
1
2
  from typing import Self
2
3
 
3
4
  from roborock.data import AppInitStatus, RoborockProductNickname
@@ -16,6 +17,10 @@ class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
16
17
  """Initialize MapContentTrait."""
17
18
  self._nickname = product_nickname
18
19
  self._cache = cache
20
+ # All fields of DeviceFeatures are required. Initialize them to False
21
+ # so we have some known state.
22
+ for field in fields(self):
23
+ setattr(self, field.name, False)
19
24
 
20
25
  async def refresh(self) -> Self:
21
26
  """Refresh the contents of this trait.
@@ -0,0 +1,57 @@
1
+ """Trait for device network information."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Self
7
+
8
+ from roborock.data import NetworkInfo
9
+ from roborock.devices.cache import Cache
10
+ from roborock.devices.traits.v1 import common
11
+ from roborock.roborock_typing import RoborockCommand
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+
16
+ class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
17
+ """Trait for device network information.
18
+
19
+ This trait will always prefer reading from the cache if available. This
20
+ information is usually already fetched when creating the device local
21
+ connection, so reading from the cache avoids an unnecessary RPC call.
22
+ However, we have the fallback to reading from the device if the cache is
23
+ not populated for some reason.
24
+ """
25
+
26
+ command = RoborockCommand.GET_NETWORK_INFO
27
+
28
+ def __init__(self, device_uid: str, cache: Cache) -> None:
29
+ """Initialize the trait."""
30
+ self._device_uid = device_uid
31
+ self._cache = cache
32
+ self.ip = ""
33
+
34
+ async def refresh(self) -> Self:
35
+ """Refresh the network info from the cache."""
36
+
37
+ cache_data = await self._cache.get()
38
+ if cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
39
+ _LOGGER.debug("Using cached network info for device %s", self._device_uid)
40
+ self._update_trait_values(network_info)
41
+ return self
42
+
43
+ # Load from device if not in cache
44
+ _LOGGER.debug("No cached network info for device %s, fetching from device", self._device_uid)
45
+ await super().refresh()
46
+
47
+ # Update the cache with the new network info
48
+ cache_data.network_info[self._device_uid] = self
49
+ await self._cache.set(cache_data)
50
+
51
+ return self
52
+
53
+ def _parse_response(self, response: common.V1ResponseData) -> NetworkInfo:
54
+ """Parse the response from the device into a NetworkInfo."""
55
+ if not isinstance(response, dict):
56
+ raise ValueError(f"Unexpected NetworkInfoTrait response format: {response!r}")
57
+ return NetworkInfo.from_dict(response)
@@ -207,7 +207,7 @@ class RoborockClientV1(RoborockClient, ABC):
207
207
  records = [CleanRecord.from_dict(rec) for rec in record]
208
208
  final_record = records[-1]
209
209
  try:
210
- # This code is semi-presumptions - so it is put in a try finally to be safe.
210
+ # This code is semi-presumptuous - so it is put in a try finally to be safe.
211
211
  final_record.begin = records[0].begin
212
212
  final_record.begin_datetime = records[0].begin_datetime
213
213
  final_record.start_type = records[0].start_type
@@ -1,29 +0,0 @@
1
- from typing import Self
2
-
3
- from roborock.data import CleanSummary
4
- from roborock.devices.traits.v1 import common
5
- from roborock.roborock_typing import RoborockCommand
6
- from roborock.util import unpack_list
7
-
8
-
9
- class CleanSummaryTrait(CleanSummary, common.V1TraitMixin):
10
- """Trait for managing the clean summary of Roborock devices."""
11
-
12
- command = RoborockCommand.GET_CLEAN_SUMMARY
13
-
14
- @classmethod
15
- def _parse_type_response(cls, response: common.V1ResponseData) -> Self:
16
- """Parse the response from the device into a CleanSummary."""
17
- if isinstance(response, dict):
18
- return cls.from_dict(response)
19
- elif isinstance(response, list):
20
- clean_time, clean_area, clean_count, records = unpack_list(response, 4)
21
- return cls(
22
- clean_time=clean_time,
23
- clean_area=clean_area,
24
- clean_count=clean_count,
25
- records=records,
26
- )
27
- elif isinstance(response, int):
28
- return cls(clean_time=response)
29
- raise ValueError(f"Unexpected clean summary format: {response!r}")
File without changes