roborock.devices.traits.v1.device_features

 1from dataclasses import Field, fields
 2
 3from roborock.data import AppInitStatus, HomeDataProduct, RoborockBase
 4from roborock.data.v1.v1_containers import FieldNameBase
 5from roborock.device_features import DeviceFeatures
 6from roborock.devices.cache import DeviceCache
 7from roborock.devices.traits.v1 import common
 8from roborock.roborock_typing import RoborockCommand
 9
10
11class DeviceTraitsConverter(common.V1TraitDataConverter):
12    """Converter for APP_GET_INIT_STATUS responses into DeviceFeatures."""
13
14    def __init__(self, product: HomeDataProduct) -> None:
15        """Initialize DeviceTraitsConverter."""
16        self._product = product
17
18    def convert(self, response: common.V1ResponseData) -> DeviceFeatures:
19        """Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance."""
20        if not isinstance(response, list):
21            raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}: {response!r}")
22        app_status = AppInitStatus.from_dict(response[0])
23        return DeviceFeatures.from_feature_flags(
24            new_feature_info=app_status.new_feature_info,
25            new_feature_info_str=app_status.new_feature_info_str,
26            feature_info=app_status.feature_info,
27            product_nickname=self._product.product_nickname,
28        )
29
30
31class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
32    """Trait for managing supported features on Roborock devices."""
33
34    command = RoborockCommand.APP_GET_INIT_STATUS
35    converter: DeviceTraitsConverter
36
37    def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:  # pylint: disable=super-init-not-called
38        """Initialize DeviceFeaturesTrait."""
39        common.V1TraitMixin.__init__(self)
40        self.converter = DeviceTraitsConverter(product)
41        self._product = product
42        self._device_cache = device_cache
43        # All fields of DeviceFeatures are required. Initialize them to False
44        # so we have some known state.
45        for field in fields(self):
46            setattr(self, field.name, False)
47
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
56    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
57        """Determines if the specified field is supported by this device.
58
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.
62        """
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
66            return True
67        return int(dps) in self._product.supported_schema_ids
68
69    async def refresh(self) -> None:
70        """Refresh the contents of this trait.
71
72        This will use cached device features if available since they do not
73        change often and this avoids unnecessary RPC calls. This would only
74        ever change with a firmware update, so caching is appropriate.
75        """
76        cache_data = await self._device_cache.get()
77        if cache_data.device_features is not None:
78            common.merge_trait_values(self, cache_data.device_features)
79            return
80        # Save cached device features
81        await super().refresh()
82        cache_data.device_features = self
83        await self._device_cache.set(cache_data)
class DeviceTraitsConverter(roborock.devices.traits.v1.common.V1TraitDataConverter):
12class DeviceTraitsConverter(common.V1TraitDataConverter):
13    """Converter for APP_GET_INIT_STATUS responses into DeviceFeatures."""
14
15    def __init__(self, product: HomeDataProduct) -> None:
16        """Initialize DeviceTraitsConverter."""
17        self._product = product
18
19    def convert(self, response: common.V1ResponseData) -> DeviceFeatures:
20        """Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance."""
21        if not isinstance(response, list):
22            raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}: {response!r}")
23        app_status = AppInitStatus.from_dict(response[0])
24        return DeviceFeatures.from_feature_flags(
25            new_feature_info=app_status.new_feature_info,
26            new_feature_info_str=app_status.new_feature_info_str,
27            feature_info=app_status.feature_info,
28            product_nickname=self._product.product_nickname,
29        )

Converter for APP_GET_INIT_STATUS responses into DeviceFeatures.

DeviceTraitsConverter(product: roborock.data.containers.HomeDataProduct)
15    def __init__(self, product: HomeDataProduct) -> None:
16        """Initialize DeviceTraitsConverter."""
17        self._product = product

Initialize DeviceTraitsConverter.

def convert( self, response: dict | list | int | str) -> roborock.device_features.DeviceFeatures:
19    def convert(self, response: common.V1ResponseData) -> DeviceFeatures:
20        """Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance."""
21        if not isinstance(response, list):
22            raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}: {response!r}")
23        app_status = AppInitStatus.from_dict(response[0])
24        return DeviceFeatures.from_feature_flags(
25            new_feature_info=app_status.new_feature_info,
26            new_feature_info_str=app_status.new_feature_info_str,
27            feature_info=app_status.feature_info,
28            product_nickname=self._product.product_nickname,
29        )

Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance.

class DeviceFeaturesTrait(roborock.device_features.DeviceFeatures, roborock.devices.traits.v1.common.V1TraitMixin):
32class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
33    """Trait for managing supported features on Roborock devices."""
34
35    command = RoborockCommand.APP_GET_INIT_STATUS
36    converter: DeviceTraitsConverter
37
38    def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:  # pylint: disable=super-init-not-called
39        """Initialize DeviceFeaturesTrait."""
40        common.V1TraitMixin.__init__(self)
41        self.converter = DeviceTraitsConverter(product)
42        self._product = product
43        self._device_cache = device_cache
44        # All fields of DeviceFeatures are required. Initialize them to False
45        # so we have some known state.
46        for field in fields(self):
47            setattr(self, field.name, False)
48
49    @staticmethod
50    def _get_dataclass_field(cls: type[RoborockBase], field_name: FieldNameBase) -> Field:
51        """Look up a dataclass field by its FieldNameBase name."""
52        for f in fields(cls):
53            if f.name == field_name:
54                return f
55        raise ValueError(f"Field {field_name!r} not found in {cls}")
56
57    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
58        """Determines if the specified field is supported by this device.
59
60        We use the `dps` dataclass field metadata to get the `RoborockDataProtocol`
61        integer ID and check it against the set of supported schema IDs for the
62        device returned in the product information.
63        """
64        dataclass_field = self._get_dataclass_field(cls, field_name)
65        if (dps := dataclass_field.metadata.get("dps")) is None:
66            # No DPS metadata — field is assumed always supported
67            return True
68        return int(dps) in self._product.supported_schema_ids
69
70    async def refresh(self) -> None:
71        """Refresh the contents of this trait.
72
73        This will use cached device features if available since they do not
74        change often and this avoids unnecessary RPC calls. This would only
75        ever change with a firmware update, so caching is appropriate.
76        """
77        cache_data = await self._device_cache.get()
78        if cache_data.device_features is not None:
79            common.merge_trait_values(self, cache_data.device_features)
80            return
81        # Save cached device features
82        await super().refresh()
83        cache_data.device_features = self
84        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)
38    def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None:  # pylint: disable=super-init-not-called
39        """Initialize DeviceFeaturesTrait."""
40        common.V1TraitMixin.__init__(self)
41        self.converter = DeviceTraitsConverter(product)
42        self._product = product
43        self._device_cache = device_cache
44        # All fields of DeviceFeatures are required. Initialize them to False
45        # so we have some known state.
46        for field in fields(self):
47            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).

def is_field_supported( self, cls: type[roborock.data.containers.RoborockBase], field_name: roborock.data.v1.v1_containers.FieldNameBase) -> bool:
57    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
58        """Determines if the specified field is supported by this device.
59
60        We use the `dps` dataclass field metadata to get the `RoborockDataProtocol`
61        integer ID and check it against the set of supported schema IDs for the
62        device returned in the product information.
63        """
64        dataclass_field = self._get_dataclass_field(cls, field_name)
65        if (dps := dataclass_field.metadata.get("dps")) is None:
66            # No DPS metadata — field is assumed always supported
67            return True
68        return int(dps) in self._product.supported_schema_ids

Determines if the specified field is supported by this device.

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.

async def refresh(self) -> None:
70    async def refresh(self) -> None:
71        """Refresh the contents of this trait.
72
73        This will use cached device features if available since they do not
74        change often and this avoids unnecessary RPC calls. This would only
75        ever change with a firmware update, so caching is appropriate.
76        """
77        cache_data = await self._device_cache.get()
78        if cache_data.device_features is not None:
79            common.merge_trait_values(self, cache_data.device_features)
80            return
81        # Save cached device features
82        await super().refresh()
83        cache_data.device_features = self
84        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.