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    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
49        """Determines if the specified field is supported by this device.
50
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.
54        """
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
66            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
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)
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    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
50        """Determines if the specified field is supported by this device.
51
52        We use dataclass attributes on the field to specify the schema code that is required
53        for the field to be supported and it is compared against the list of
54        supported schema codes for the device returned in the product information.
55        """
56        dataclass_field: Field | None = None
57        for field in fields(cls):
58            if field.name == field_name:
59                dataclass_field = field
60                break
61        if dataclass_field is None:
62            raise ValueError(f"Field {field_name} not found in {cls}")
63
64        requires_schema_code = dataclass_field.metadata.get("requires_schema_code", None)
65        if requires_schema_code is None:
66            # We assume the field is supported
67            return True
68        # If the field requires a protocol that is not supported, we return False
69        return requires_schema_code in self._product.supported_schema_codes
70
71    async def refresh(self) -> None:
72        """Refresh the contents of this trait.
73
74        This will use cached device features if available since they do not
75        change often and this avoids unnecessary RPC calls. This would only
76        ever change with a firmware update, so caching is appropriate.
77        """
78        cache_data = await self._device_cache.get()
79        if cache_data.device_features is not None:
80            common.merge_trait_values(self, cache_data.device_features)
81            return
82        # Save cached device features
83        await super().refresh()
84        cache_data.device_features = self
85        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:
49    def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
50        """Determines if the specified field is supported by this device.
51
52        We use dataclass attributes on the field to specify the schema code that is required
53        for the field to be supported and it is compared against the list of
54        supported schema codes for the device returned in the product information.
55        """
56        dataclass_field: Field | None = None
57        for field in fields(cls):
58            if field.name == field_name:
59                dataclass_field = field
60                break
61        if dataclass_field is None:
62            raise ValueError(f"Field {field_name} not found in {cls}")
63
64        requires_schema_code = dataclass_field.metadata.get("requires_schema_code", None)
65        if requires_schema_code is None:
66            # We assume the field is supported
67            return True
68        # If the field requires a protocol that is not supported, we return False
69        return requires_schema_code in self._product.supported_schema_codes

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.

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