roborock.devices.traits.v1.device_features

  1from dataclasses import fields
  2from typing import Any
  3
  4from roborock.data import AppInitStatus, HomeDataProduct, RoborockBase
  5from roborock.data.v1 import RoborockDockTypeCode
  6from roborock.data.v1.v1_containers import FieldNameBase
  7from roborock.device_features import DeviceFeatures, RoborockDockFeatures
  8from roborock.devices.cache import DeviceCache
  9from roborock.devices.traits.v1 import common
 10from roborock.roborock_typing import RoborockCommand
 11
 12# Cache of metadata for each trait class
 13_metadata_cache: dict[type[RoborockBase], dict[str, dict[str, Any]]] = {}
 14
 15
 16def _get_field_metadata(cls: type[RoborockBase]) -> dict[str, Any]:
 17    """Helper to get metadata from either class properties or dataclass fields."""
 18    if cls not in _metadata_cache:
 19        metadata_map = {}
 20        # Inspect properties with @field_metadata
 21        for name in dir(cls):
 22            prop = getattr(cls, name, None)
 23            if isinstance(prop, property):
 24                metadata_map[name] = getattr(prop.fget, "_field_metadata", {})
 25        # Inspect dataclass fields metadata
 26        for f in fields(cls):
 27            metadata_map[f.name] = f.metadata
 28        _metadata_cache[cls] = metadata_map
 29    return _metadata_cache[cls]
 30
 31
 32class DeviceTraitsConverter(common.V1TraitDataConverter):
 33    """Converter for APP_GET_INIT_STATUS responses into DeviceFeatures."""
 34
 35    def __init__(self, product: HomeDataProduct) -> None:
 36        """Initialize DeviceTraitsConverter."""
 37        self._product = product
 38
 39    def convert(self, response: common.V1ResponseData) -> DeviceFeatures:
 40        """Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance."""
 41        if not isinstance(response, list):
 42            raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}: {response!r}")
 43        app_status = AppInitStatus.from_dict(response[0])
 44        return DeviceFeatures.from_feature_flags(
 45            new_feature_info=app_status.new_feature_info,
 46            new_feature_info_str=app_status.new_feature_info_str,
 47            feature_info=app_status.feature_info,
 48            product_nickname=self._product.product_nickname,
 49        )
 50
 51
 52class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
 53    """Trait for managing supported features on Roborock devices."""
 54
 55    command = RoborockCommand.APP_GET_INIT_STATUS
 56    converter: DeviceTraitsConverter
 57
 58    def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:  # pylint: disable=super-init-not-called
 59        """Initialize DeviceFeaturesTrait."""
 60        common.V1TraitMixin.__init__(self)
 61        self.converter = DeviceTraitsConverter(product)
 62        self._product = product
 63        self._device_cache = device_cache
 64        # Dock features are populated after device feature discovery
 65        # is triggered.
 66        self.dock_features: RoborockDockFeatures = RoborockDockFeatures.from_dock_type(RoborockDockTypeCode.o0_dock)
 67        # All fields of DeviceFeatures are required. Initialize them to False
 68        # so we have some known state.
 69        for field in fields(self):
 70            setattr(self, field.name, False)
 71
 72    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
 73        """Determines if the specified field is supported by this device.
 74
 75        We inspect the metadata defined for the field (either via dataclass field metadata
 76        or the `@field_metadata` decorator on properties). Supported checks include:
 77
 78        - `feature`: Maps to a boolean capability property on `DeviceFeatures` / `DeviceFeaturesTrait`
 79          (e.g. `is_support_water_mode`).
 80        - `dock_feature`: Maps to a boolean capability property on `RoborockDockFeatures` (e.g. `is_washable`).
 81        - `dps`: Maps to a `RoborockDataProtocol` ID checked against the product's supported schema IDs.
 82        """
 83        if self.dock_features is None:
 84            raise ValueError("DeviceFeaturesTrait was invoked but was not fully initialized")
 85        metadata_map = _get_field_metadata(cls)
 86        if (field_metadata := metadata_map.get(field_name)) is not None:
 87            if (feature := field_metadata.get("feature")) is not None:
 88                return getattr(self, feature, False)
 89            if (dock_feature := field_metadata.get("dock_feature")) is not None:
 90                return getattr(self.dock_features, dock_feature, False)
 91            if (dps := field_metadata.get("dps")) is not None:
 92                return int(dps) in self._product.supported_schema_ids
 93        # No metadata, field is assumed always supported
 94        return True
 95
 96    async def refresh(self) -> None:
 97        """Refresh the contents of this trait.
 98
 99        This will use cached device features if available since they do not
100        change often and this avoids unnecessary RPC calls. This would only
101        ever change with a firmware update, so caching is appropriate.
102        """
103        cache_data = await self._device_cache.get()
104        if cache_data.device_features is not None:
105            common.merge_trait_values(self, cache_data.device_features)
106            return
107        # Save cached device features
108        await super().refresh()
109        cache_data.device_features = self
110        await self._device_cache.set(cache_data)
class DeviceTraitsConverter(roborock.devices.traits.v1.common.V1TraitDataConverter):
33class DeviceTraitsConverter(common.V1TraitDataConverter):
34    """Converter for APP_GET_INIT_STATUS responses into DeviceFeatures."""
35
36    def __init__(self, product: HomeDataProduct) -> None:
37        """Initialize DeviceTraitsConverter."""
38        self._product = product
39
40    def convert(self, response: common.V1ResponseData) -> DeviceFeatures:
41        """Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance."""
42        if not isinstance(response, list):
43            raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}: {response!r}")
44        app_status = AppInitStatus.from_dict(response[0])
45        return DeviceFeatures.from_feature_flags(
46            new_feature_info=app_status.new_feature_info,
47            new_feature_info_str=app_status.new_feature_info_str,
48            feature_info=app_status.feature_info,
49            product_nickname=self._product.product_nickname,
50        )

Converter for APP_GET_INIT_STATUS responses into DeviceFeatures.

DeviceTraitsConverter(product: roborock.data.containers.HomeDataProduct)
36    def __init__(self, product: HomeDataProduct) -> None:
37        """Initialize DeviceTraitsConverter."""
38        self._product = product

Initialize DeviceTraitsConverter.

def convert( self, response: dict | list | int | str | bytes) -> roborock.device_features.DeviceFeatures:
40    def convert(self, response: common.V1ResponseData) -> DeviceFeatures:
41        """Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance."""
42        if not isinstance(response, list):
43            raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}: {response!r}")
44        app_status = AppInitStatus.from_dict(response[0])
45        return DeviceFeatures.from_feature_flags(
46            new_feature_info=app_status.new_feature_info,
47            new_feature_info_str=app_status.new_feature_info_str,
48            feature_info=app_status.feature_info,
49            product_nickname=self._product.product_nickname,
50        )

Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance.

class DeviceFeaturesTrait(roborock.device_features.DeviceFeatures, roborock.devices.traits.v1.common.V1TraitMixin):
 53class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
 54    """Trait for managing supported features on Roborock devices."""
 55
 56    command = RoborockCommand.APP_GET_INIT_STATUS
 57    converter: DeviceTraitsConverter
 58
 59    def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:  # pylint: disable=super-init-not-called
 60        """Initialize DeviceFeaturesTrait."""
 61        common.V1TraitMixin.__init__(self)
 62        self.converter = DeviceTraitsConverter(product)
 63        self._product = product
 64        self._device_cache = device_cache
 65        # Dock features are populated after device feature discovery
 66        # is triggered.
 67        self.dock_features: RoborockDockFeatures = RoborockDockFeatures.from_dock_type(RoborockDockTypeCode.o0_dock)
 68        # All fields of DeviceFeatures are required. Initialize them to False
 69        # so we have some known state.
 70        for field in fields(self):
 71            setattr(self, field.name, False)
 72
 73    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
 74        """Determines if the specified field is supported by this device.
 75
 76        We inspect the metadata defined for the field (either via dataclass field metadata
 77        or the `@field_metadata` decorator on properties). Supported checks include:
 78
 79        - `feature`: Maps to a boolean capability property on `DeviceFeatures` / `DeviceFeaturesTrait`
 80          (e.g. `is_support_water_mode`).
 81        - `dock_feature`: Maps to a boolean capability property on `RoborockDockFeatures` (e.g. `is_washable`).
 82        - `dps`: Maps to a `RoborockDataProtocol` ID checked against the product's supported schema IDs.
 83        """
 84        if self.dock_features is None:
 85            raise ValueError("DeviceFeaturesTrait was invoked but was not fully initialized")
 86        metadata_map = _get_field_metadata(cls)
 87        if (field_metadata := metadata_map.get(field_name)) is not None:
 88            if (feature := field_metadata.get("feature")) is not None:
 89                return getattr(self, feature, False)
 90            if (dock_feature := field_metadata.get("dock_feature")) is not None:
 91                return getattr(self.dock_features, dock_feature, False)
 92            if (dps := field_metadata.get("dps")) is not None:
 93                return int(dps) in self._product.supported_schema_ids
 94        # No metadata, field is assumed always supported
 95        return True
 96
 97    async def refresh(self) -> None:
 98        """Refresh the contents of this trait.
 99
100        This will use cached device features if available since they do not
101        change often and this avoids unnecessary RPC calls. This would only
102        ever change with a firmware update, so caching is appropriate.
103        """
104        cache_data = await self._device_cache.get()
105        if cache_data.device_features is not None:
106            common.merge_trait_values(self, cache_data.device_features)
107            return
108        # Save cached device features
109        await super().refresh()
110        cache_data.device_features = self
111        await self._device_cache.set(cache_data)

Trait for managing supported features on Roborock devices.

DeviceFeaturesTrait( product: roborock.data.containers.HomeDataProduct, device_cache: roborock.devices.cache.DeviceCache)
59    def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:  # pylint: disable=super-init-not-called
60        """Initialize DeviceFeaturesTrait."""
61        common.V1TraitMixin.__init__(self)
62        self.converter = DeviceTraitsConverter(product)
63        self._product = product
64        self._device_cache = device_cache
65        # Dock features are populated after device feature discovery
66        # is triggered.
67        self.dock_features: RoborockDockFeatures = RoborockDockFeatures.from_dock_type(RoborockDockTypeCode.o0_dock)
68        # All fields of DeviceFeatures are required. Initialize them to False
69        # so we have some known state.
70        for field in fields(self):
71            setattr(self, field.name, False)

Initialize DeviceFeaturesTrait.

command = <RoborockCommand.APP_GET_INIT_STATUS: 'app_get_init_status'>

The RoborockCommand used to fetch the trait data from the device (internal only).

The converter used to parse the response from the device (internal only).

dock_features: roborock.device_features.RoborockDockFeatures
def is_field_supported( self, cls: type[roborock.data.containers.RoborockBase], field_name: roborock.data.v1.v1_containers.FieldNameBase) -> bool:
73    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
74        """Determines if the specified field is supported by this device.
75
76        We inspect the metadata defined for the field (either via dataclass field metadata
77        or the `@field_metadata` decorator on properties). Supported checks include:
78
79        - `feature`: Maps to a boolean capability property on `DeviceFeatures` / `DeviceFeaturesTrait`
80          (e.g. `is_support_water_mode`).
81        - `dock_feature`: Maps to a boolean capability property on `RoborockDockFeatures` (e.g. `is_washable`).
82        - `dps`: Maps to a `RoborockDataProtocol` ID checked against the product's supported schema IDs.
83        """
84        if self.dock_features is None:
85            raise ValueError("DeviceFeaturesTrait was invoked but was not fully initialized")
86        metadata_map = _get_field_metadata(cls)
87        if (field_metadata := metadata_map.get(field_name)) is not None:
88            if (feature := field_metadata.get("feature")) is not None:
89                return getattr(self, feature, False)
90            if (dock_feature := field_metadata.get("dock_feature")) is not None:
91                return getattr(self.dock_features, dock_feature, False)
92            if (dps := field_metadata.get("dps")) is not None:
93                return int(dps) in self._product.supported_schema_ids
94        # No metadata, field is assumed always supported
95        return True

Determines if the specified field is supported by this device.

We inspect the metadata defined for the field (either via dataclass field metadata or the @field_metadata decorator on properties). Supported checks include:

  • feature: Maps to a boolean capability property on DeviceFeatures / DeviceFeaturesTrait (e.g. is_support_water_mode).
  • dock_feature: Maps to a boolean capability property on RoborockDockFeatures (e.g. is_washable).
  • dps: Maps to a RoborockDataProtocol ID checked against the product's supported schema IDs.
async def refresh(self) -> None:
 97    async def refresh(self) -> None:
 98        """Refresh the contents of this trait.
 99
100        This will use cached device features if available since they do not
101        change often and this avoids unnecessary RPC calls. This would only
102        ever change with a firmware update, so caching is appropriate.
103        """
104        cache_data = await self._device_cache.get()
105        if cache_data.device_features is not None:
106            common.merge_trait_values(self, cache_data.device_features)
107            return
108        # Save cached device features
109        await super().refresh()
110        cache_data.device_features = self
111        await self._device_cache.set(cache_data)

Refresh the contents of this trait.

This will use cached device features if available since they do not change often and this avoids unnecessary RPC calls. This would only ever change with a firmware update, so caching is appropriate.