Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion roborock/data/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,26 @@ def summary_info(self) -> str:

@cached_property
def supported_schema_codes(self) -> set[str]:
"""Return a set of fields that are supported by the device."""
"""Return a set of schema codes that are supported by the device.

These correspond with string field names like "state" or "error_code" that
correspond to RoborockDataProtocol or RoborockB01Protocol code values.
"""
if self.schema is None:
return set()
return {schema.code for schema in self.schema if schema.code is not None}

@cached_property
def supported_schema_ids(self) -> set[int]:
"""Return a set of schema IDs (DPS integers) that are supported by the device.

These correspond to RoborockMessageProtocol and RoborockDataProtocol or
RoborockB01Protocol enum number values (depends on the device protocol versions).
"""
if self.schema is None:
return set()
return {int(schema.id) for schema in self.schema if schema.id is not None}


@dataclass
class HomeDataDevice(RoborockBase):
Expand Down
53 changes: 25 additions & 28 deletions roborock/data/v1/v1_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
ROBOROCK_G20S_Ultra,
)
from roborock.exceptions import RoborockException
from roborock.roborock_message import RoborockDataProtocol

from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr
from .v1_clean_modes import WashTowelModes
Expand Down Expand Up @@ -102,9 +103,8 @@ class StatusField(FieldNameBase):
This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait`
to understand if a feature is supported by the device using `is_field_supported`.

The enum values are names of fields in the `Status` class. Each field is
annotated with `requires_schema_code` metadata to map the field to a schema
code in the product schema, which may have a different name than the field/attribute name.
The enum values are names of fields in the `Status` class. Each field is annotated
with a metadata value to determine if the field is supported by the device.
"""

STATE = "state"
Expand All @@ -116,21 +116,17 @@ class StatusField(FieldNameBase):
ERROR_CODE = "error_code"


def _requires_schema_code(requires_schema_code: str, default=None) -> Any:
return field(metadata={"requires_schema_code": requires_schema_code}, default=default)


@dataclass
class Status(RoborockBase):
"""This status will be deprecated in favor of StatusV2."""

msg_ver: int | None = None
msg_seq: int | None = None
state: RoborockStateCode | None = _requires_schema_code("state")
battery: int | None = _requires_schema_code("battery")
state: RoborockStateCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.STATE})
battery: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.BATTERY})
clean_time: int | None = None
clean_area: int | None = None
error_code: RoborockErrorCode | None = _requires_schema_code("error_code")
error_code: RoborockErrorCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.ERROR_CODE})
map_present: int | None = None
in_cleaning: RoborockInCleaning | None = None
in_returning: int | None = None
Expand All @@ -140,12 +136,14 @@ class Status(RoborockBase):
back_type: int | None = None
wash_phase: int | None = None
wash_ready: int | None = None
fan_power: RoborockFanPowerCode | None = _requires_schema_code("fan_power")
fan_power: RoborockFanPowerCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.FAN_POWER})
dnd_enabled: int | None = None
map_status: int | None = None
is_locating: int | None = None
lock_status: int | None = None
water_box_mode: RoborockMopIntensityCode | None = _requires_schema_code("water_box_mode")
water_box_mode: RoborockMopIntensityCode | None = field(
default=None, metadata={"dps": RoborockDataProtocol.WATER_BOX_MODE}
)
water_box_carriage_status: int | None = None
mop_forbidden_enable: int | None = None
camera_status: int | None = None
Expand All @@ -163,13 +161,13 @@ class Status(RoborockBase):
collision_avoid_status: int | None = None
switch_map_mode: int | None = None
dock_error_status: RoborockDockErrorCode | None = None
charge_status: int | None = _requires_schema_code("charge_status")
charge_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS})
unsave_map_reason: int | None = None
unsave_map_flag: int | None = None
wash_status: int | None = None
distance_off: int | None = None
in_warmup: int | None = None
dry_status: int | None = _requires_schema_code("drying_status")
dry_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.DRYING_STATUS})
rdt: int | None = None
clean_percent: int | None = None
rss: int | None = None
Expand Down Expand Up @@ -294,11 +292,11 @@ class StatusV2(RoborockBase):

msg_ver: int | None = None
msg_seq: int | None = None
state: RoborockStateCode | None = _requires_schema_code("state")
battery: int | None = _requires_schema_code("battery")
state: RoborockStateCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.STATE})
battery: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.BATTERY})
clean_time: int | None = None
clean_area: int | None = None
error_code: RoborockErrorCode | None = _requires_schema_code("error_code")
error_code: RoborockErrorCode | None = field(default=None, metadata={"dps": RoborockDataProtocol.ERROR_CODE})
map_present: int | None = None
in_cleaning: RoborockInCleaning | None = None
in_returning: int | None = None
Expand All @@ -308,12 +306,12 @@ class StatusV2(RoborockBase):
back_type: int | None = None
wash_phase: int | None = None
wash_ready: int | None = None
fan_power: int | None = _requires_schema_code("fan_power")
fan_power: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.FAN_POWER})
dnd_enabled: int | None = None
map_status: int | None = None
is_locating: int | None = None
lock_status: int | None = None
water_box_mode: int | None = _requires_schema_code("water_box_mode")
water_box_mode: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.WATER_BOX_MODE})
water_box_carriage_status: int | None = None
mop_forbidden_enable: int | None = None
camera_status: int | None = None
Expand All @@ -330,14 +328,14 @@ class StatusV2(RoborockBase):
debug_mode: int | None = None
collision_avoid_status: int | None = None
switch_map_mode: int | None = None
dock_error_status: RoborockDockErrorCode | None = _requires_schema_code("dock_error_status")
charge_status: int | None = _requires_schema_code("charge_status")
dock_error_status: RoborockDockErrorCode | None = None
charge_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS})
unsave_map_reason: int | None = None
unsave_map_flag: int | None = None
wash_status: int | None = None
distance_off: int | None = None
in_warmup: int | None = None
dry_status: int | None = _requires_schema_code("drying_status")
dry_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.DRYING_STATUS})
rdt: int | None = None
clean_percent: int | None = None
rss: int | None = None
Expand Down Expand Up @@ -631,9 +629,8 @@ class ConsumableField(FieldNameBase):
This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait`
to understand if a feature is supported by the device using `is_field_supported`.

The enum values are names of fields in the `Consumable` class. Each field is
annotated with `requires_schema_code` metadata to map the field to a schema
code in the product schema, which may have a different name than the field/attribute name.
The enum values are names of fields in the `Consumable` class. Each field is annotated
with a metadata value to determine if the field is supported by the device.
"""

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

@dataclass
class Consumable(RoborockBase):
main_brush_work_time: int | None = field(metadata={"requires_schema_code": "main_brush_life"}, default=None)
side_brush_work_time: int | None = field(metadata={"requires_schema_code": "side_brush_life"}, default=None)
filter_work_time: int | None = field(metadata={"requires_schema_code": "filter_life"}, default=None)
main_brush_work_time: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.MAIN_BRUSH_WORK_TIME})
side_brush_work_time: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.SIDE_BRUSH_WORK_TIME})
filter_work_time: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.FILTER_WORK_TIME})
filter_element_work_time: int | None = None
sensor_dirty_time: int | None = None
strainer_work_times: int | None = None
Expand Down
4 changes: 2 additions & 2 deletions roborock/devices/traits/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
Additionally, DeviceFeaturesTrait has a method `is_field_supported` that is used to
check individual trait field values. This is a more fine grained version to allow
optional fields in a dataclass, vs the above feature checks that apply to an entire
trait. The `requires_schema_code` field metadata attribute is a string of the schema
code in HomeDataProduct Schema that is required for the field to be supported.
trait. The `dps` field metadata attribute references a schema code in
HomeDataProduct Schema that is required for the field to be supported.
"""

import logging
Expand Down
31 changes: 15 additions & 16 deletions roborock/devices/traits/v1/device_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,26 @@ def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:
for field in fields(self):
setattr(self, field.name, False)

@staticmethod
def _get_dataclass_field(cls: type[RoborockBase], field_name: FieldNameBase) -> Field:
"""Look up a dataclass field by its FieldNameBase name."""
for f in fields(cls):
if f.name == field_name:
return f
raise ValueError(f"Field {field_name!r} not found in {cls}")

def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
"""Determines if the specified field is supported by this device.

We use dataclass attributes on the field to specify the schema code that is required
for the field to be supported and it is compared against the list of
supported schema codes for the device returned in the product information.
We use the `dps` dataclass field metadata to get the `RoborockDataProtocol`
integer ID and check it against the set of supported schema IDs for the
device returned in the product information.
"""
dataclass_field: Field | None = None
for field in fields(cls):
if field.name == field_name:
dataclass_field = field
break
if dataclass_field is None:
raise ValueError(f"Field {field_name} not found in {cls}")

requires_schema_code = dataclass_field.metadata.get("requires_schema_code", None)
if requires_schema_code is None:
# We assume the field is supported
dataclass_field = self._get_dataclass_field(cls, field_name)
if (dps := dataclass_field.metadata.get("dps")) is None:
# No DPS metadata — field is assumed always supported
return True
# If the field requires a protocol that is not supported, we return False
return requires_schema_code in self._product.supported_schema_codes
return int(dps) in self._product.supported_schema_ids

async def refresh(self) -> None:
"""Refresh the contents of this trait.
Expand Down
2 changes: 1 addition & 1 deletion roborock/roborock_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from enum import StrEnum
from typing import Self

from roborock import RoborockEnum
from roborock.data.code_mappings import RoborockEnum
from roborock.util import get_next_int, get_timestamp


Expand Down
22 changes: 22 additions & 0 deletions tests/data/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
_camelize,
_decamelize,
)
from roborock.roborock_message import RoborockDataProtocol, RoborockMessageProtocol
from tests.mock_data import (
HOME_DATA_RAW,
K_VALUE,
Expand Down Expand Up @@ -213,6 +214,27 @@ def test_home_data():
"task_complete",
"water_box_mode",
}
assert product.supported_schema_ids == {
int(v)
for v in (
RoborockMessageProtocol.RPC_REQUEST,
RoborockMessageProtocol.RPC_RESPONSE,
RoborockDataProtocol.ERROR_CODE,
RoborockDataProtocol.STATE,
RoborockDataProtocol.BATTERY,
RoborockDataProtocol.FAN_POWER,
RoborockDataProtocol.WATER_BOX_MODE,
RoborockDataProtocol.MAIN_BRUSH_WORK_TIME,
RoborockDataProtocol.SIDE_BRUSH_WORK_TIME,
RoborockDataProtocol.FILTER_WORK_TIME,
RoborockDataProtocol.ADDITIONAL_PROPS,
RoborockDataProtocol.TASK_COMPLETE,
RoborockDataProtocol.TASK_CANCEL_LOW_POWER,
RoborockDataProtocol.TASK_CANCEL_IN_MOTION,
RoborockDataProtocol.CHARGE_STATUS,
RoborockDataProtocol.DRYING_STATUS,
)
}

device = hd.devices[0]
assert device.duid == "abc123"
Expand Down
Loading