Skip to content

Commit 4fcef24

Browse files
authored
refactor: replace schema code strings with RoborockDataProtocol enums (#808)
* chore: replace schema code strings with RoborockDataProtocol enum values in status containers and add supported_schema_ids helper * feat: update supported_schema_ids to include additional RoborockMessageProtocol and RoborockDataProtocol constants * chore: update dps field metadata description in DeviceFeaturesTrait docstring
1 parent 011c9d1 commit 4fcef24

File tree

6 files changed

+81
-48
lines changed

6 files changed

+81
-48
lines changed

roborock/data/containers.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,26 @@ def summary_info(self) -> str:
251251

252252
@cached_property
253253
def supported_schema_codes(self) -> set[str]:
254-
"""Return a set of fields that are supported by the device."""
254+
"""Return a set of schema codes that are supported by the device.
255+
256+
These correspond with string field names like "state" or "error_code" that
257+
correspond to RoborockDataProtocol or RoborockB01Protocol code values.
258+
"""
255259
if self.schema is None:
256260
return set()
257261
return {schema.code for schema in self.schema if schema.code is not None}
258262

263+
@cached_property
264+
def supported_schema_ids(self) -> set[int]:
265+
"""Return a set of schema IDs (DPS integers) that are supported by the device.
266+
267+
These correspond to RoborockMessageProtocol and RoborockDataProtocol or
268+
RoborockB01Protocol enum number values (depends on the device protocol versions).
269+
"""
270+
if self.schema is None:
271+
return set()
272+
return {int(schema.id) for schema in self.schema if schema.id is not None}
273+
259274

260275
@dataclass
261276
class HomeDataDevice(RoborockBase):

roborock/data/v1/v1_containers.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
ROBOROCK_G20S_Ultra,
3838
)
3939
from roborock.exceptions import RoborockException
40+
from roborock.roborock_message import RoborockDataProtocol
4041

4142
from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr
4243
from .v1_clean_modes import WashTowelModes
@@ -102,9 +103,8 @@ class StatusField(FieldNameBase):
102103
This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait`
103104
to understand if a feature is supported by the device using `is_field_supported`.
104105
105-
The enum values are names of fields in the `Status` class. Each field is
106-
annotated with `requires_schema_code` metadata to map the field to a schema
107-
code in the product schema, which may have a different name than the field/attribute name.
106+
The enum values are names of fields in the `Status` class. Each field is annotated
107+
with a metadata value to determine if the field is supported by the device.
108108
"""
109109

110110
STATE = "state"
@@ -116,21 +116,17 @@ class StatusField(FieldNameBase):
116116
ERROR_CODE = "error_code"
117117

118118

119-
def _requires_schema_code(requires_schema_code: str, default=None) -> Any:
120-
return field(metadata={"requires_schema_code": requires_schema_code}, default=default)
121-
122-
123119
@dataclass
124120
class Status(RoborockBase):
125121
"""This status will be deprecated in favor of StatusV2."""
126122

127123
msg_ver: int | None = None
128124
msg_seq: int | None = None
129-
state: RoborockStateCode | None = _requires_schema_code("state")
130-
battery: int | None = _requires_schema_code("battery")
125+
state: RoborockStateCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.STATE})
126+
battery: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.BATTERY})
131127
clean_time: int | None = None
132128
clean_area: int | None = None
133-
error_code: RoborockErrorCode | None = _requires_schema_code("error_code")
129+
error_code: RoborockErrorCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.ERROR_CODE})
134130
map_present: int | None = None
135131
in_cleaning: RoborockInCleaning | None = None
136132
in_returning: int | None = None
@@ -140,12 +136,14 @@ class Status(RoborockBase):
140136
back_type: int | None = None
141137
wash_phase: int | None = None
142138
wash_ready: int | None = None
143-
fan_power: RoborockFanPowerCode | None = _requires_schema_code("fan_power")
139+
fan_power: RoborockFanPowerCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.FAN_POWER})
144140
dnd_enabled: int | None = None
145141
map_status: int | None = None
146142
is_locating: int | None = None
147143
lock_status: int | None = None
148-
water_box_mode: RoborockMopIntensityCode | None = _requires_schema_code("water_box_mode")
144+
water_box_mode: RoborockMopIntensityCode | None = field(
145+
default=None, metadata={"dps": RoborockDataProtocol.WATER_BOX_MODE}
146+
)
149147
water_box_carriage_status: int | None = None
150148
mop_forbidden_enable: int | None = None
151149
camera_status: int | None = None
@@ -163,13 +161,13 @@ class Status(RoborockBase):
163161
collision_avoid_status: int | None = None
164162
switch_map_mode: int | None = None
165163
dock_error_status: RoborockDockErrorCode | None = None
166-
charge_status: int | None = _requires_schema_code("charge_status")
164+
charge_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS})
167165
unsave_map_reason: int | None = None
168166
unsave_map_flag: int | None = None
169167
wash_status: int | None = None
170168
distance_off: int | None = None
171169
in_warmup: int | None = None
172-
dry_status: int | None = _requires_schema_code("drying_status")
170+
dry_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.DRYING_STATUS})
173171
rdt: int | None = None
174172
clean_percent: int | None = None
175173
rss: int | None = None
@@ -294,11 +292,11 @@ class StatusV2(RoborockBase):
294292

295293
msg_ver: int | None = None
296294
msg_seq: int | None = None
297-
state: RoborockStateCode | None = _requires_schema_code("state")
298-
battery: int | None = _requires_schema_code("battery")
295+
state: RoborockStateCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.STATE})
296+
battery: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.BATTERY})
299297
clean_time: int | None = None
300298
clean_area: int | None = None
301-
error_code: RoborockErrorCode | None = _requires_schema_code("error_code")
299+
error_code: RoborockErrorCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.ERROR_CODE})
302300
map_present: int | None = None
303301
in_cleaning: RoborockInCleaning | None = None
304302
in_returning: int | None = None
@@ -308,12 +306,12 @@ class StatusV2(RoborockBase):
308306
back_type: int | None = None
309307
wash_phase: int | None = None
310308
wash_ready: int | None = None
311-
fan_power: int | None = _requires_schema_code("fan_power")
309+
fan_power: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.FAN_POWER})
312310
dnd_enabled: int | None = None
313311
map_status: int | None = None
314312
is_locating: int | None = None
315313
lock_status: int | None = None
316-
water_box_mode: int | None = _requires_schema_code("water_box_mode")
314+
water_box_mode: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.WATER_BOX_MODE})
317315
water_box_carriage_status: int | None = None
318316
mop_forbidden_enable: int | None = None
319317
camera_status: int | None = None
@@ -330,14 +328,14 @@ class StatusV2(RoborockBase):
330328
debug_mode: int | None = None
331329
collision_avoid_status: int | None = None
332330
switch_map_mode: int | None = None
333-
dock_error_status: RoborockDockErrorCode | None = _requires_schema_code("dock_error_status")
334-
charge_status: int | None = _requires_schema_code("charge_status")
331+
dock_error_status: RoborockDockErrorCode | None = None
332+
charge_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS})
335333
unsave_map_reason: int | None = None
336334
unsave_map_flag: int | None = None
337335
wash_status: int | None = None
338336
distance_off: int | None = None
339337
in_warmup: int | None = None
340-
dry_status: int | None = _requires_schema_code("drying_status")
338+
dry_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.DRYING_STATUS})
341339
rdt: int | None = None
342340
clean_percent: int | None = None
343341
rss: int | None = None
@@ -631,9 +629,8 @@ class ConsumableField(FieldNameBase):
631629
This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait`
632630
to understand if a feature is supported by the device using `is_field_supported`.
633631
634-
The enum values are names of fields in the `Consumable` class. Each field is
635-
annotated with `requires_schema_code` metadata to map the field to a schema
636-
code in the product schema, which may have a different name than the field/attribute name.
632+
The enum values are names of fields in the `Consumable` class. Each field is annotated
633+
with a metadata value to determine if the field is supported by the device.
637634
"""
638635

639636
MAIN_BRUSH_WORK_TIME = "main_brush_work_time"
@@ -643,9 +640,9 @@ class ConsumableField(FieldNameBase):
643640

644641
@dataclass
645642
class Consumable(RoborockBase):
646-
main_brush_work_time: int | None = field(metadata={"requires_schema_code": "main_brush_life"}, default=None)
647-
side_brush_work_time: int | None = field(metadata={"requires_schema_code": "side_brush_life"}, default=None)
648-
filter_work_time: int | None = field(metadata={"requires_schema_code": "filter_life"}, default=None)
643+
main_brush_work_time: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.MAIN_BRUSH_WORK_TIME})
644+
side_brush_work_time: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.SIDE_BRUSH_WORK_TIME})
645+
filter_work_time: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.FILTER_WORK_TIME})
649646
filter_element_work_time: int | None = None
650647
sensor_dirty_time: int | None = None
651648
strainer_work_times: int | None = None

roborock/devices/traits/v1/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
Additionally, DeviceFeaturesTrait has a method `is_field_supported` that is used to
4949
check individual trait field values. This is a more fine grained version to allow
5050
optional fields in a dataclass, vs the above feature checks that apply to an entire
51-
trait. The `requires_schema_code` field metadata attribute is a string of the schema
52-
code in HomeDataProduct Schema that is required for the field to be supported.
51+
trait. The `dps` field metadata attribute references a schema code in
52+
HomeDataProduct Schema that is required for the field to be supported.
5353
"""
5454

5555
import logging

roborock/devices/traits/v1/device_features.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,26 @@ def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:
4545
for field in fields(self):
4646
setattr(self, field.name, False)
4747

48+
@staticmethod
49+
def _get_dataclass_field(cls: type[RoborockBase], field_name: FieldNameBase) -> Field:
50+
"""Look up a dataclass field by its FieldNameBase name."""
51+
for f in fields(cls):
52+
if f.name == field_name:
53+
return f
54+
raise ValueError(f"Field {field_name!r} not found in {cls}")
55+
4856
def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
4957
"""Determines if the specified field is supported by this device.
5058
51-
We use dataclass attributes on the field to specify the schema code that is required
52-
for the field to be supported and it is compared against the list of
53-
supported schema codes for the device returned in the product information.
59+
We use the `dps` dataclass field metadata to get the `RoborockDataProtocol`
60+
integer ID and check it against the set of supported schema IDs for the
61+
device returned in the product information.
5462
"""
55-
dataclass_field: Field | None = None
56-
for field in fields(cls):
57-
if field.name == field_name:
58-
dataclass_field = field
59-
break
60-
if dataclass_field is None:
61-
raise ValueError(f"Field {field_name} not found in {cls}")
62-
63-
requires_schema_code = dataclass_field.metadata.get("requires_schema_code", None)
64-
if requires_schema_code is None:
65-
# We assume the field is supported
63+
dataclass_field = self._get_dataclass_field(cls, field_name)
64+
if (dps := dataclass_field.metadata.get("dps")) is None:
65+
# No DPS metadata — field is assumed always supported
6666
return True
67-
# If the field requires a protocol that is not supported, we return False
68-
return requires_schema_code in self._product.supported_schema_codes
67+
return int(dps) in self._product.supported_schema_ids
6968

7069
async def refresh(self) -> None:
7170
"""Refresh the contents of this trait.

roborock/roborock_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from enum import StrEnum
33
from typing import Self
44

5-
from roborock import RoborockEnum
5+
from roborock.data.code_mappings import RoborockEnum
66
from roborock.util import get_next_int, get_timestamp
77

88

tests/data/test_containers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
_camelize,
1616
_decamelize,
1717
)
18+
from roborock.roborock_message import RoborockDataProtocol, RoborockMessageProtocol
1819
from tests.mock_data import (
1920
HOME_DATA_RAW,
2021
K_VALUE,
@@ -213,6 +214,27 @@ def test_home_data():
213214
"task_complete",
214215
"water_box_mode",
215216
}
217+
assert product.supported_schema_ids == {
218+
int(v)
219+
for v in (
220+
RoborockMessageProtocol.RPC_REQUEST,
221+
RoborockMessageProtocol.RPC_RESPONSE,
222+
RoborockDataProtocol.ERROR_CODE,
223+
RoborockDataProtocol.STATE,
224+
RoborockDataProtocol.BATTERY,
225+
RoborockDataProtocol.FAN_POWER,
226+
RoborockDataProtocol.WATER_BOX_MODE,
227+
RoborockDataProtocol.MAIN_BRUSH_WORK_TIME,
228+
RoborockDataProtocol.SIDE_BRUSH_WORK_TIME,
229+
RoborockDataProtocol.FILTER_WORK_TIME,
230+
RoborockDataProtocol.ADDITIONAL_PROPS,
231+
RoborockDataProtocol.TASK_COMPLETE,
232+
RoborockDataProtocol.TASK_CANCEL_LOW_POWER,
233+
RoborockDataProtocol.TASK_CANCEL_IN_MOTION,
234+
RoborockDataProtocol.CHARGE_STATUS,
235+
RoborockDataProtocol.DRYING_STATUS,
236+
)
237+
}
216238

217239
device = hd.devices[0]
218240
assert device.duid == "abc123"

0 commit comments

Comments
 (0)