roborock.devices.traits.v1

Create traits for V1 devices.

Traits are modular components that encapsulate specific features of a Roborock device. This module provides a factory function to create and initialize the appropriate traits for V1 devices based on their capabilities.

Using Traits

Traits are accessed via the v1_properties attribute on a device. Each trait represents a specific capability, such as status, consumables, or rooms.

Traits serve two main purposes:

  1. State: Traits are dataclasses that hold the current state of the device feature. You can access attributes directly (e.g., device.v1_properties.status.battery).
  2. Commands: Traits provide methods to control the device. For example, device.v1_properties.volume.set_volume().

Additionally, the command trait provides a generic way to send any command to the device (e.g. device.v1_properties.command.send("app_start")). This is often used for basic cleaning operations like starting, stopping, or docking the vacuum.

Most traits have a refresh() method that must be called to update their state from the device. The state is not updated automatically in real-time unless specifically implemented by the trait or via polling.

Adding New Traits

When adding a new trait, the most common pattern is to subclass V1TraitMixin and a RoborockBase dataclass. You must define a command class variable that specifies the RoborockCommand used to fetch the trait data from the device. See common.py for more details on common patterns used across traits.

There are some additional decorators in common.py that can be used to specify which RPC channel to use for the trait (standard, MQTT/cloud, or map-specific).

  • @common.mqtt_rpc_channel - Use the MQTT RPC channel for this trait.
  • @common.map_rpc_channel - Use the map RPC channel for this trait.

There are also some attributes that specify device feature dependencies for optional traits:

- `requires_feature` - The string name of the device feature that must be supported
    for this trait to be enabled. See `DeviceFeaturesTrait` for a list of
    available features.
- `requires_dock_type` - If set, this is a function that accepts a `RoborockDockTypeCode`
    and returns a boolean indicating whether the trait is supported for that dock type.

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.

  1"""Create traits for V1 devices.
  2
  3Traits are modular components that encapsulate specific features of a Roborock
  4device. This module provides a factory function to create and initialize the
  5appropriate traits for V1 devices based on their capabilities.
  6
  7Using Traits
  8------------
  9Traits are accessed via the `v1_properties` attribute on a device. Each trait
 10represents a specific capability, such as `status`, `consumables`, or `rooms`.
 11
 12Traits serve two main purposes:
 131.  **State**: Traits are dataclasses that hold the current state of the device
 14    feature. You can access attributes directly (e.g., `device.v1_properties.status.battery`).
 152.  **Commands**: Traits provide methods to control the device. For example,
 16    `device.v1_properties.volume.set_volume()`.
 17
 18Additionally, the `command` trait provides a generic way to send any command to the
 19device (e.g. `device.v1_properties.command.send("app_start")`). This is often used
 20for basic cleaning operations like starting, stopping, or docking the vacuum.
 21
 22Most traits have a `refresh()` method that must be called to update their state
 23from the device. The state is not updated automatically in real-time unless
 24specifically implemented by the trait or via polling.
 25
 26Adding New Traits
 27-----------------
 28When adding a new trait, the most common pattern is to subclass `V1TraitMixin`
 29and a `RoborockBase` dataclass. You must define a `command` class variable that
 30specifies the `RoborockCommand` used to fetch the trait data from the device.
 31See `common.py` for more details on common patterns used across traits.
 32
 33There are some additional decorators in `common.py` that can be used to specify which
 34RPC channel to use for the trait (standard, MQTT/cloud, or map-specific).
 35
 36  - `@common.mqtt_rpc_channel` - Use the MQTT RPC channel for this trait.
 37  - `@common.map_rpc_channel` - Use the map RPC channel for this trait.
 38
 39There are also some attributes that specify device feature dependencies for
 40optional traits:
 41
 42    - `requires_feature` - The string name of the device feature that must be supported
 43        for this trait to be enabled. See `DeviceFeaturesTrait` for a list of
 44        available features.
 45    - `requires_dock_type` - If set, this is a function that accepts a `RoborockDockTypeCode`
 46        and returns a boolean indicating whether the trait is supported for that dock type.
 47
 48Additionally, DeviceFeaturesTrait has a method `is_field_supported` that is used to
 49check individual trait field values. This is a more fine grained version to allow
 50optional fields in a dataclass, vs the above feature checks that apply to an entire
 51trait. The `requires_schema_code` field metadata attribute is a string of the schema
 52code in HomeDataProduct Schema that is required for the field to be supported.
 53"""
 54
 55import logging
 56from dataclasses import dataclass, field, fields
 57from functools import cache
 58from typing import Any, get_args
 59
 60from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase
 61from roborock.data.v1.v1_code_mappings import RoborockDockTypeCode
 62from roborock.devices.cache import DeviceCache
 63from roborock.devices.traits import Trait
 64from roborock.map.map_parser import MapParserConfig
 65from roborock.protocols.v1_protocol import V1RpcChannel
 66from roborock.web_api import UserWebApiClient
 67
 68from . import (
 69    child_lock,
 70    clean_summary,
 71    command,
 72    common,
 73    consumeable,
 74    device_features,
 75    do_not_disturb,
 76    dust_collection_mode,
 77    flow_led_status,
 78    home,
 79    led_status,
 80    map_content,
 81    maps,
 82    network_info,
 83    rooms,
 84    routines,
 85    smart_wash_params,
 86    status,
 87    valley_electricity_timer,
 88    volume,
 89    wash_towel_mode,
 90)
 91from .child_lock import ChildLockTrait
 92from .clean_summary import CleanSummaryTrait
 93from .command import CommandTrait
 94from .common import V1TraitMixin
 95from .consumeable import ConsumableTrait
 96from .device_features import DeviceFeaturesTrait
 97from .do_not_disturb import DoNotDisturbTrait
 98from .dust_collection_mode import DustCollectionModeTrait
 99from .flow_led_status import FlowLedStatusTrait
100from .home import HomeTrait
101from .led_status import LedStatusTrait
102from .map_content import MapContentTrait
103from .maps import MapsTrait
104from .network_info import NetworkInfoTrait
105from .rooms import RoomsTrait
106from .routines import RoutinesTrait
107from .smart_wash_params import SmartWashParamsTrait
108from .status import StatusTrait
109from .valley_electricity_timer import ValleyElectricityTimerTrait
110from .volume import SoundVolumeTrait
111from .wash_towel_mode import WashTowelModeTrait
112
113_LOGGER = logging.getLogger(__name__)
114
115__all__ = [
116    "PropertiesApi",
117    "child_lock",
118    "clean_summary",
119    "command",
120    "common",
121    "consumeable",
122    "device_features",
123    "do_not_disturb",
124    "dust_collection_mode",
125    "flow_led_status",
126    "home",
127    "led_status",
128    "map_content",
129    "maps",
130    "network_info",
131    "rooms",
132    "routines",
133    "smart_wash_params",
134    "status",
135    "valley_electricity_timer",
136    "volume",
137    "wash_towel_mode",
138]
139
140
141@dataclass
142class PropertiesApi(Trait):
143    """Common properties for V1 devices.
144
145    This class holds all the traits that are common across all V1 devices.
146    """
147
148    # All v1 devices have these traits
149    status: StatusTrait
150    command: CommandTrait
151    dnd: DoNotDisturbTrait
152    clean_summary: CleanSummaryTrait
153    sound_volume: SoundVolumeTrait
154    rooms: RoomsTrait
155    maps: MapsTrait
156    map_content: MapContentTrait
157    consumables: ConsumableTrait
158    home: HomeTrait
159    device_features: DeviceFeaturesTrait
160    network_info: NetworkInfoTrait
161    routines: RoutinesTrait
162
163    # Optional features that may not be supported on all devices
164    child_lock: ChildLockTrait | None = None
165    led_status: LedStatusTrait | None = None
166    flow_led_status: FlowLedStatusTrait | None = None
167    valley_electricity_timer: ValleyElectricityTimerTrait | None = None
168    dust_collection_mode: DustCollectionModeTrait | None = None
169    wash_towel_mode: WashTowelModeTrait | None = None
170    smart_wash_params: SmartWashParamsTrait | None = None
171
172    def __init__(
173        self,
174        device_uid: str,
175        product: HomeDataProduct,
176        home_data: HomeData,
177        rpc_channel: V1RpcChannel,
178        mqtt_rpc_channel: V1RpcChannel,
179        map_rpc_channel: V1RpcChannel,
180        web_api: UserWebApiClient,
181        device_cache: DeviceCache,
182        map_parser_config: MapParserConfig | None = None,
183    ) -> None:
184        """Initialize the V1TraitProps."""
185        self._device_uid = device_uid
186        self._rpc_channel = rpc_channel
187        self._mqtt_rpc_channel = mqtt_rpc_channel
188        self._map_rpc_channel = map_rpc_channel
189        self._web_api = web_api
190        self._device_cache = device_cache
191
192        self.status = StatusTrait(product)
193        self.consumables = ConsumableTrait()
194        self.rooms = RoomsTrait(home_data)
195        self.maps = MapsTrait(self.status)
196        self.map_content = MapContentTrait(map_parser_config)
197        self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
198        self.device_features = DeviceFeaturesTrait(product, self._device_cache)
199        self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
200        self.routines = RoutinesTrait(device_uid, web_api)
201
202        # Dynamically create any traits that need to be populated
203        for item in fields(self):
204            if (trait := getattr(self, item.name, None)) is None:
205                # We exclude optional features and them via discover_features
206                if (union_args := get_args(item.type)) is None or len(union_args) > 0:
207                    continue
208                _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
209                trait = item.type()
210                setattr(self, item.name, trait)
211            # This is a hack to allow setting the rpc_channel on all traits. This is
212            # used so we can preserve the dataclass behavior when the values in the
213            # traits are updated, but still want to allow them to have a reference
214            # to the rpc channel for sending commands.
215            trait._rpc_channel = self._get_rpc_channel(trait)
216
217    def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel:
218        # The decorator `@common.mqtt_rpc_channel` means that the trait needs
219        # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
220        if hasattr(trait, "mqtt_rpc_channel"):
221            return self._mqtt_rpc_channel
222        elif hasattr(trait, "map_rpc_channel"):
223            return self._map_rpc_channel
224        else:
225            return self._rpc_channel
226
227    async def discover_features(self) -> None:
228        """Populate any supported traits that were not initialized in __init__."""
229        _LOGGER.debug("Starting optional trait discovery")
230        await self.device_features.refresh()
231        # Dock type also acts like a device feature for some traits.
232        dock_type = await self._dock_type()
233
234        # Dynamically create any traits that need to be populated
235        for item in fields(self):
236            if (trait := getattr(self, item.name, None)) is not None:
237                continue
238            if (union_args := get_args(item.type)) is None:
239                raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
240            if len(union_args) != 2 or type(None) not in union_args:
241                raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
242
243            # Union args may not be in declared order
244            item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
245            if not self._is_supported(item_type, item.name, dock_type):
246                _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
247                continue
248            _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
249            trait = item_type()
250            setattr(self, item.name, trait)
251            trait._rpc_channel = self._get_rpc_channel(trait)
252
253    def _is_supported(self, trait_type: type[V1TraitMixin], name: str, dock_type: RoborockDockTypeCode) -> bool:
254        """Check if a trait is supported by the device."""
255
256        if (requires_dock_type := getattr(trait_type, "requires_dock_type", None)) is not None:
257            return requires_dock_type(dock_type)
258
259        if (feature_name := getattr(trait_type, "requires_feature", None)) is None:
260            _LOGGER.debug("Optional trait missing 'requires_feature' attribute %s, skipping", name)
261            return False
262        if (is_supported := getattr(self.device_features, feature_name)) is None:
263            raise ValueError(f"Device feature '{feature_name}' on trait '{name}' is unknown")
264        return is_supported
265
266    async def _dock_type(self) -> RoborockDockTypeCode:
267        """Get the dock type from the status trait or cache."""
268        dock_type = await self._get_cached_trait_data("dock_type")
269        if dock_type is not None:
270            _LOGGER.debug("Using cached dock type: %s", dock_type)
271            try:
272                return RoborockDockTypeCode(dock_type)
273            except ValueError:
274                _LOGGER.debug("Cached dock type %s is invalid, refreshing", dock_type)
275
276        _LOGGER.debug("Starting dock type discovery")
277        await self.status.refresh()
278        _LOGGER.debug("Fetched dock type: %s", self.status.dock_type)
279        if self.status.dock_type is None:
280            # Explicitly set so we reuse cached value next type
281            dock_type = RoborockDockTypeCode.no_dock
282        else:
283            dock_type = self.status.dock_type
284        await self._set_cached_trait_data("dock_type", dock_type)
285        return dock_type
286
287    async def _get_cached_trait_data(self, name: str) -> Any:
288        """Get the dock type from the status trait or cache."""
289        cache_data = await self._device_cache.get()
290        if cache_data.trait_data is None:
291            cache_data.trait_data = {}
292        _LOGGER.debug("Cached trait data: %s", cache_data.trait_data)
293        return cache_data.trait_data.get(name)
294
295    async def _set_cached_trait_data(self, name: str, value: Any) -> None:
296        """Set trait-specific cached data."""
297        cache_data = await self._device_cache.get()
298        if cache_data.trait_data is None:
299            cache_data.trait_data = {}
300        cache_data.trait_data[name] = value
301        _LOGGER.debug("Updating cached trait data: %s", cache_data.trait_data)
302        await self._device_cache.set(cache_data)
303
304    def as_dict(self) -> dict[str, Any]:
305        """Return the trait data as a dictionary."""
306        result: dict[str, Any] = {}
307        for item in fields(self):
308            trait = getattr(self, item.name, None)
309            if trait is None or not isinstance(trait, RoborockBase):
310                continue
311            data = trait.as_dict()
312            if data:  # Don't omit unset traits
313                result[item.name] = data
314        return result
315
316
317def create(
318    device_uid: str,
319    product: HomeDataProduct,
320    home_data: HomeData,
321    rpc_channel: V1RpcChannel,
322    mqtt_rpc_channel: V1RpcChannel,
323    map_rpc_channel: V1RpcChannel,
324    web_api: UserWebApiClient,
325    device_cache: DeviceCache,
326    map_parser_config: MapParserConfig | None = None,
327) -> PropertiesApi:
328    """Create traits for V1 devices."""
329    return PropertiesApi(
330        device_uid,
331        product,
332        home_data,
333        rpc_channel,
334        mqtt_rpc_channel,
335        map_rpc_channel,
336        web_api,
337        device_cache,
338        map_parser_config,
339    )
@dataclass
class PropertiesApi(roborock.devices.traits.Trait):
142@dataclass
143class PropertiesApi(Trait):
144    """Common properties for V1 devices.
145
146    This class holds all the traits that are common across all V1 devices.
147    """
148
149    # All v1 devices have these traits
150    status: StatusTrait
151    command: CommandTrait
152    dnd: DoNotDisturbTrait
153    clean_summary: CleanSummaryTrait
154    sound_volume: SoundVolumeTrait
155    rooms: RoomsTrait
156    maps: MapsTrait
157    map_content: MapContentTrait
158    consumables: ConsumableTrait
159    home: HomeTrait
160    device_features: DeviceFeaturesTrait
161    network_info: NetworkInfoTrait
162    routines: RoutinesTrait
163
164    # Optional features that may not be supported on all devices
165    child_lock: ChildLockTrait | None = None
166    led_status: LedStatusTrait | None = None
167    flow_led_status: FlowLedStatusTrait | None = None
168    valley_electricity_timer: ValleyElectricityTimerTrait | None = None
169    dust_collection_mode: DustCollectionModeTrait | None = None
170    wash_towel_mode: WashTowelModeTrait | None = None
171    smart_wash_params: SmartWashParamsTrait | None = None
172
173    def __init__(
174        self,
175        device_uid: str,
176        product: HomeDataProduct,
177        home_data: HomeData,
178        rpc_channel: V1RpcChannel,
179        mqtt_rpc_channel: V1RpcChannel,
180        map_rpc_channel: V1RpcChannel,
181        web_api: UserWebApiClient,
182        device_cache: DeviceCache,
183        map_parser_config: MapParserConfig | None = None,
184    ) -> None:
185        """Initialize the V1TraitProps."""
186        self._device_uid = device_uid
187        self._rpc_channel = rpc_channel
188        self._mqtt_rpc_channel = mqtt_rpc_channel
189        self._map_rpc_channel = map_rpc_channel
190        self._web_api = web_api
191        self._device_cache = device_cache
192
193        self.status = StatusTrait(product)
194        self.consumables = ConsumableTrait()
195        self.rooms = RoomsTrait(home_data)
196        self.maps = MapsTrait(self.status)
197        self.map_content = MapContentTrait(map_parser_config)
198        self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
199        self.device_features = DeviceFeaturesTrait(product, self._device_cache)
200        self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
201        self.routines = RoutinesTrait(device_uid, web_api)
202
203        # Dynamically create any traits that need to be populated
204        for item in fields(self):
205            if (trait := getattr(self, item.name, None)) is None:
206                # We exclude optional features and them via discover_features
207                if (union_args := get_args(item.type)) is None or len(union_args) > 0:
208                    continue
209                _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
210                trait = item.type()
211                setattr(self, item.name, trait)
212            # This is a hack to allow setting the rpc_channel on all traits. This is
213            # used so we can preserve the dataclass behavior when the values in the
214            # traits are updated, but still want to allow them to have a reference
215            # to the rpc channel for sending commands.
216            trait._rpc_channel = self._get_rpc_channel(trait)
217
218    def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel:
219        # The decorator `@common.mqtt_rpc_channel` means that the trait needs
220        # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
221        if hasattr(trait, "mqtt_rpc_channel"):
222            return self._mqtt_rpc_channel
223        elif hasattr(trait, "map_rpc_channel"):
224            return self._map_rpc_channel
225        else:
226            return self._rpc_channel
227
228    async def discover_features(self) -> None:
229        """Populate any supported traits that were not initialized in __init__."""
230        _LOGGER.debug("Starting optional trait discovery")
231        await self.device_features.refresh()
232        # Dock type also acts like a device feature for some traits.
233        dock_type = await self._dock_type()
234
235        # Dynamically create any traits that need to be populated
236        for item in fields(self):
237            if (trait := getattr(self, item.name, None)) is not None:
238                continue
239            if (union_args := get_args(item.type)) is None:
240                raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
241            if len(union_args) != 2 or type(None) not in union_args:
242                raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
243
244            # Union args may not be in declared order
245            item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
246            if not self._is_supported(item_type, item.name, dock_type):
247                _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
248                continue
249            _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
250            trait = item_type()
251            setattr(self, item.name, trait)
252            trait._rpc_channel = self._get_rpc_channel(trait)
253
254    def _is_supported(self, trait_type: type[V1TraitMixin], name: str, dock_type: RoborockDockTypeCode) -> bool:
255        """Check if a trait is supported by the device."""
256
257        if (requires_dock_type := getattr(trait_type, "requires_dock_type", None)) is not None:
258            return requires_dock_type(dock_type)
259
260        if (feature_name := getattr(trait_type, "requires_feature", None)) is None:
261            _LOGGER.debug("Optional trait missing 'requires_feature' attribute %s, skipping", name)
262            return False
263        if (is_supported := getattr(self.device_features, feature_name)) is None:
264            raise ValueError(f"Device feature '{feature_name}' on trait '{name}' is unknown")
265        return is_supported
266
267    async def _dock_type(self) -> RoborockDockTypeCode:
268        """Get the dock type from the status trait or cache."""
269        dock_type = await self._get_cached_trait_data("dock_type")
270        if dock_type is not None:
271            _LOGGER.debug("Using cached dock type: %s", dock_type)
272            try:
273                return RoborockDockTypeCode(dock_type)
274            except ValueError:
275                _LOGGER.debug("Cached dock type %s is invalid, refreshing", dock_type)
276
277        _LOGGER.debug("Starting dock type discovery")
278        await self.status.refresh()
279        _LOGGER.debug("Fetched dock type: %s", self.status.dock_type)
280        if self.status.dock_type is None:
281            # Explicitly set so we reuse cached value next type
282            dock_type = RoborockDockTypeCode.no_dock
283        else:
284            dock_type = self.status.dock_type
285        await self._set_cached_trait_data("dock_type", dock_type)
286        return dock_type
287
288    async def _get_cached_trait_data(self, name: str) -> Any:
289        """Get the dock type from the status trait or cache."""
290        cache_data = await self._device_cache.get()
291        if cache_data.trait_data is None:
292            cache_data.trait_data = {}
293        _LOGGER.debug("Cached trait data: %s", cache_data.trait_data)
294        return cache_data.trait_data.get(name)
295
296    async def _set_cached_trait_data(self, name: str, value: Any) -> None:
297        """Set trait-specific cached data."""
298        cache_data = await self._device_cache.get()
299        if cache_data.trait_data is None:
300            cache_data.trait_data = {}
301        cache_data.trait_data[name] = value
302        _LOGGER.debug("Updating cached trait data: %s", cache_data.trait_data)
303        await self._device_cache.set(cache_data)
304
305    def as_dict(self) -> dict[str, Any]:
306        """Return the trait data as a dictionary."""
307        result: dict[str, Any] = {}
308        for item in fields(self):
309            trait = getattr(self, item.name, None)
310            if trait is None or not isinstance(trait, RoborockBase):
311                continue
312            data = trait.as_dict()
313            if data:  # Don't omit unset traits
314                result[item.name] = data
315        return result

Common properties for V1 devices.

This class holds all the traits that are common across all V1 devices.

PropertiesApi( device_uid: str, product: roborock.data.containers.HomeDataProduct, home_data: roborock.data.containers.HomeData, rpc_channel: roborock.protocols.v1_protocol.V1RpcChannel, mqtt_rpc_channel: roborock.protocols.v1_protocol.V1RpcChannel, map_rpc_channel: roborock.protocols.v1_protocol.V1RpcChannel, web_api: roborock.web_api.UserWebApiClient, device_cache: roborock.devices.cache.DeviceCache, map_parser_config: roborock.map.MapParserConfig | None = None)
173    def __init__(
174        self,
175        device_uid: str,
176        product: HomeDataProduct,
177        home_data: HomeData,
178        rpc_channel: V1RpcChannel,
179        mqtt_rpc_channel: V1RpcChannel,
180        map_rpc_channel: V1RpcChannel,
181        web_api: UserWebApiClient,
182        device_cache: DeviceCache,
183        map_parser_config: MapParserConfig | None = None,
184    ) -> None:
185        """Initialize the V1TraitProps."""
186        self._device_uid = device_uid
187        self._rpc_channel = rpc_channel
188        self._mqtt_rpc_channel = mqtt_rpc_channel
189        self._map_rpc_channel = map_rpc_channel
190        self._web_api = web_api
191        self._device_cache = device_cache
192
193        self.status = StatusTrait(product)
194        self.consumables = ConsumableTrait()
195        self.rooms = RoomsTrait(home_data)
196        self.maps = MapsTrait(self.status)
197        self.map_content = MapContentTrait(map_parser_config)
198        self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
199        self.device_features = DeviceFeaturesTrait(product, self._device_cache)
200        self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
201        self.routines = RoutinesTrait(device_uid, web_api)
202
203        # Dynamically create any traits that need to be populated
204        for item in fields(self):
205            if (trait := getattr(self, item.name, None)) is None:
206                # We exclude optional features and them via discover_features
207                if (union_args := get_args(item.type)) is None or len(union_args) > 0:
208                    continue
209                _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
210                trait = item.type()
211                setattr(self, item.name, trait)
212            # This is a hack to allow setting the rpc_channel on all traits. This is
213            # used so we can preserve the dataclass behavior when the values in the
214            # traits are updated, but still want to allow them to have a reference
215            # to the rpc channel for sending commands.
216            trait._rpc_channel = self._get_rpc_channel(trait)

Initialize the V1TraitProps.

maps: <function mqtt_rpc_channel.<locals>.wrapper at 0x7f1e71ea9080>
map_content: <function map_rpc_channel.<locals>.wrapper at 0x7f1e71e934c0>
async def discover_features(self) -> None:
228    async def discover_features(self) -> None:
229        """Populate any supported traits that were not initialized in __init__."""
230        _LOGGER.debug("Starting optional trait discovery")
231        await self.device_features.refresh()
232        # Dock type also acts like a device feature for some traits.
233        dock_type = await self._dock_type()
234
235        # Dynamically create any traits that need to be populated
236        for item in fields(self):
237            if (trait := getattr(self, item.name, None)) is not None:
238                continue
239            if (union_args := get_args(item.type)) is None:
240                raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
241            if len(union_args) != 2 or type(None) not in union_args:
242                raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
243
244            # Union args may not be in declared order
245            item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
246            if not self._is_supported(item_type, item.name, dock_type):
247                _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
248                continue
249            _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
250            trait = item_type()
251            setattr(self, item.name, trait)
252            trait._rpc_channel = self._get_rpc_channel(trait)

Populate any supported traits that were not initialized in __init__.

def as_dict(self) -> dict[str, typing.Any]:
305    def as_dict(self) -> dict[str, Any]:
306        """Return the trait data as a dictionary."""
307        result: dict[str, Any] = {}
308        for item in fields(self):
309            trait = getattr(self, item.name, None)
310            if trait is None or not isinstance(trait, RoborockBase):
311                continue
312            data = trait.as_dict()
313            if data:  # Don't omit unset traits
314                result[item.name] = data
315        return result

Return the trait data as a dictionary.