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 typing import Any, get_args
 58
 59from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase
 60from roborock.data.v1.v1_code_mappings import RoborockDockTypeCode
 61from roborock.devices.cache import DeviceCache
 62from roborock.devices.traits import Trait
 63from roborock.map.map_parser import MapParserConfig
 64from roborock.protocols.v1_protocol import V1RpcChannel
 65from roborock.web_api import UserWebApiClient
 66
 67from . import (
 68    child_lock,
 69    clean_summary,
 70    command,
 71    common,
 72    consumeable,
 73    device_features,
 74    do_not_disturb,
 75    dust_collection_mode,
 76    flow_led_status,
 77    home,
 78    led_status,
 79    map_content,
 80    maps,
 81    network_info,
 82    rooms,
 83    routines,
 84    smart_wash_params,
 85    status,
 86    valley_electricity_timer,
 87    volume,
 88    wash_towel_mode,
 89)
 90from .child_lock import ChildLockTrait
 91from .clean_summary import CleanSummaryTrait
 92from .command import CommandTrait
 93from .common import V1TraitMixin
 94from .consumeable import ConsumableTrait
 95from .device_features import DeviceFeaturesTrait
 96from .do_not_disturb import DoNotDisturbTrait
 97from .dust_collection_mode import DustCollectionModeTrait
 98from .flow_led_status import FlowLedStatusTrait
 99from .home import HomeTrait
100from .led_status import LedStatusTrait
101from .map_content import MapContentTrait
102from .maps import MapsTrait
103from .network_info import NetworkInfoTrait
104from .rooms import RoomsTrait
105from .routines import RoutinesTrait
106from .smart_wash_params import SmartWashParamsTrait
107from .status import StatusTrait
108from .valley_electricity_timer import ValleyElectricityTimerTrait
109from .volume import SoundVolumeTrait
110from .wash_towel_mode import WashTowelModeTrait
111
112_LOGGER = logging.getLogger(__name__)
113
114__all__ = [
115    "PropertiesApi",
116    "child_lock",
117    "clean_summary",
118    "command",
119    "common",
120    "consumeable",
121    "device_features",
122    "do_not_disturb",
123    "dust_collection_mode",
124    "flow_led_status",
125    "home",
126    "led_status",
127    "map_content",
128    "maps",
129    "network_info",
130    "rooms",
131    "routines",
132    "smart_wash_params",
133    "status",
134    "valley_electricity_timer",
135    "volume",
136    "wash_towel_mode",
137]
138
139
140@dataclass
141class PropertiesApi(Trait):
142    """Common properties for V1 devices.
143
144    This class holds all the traits that are common across all V1 devices.
145    """
146
147    # All v1 devices have these traits
148    status: StatusTrait
149    command: CommandTrait
150    dnd: DoNotDisturbTrait
151    clean_summary: CleanSummaryTrait
152    sound_volume: SoundVolumeTrait
153    rooms: RoomsTrait
154    maps: MapsTrait
155    map_content: MapContentTrait
156    consumables: ConsumableTrait
157    home: HomeTrait
158    device_features: DeviceFeaturesTrait
159    network_info: NetworkInfoTrait
160    routines: RoutinesTrait
161
162    # Optional features that may not be supported on all devices
163    child_lock: ChildLockTrait | None = None
164    led_status: LedStatusTrait | None = None
165    flow_led_status: FlowLedStatusTrait | None = None
166    valley_electricity_timer: ValleyElectricityTimerTrait | None = None
167    dust_collection_mode: DustCollectionModeTrait | None = None
168    wash_towel_mode: WashTowelModeTrait | None = None
169    smart_wash_params: SmartWashParamsTrait | None = None
170
171    def __init__(
172        self,
173        device_uid: str,
174        product: HomeDataProduct,
175        home_data: HomeData,
176        rpc_channel: V1RpcChannel,
177        mqtt_rpc_channel: V1RpcChannel,
178        map_rpc_channel: V1RpcChannel,
179        web_api: UserWebApiClient,
180        device_cache: DeviceCache,
181        map_parser_config: MapParserConfig | None = None,
182        region: str | 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        self._region = region
192
193        self.device_features = DeviceFeaturesTrait(product, self._device_cache)
194        self.status = StatusTrait(self.device_features, region=self._region)
195        self.consumables = ConsumableTrait()
196        self.rooms = RoomsTrait(home_data, web_api)
197        self.maps = MapsTrait(self.status)
198        self.map_content = MapContentTrait(map_parser_config)
199        self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, 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                if not callable(item.type):
211                    continue
212                trait = item.type()
213                setattr(self, item.name, trait)
214            # This is a hack to allow setting the rpc_channel on all traits. This is
215            # used so we can preserve the dataclass behavior when the values in the
216            # traits are updated, but still want to allow them to have a reference
217            # to the rpc channel for sending commands.
218            trait._rpc_channel = self._get_rpc_channel(trait)
219
220    def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel:
221        # The decorator `@common.mqtt_rpc_channel` means that the trait needs
222        # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
223        if hasattr(trait, "mqtt_rpc_channel"):
224            return self._mqtt_rpc_channel
225        elif hasattr(trait, "map_rpc_channel"):
226            return self._map_rpc_channel
227        else:
228            return self._rpc_channel
229
230    async def discover_features(self) -> None:
231        """Populate any supported traits that were not initialized in __init__."""
232        _LOGGER.debug("Starting optional trait discovery")
233        await self.device_features.refresh()
234        # Dock type also acts like a device feature for some traits.
235        dock_type = await self._dock_type()
236
237        # Initialize traits with special arguments before the generic loop
238        if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_type):
239            wash_towel_mode = WashTowelModeTrait(self.device_features)
240            wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode)  # type: ignore[assignment]
241            self.wash_towel_mode = wash_towel_mode
242
243        # Dynamically create any traits that need to be populated
244        for item in fields(self):
245            if (trait := getattr(self, item.name, None)) is not None:
246                continue
247            if (union_args := get_args(item.type)) is None:
248                raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
249            if len(union_args) != 2 or type(None) not in union_args:
250                raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
251
252            # Union args may not be in declared order
253            item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
254            if not self._is_supported(item_type, item.name, dock_type):
255                _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
256                continue
257            _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
258            trait = item_type()
259            setattr(self, item.name, trait)
260            trait._rpc_channel = self._get_rpc_channel(trait)
261
262    def _is_supported(self, trait_type: type[V1TraitMixin], name: str, dock_type: RoborockDockTypeCode) -> bool:
263        """Check if a trait is supported by the device."""
264
265        if (requires_dock_type := getattr(trait_type, "requires_dock_type", None)) is not None:
266            return requires_dock_type(dock_type)
267
268        if (feature_name := getattr(trait_type, "requires_feature", None)) is None:
269            _LOGGER.debug("Optional trait missing 'requires_feature' attribute %s, skipping", name)
270            return False
271        if (is_supported := getattr(self.device_features, feature_name)) is None:
272            raise ValueError(f"Device feature '{feature_name}' on trait '{name}' is unknown")
273        return is_supported
274
275    async def _dock_type(self) -> RoborockDockTypeCode:
276        """Get the dock type from the status trait or cache."""
277        dock_type = await self._get_cached_trait_data("dock_type")
278        if dock_type is not None:
279            _LOGGER.debug("Using cached dock type: %s", dock_type)
280            try:
281                return RoborockDockTypeCode(dock_type)
282            except ValueError:
283                _LOGGER.debug("Cached dock type %s is invalid, refreshing", dock_type)
284
285        _LOGGER.debug("Starting dock type discovery")
286        await self.status.refresh()
287        _LOGGER.debug("Fetched dock type: %s", self.status.dock_type)
288        if self.status.dock_type is None:
289            # Explicitly set so we reuse cached value next type
290            dock_type = RoborockDockTypeCode.no_dock
291        else:
292            dock_type = self.status.dock_type
293        await self._set_cached_trait_data("dock_type", dock_type)
294        return dock_type
295
296    async def _get_cached_trait_data(self, name: str) -> Any:
297        """Get the dock type from the status trait or cache."""
298        cache_data = await self._device_cache.get()
299        if cache_data.trait_data is None:
300            cache_data.trait_data = {}
301        _LOGGER.debug("Cached trait data: %s", cache_data.trait_data)
302        return cache_data.trait_data.get(name)
303
304    async def _set_cached_trait_data(self, name: str, value: Any) -> None:
305        """Set trait-specific cached data."""
306        cache_data = await self._device_cache.get()
307        if cache_data.trait_data is None:
308            cache_data.trait_data = {}
309        cache_data.trait_data[name] = value
310        _LOGGER.debug("Updating cached trait data: %s", cache_data.trait_data)
311        await self._device_cache.set(cache_data)
312
313    def as_dict(self) -> dict[str, Any]:
314        """Return the trait data as a dictionary."""
315        result: dict[str, Any] = {}
316        for item in fields(self):
317            trait = getattr(self, item.name, None)
318            if trait is None or not isinstance(trait, RoborockBase):
319                continue
320            data = trait.as_dict()
321            if data:  # Don't omit unset traits
322                result[item.name] = data
323        return result
324
325
326def create(
327    device_uid: str,
328    product: HomeDataProduct,
329    home_data: HomeData,
330    rpc_channel: V1RpcChannel,
331    mqtt_rpc_channel: V1RpcChannel,
332    map_rpc_channel: V1RpcChannel,
333    web_api: UserWebApiClient,
334    device_cache: DeviceCache,
335    map_parser_config: MapParserConfig | None = None,
336    region: str | None = None,
337) -> PropertiesApi:
338    """Create traits for V1 devices."""
339    return PropertiesApi(
340        device_uid,
341        product,
342        home_data,
343        rpc_channel,
344        mqtt_rpc_channel,
345        map_rpc_channel,
346        web_api,
347        device_cache,
348        map_parser_config,
349        region=region,
350    )
@dataclass
class PropertiesApi(roborock.devices.traits.Trait):
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        region: str | 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        self._region = region
193
194        self.device_features = DeviceFeaturesTrait(product, self._device_cache)
195        self.status = StatusTrait(self.device_features, region=self._region)
196        self.consumables = ConsumableTrait()
197        self.rooms = RoomsTrait(home_data, web_api)
198        self.maps = MapsTrait(self.status)
199        self.map_content = MapContentTrait(map_parser_config)
200        self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
201        self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
202        self.routines = RoutinesTrait(device_uid, web_api)
203
204        # Dynamically create any traits that need to be populated
205        for item in fields(self):
206            if (trait := getattr(self, item.name, None)) is None:
207                # We exclude optional features and them via discover_features
208                if (union_args := get_args(item.type)) is None or len(union_args) > 0:
209                    continue
210                _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
211                if not callable(item.type):
212                    continue
213                trait = item.type()
214                setattr(self, item.name, trait)
215            # This is a hack to allow setting the rpc_channel on all traits. This is
216            # used so we can preserve the dataclass behavior when the values in the
217            # traits are updated, but still want to allow them to have a reference
218            # to the rpc channel for sending commands.
219            trait._rpc_channel = self._get_rpc_channel(trait)
220
221    def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel:
222        # The decorator `@common.mqtt_rpc_channel` means that the trait needs
223        # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
224        if hasattr(trait, "mqtt_rpc_channel"):
225            return self._mqtt_rpc_channel
226        elif hasattr(trait, "map_rpc_channel"):
227            return self._map_rpc_channel
228        else:
229            return self._rpc_channel
230
231    async def discover_features(self) -> None:
232        """Populate any supported traits that were not initialized in __init__."""
233        _LOGGER.debug("Starting optional trait discovery")
234        await self.device_features.refresh()
235        # Dock type also acts like a device feature for some traits.
236        dock_type = await self._dock_type()
237
238        # Initialize traits with special arguments before the generic loop
239        if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_type):
240            wash_towel_mode = WashTowelModeTrait(self.device_features)
241            wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode)  # type: ignore[assignment]
242            self.wash_towel_mode = wash_towel_mode
243
244        # Dynamically create any traits that need to be populated
245        for item in fields(self):
246            if (trait := getattr(self, item.name, None)) is not None:
247                continue
248            if (union_args := get_args(item.type)) is None:
249                raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
250            if len(union_args) != 2 or type(None) not in union_args:
251                raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
252
253            # Union args may not be in declared order
254            item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
255            if not self._is_supported(item_type, item.name, dock_type):
256                _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
257                continue
258            _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
259            trait = item_type()
260            setattr(self, item.name, trait)
261            trait._rpc_channel = self._get_rpc_channel(trait)
262
263    def _is_supported(self, trait_type: type[V1TraitMixin], name: str, dock_type: RoborockDockTypeCode) -> bool:
264        """Check if a trait is supported by the device."""
265
266        if (requires_dock_type := getattr(trait_type, "requires_dock_type", None)) is not None:
267            return requires_dock_type(dock_type)
268
269        if (feature_name := getattr(trait_type, "requires_feature", None)) is None:
270            _LOGGER.debug("Optional trait missing 'requires_feature' attribute %s, skipping", name)
271            return False
272        if (is_supported := getattr(self.device_features, feature_name)) is None:
273            raise ValueError(f"Device feature '{feature_name}' on trait '{name}' is unknown")
274        return is_supported
275
276    async def _dock_type(self) -> RoborockDockTypeCode:
277        """Get the dock type from the status trait or cache."""
278        dock_type = await self._get_cached_trait_data("dock_type")
279        if dock_type is not None:
280            _LOGGER.debug("Using cached dock type: %s", dock_type)
281            try:
282                return RoborockDockTypeCode(dock_type)
283            except ValueError:
284                _LOGGER.debug("Cached dock type %s is invalid, refreshing", dock_type)
285
286        _LOGGER.debug("Starting dock type discovery")
287        await self.status.refresh()
288        _LOGGER.debug("Fetched dock type: %s", self.status.dock_type)
289        if self.status.dock_type is None:
290            # Explicitly set so we reuse cached value next type
291            dock_type = RoborockDockTypeCode.no_dock
292        else:
293            dock_type = self.status.dock_type
294        await self._set_cached_trait_data("dock_type", dock_type)
295        return dock_type
296
297    async def _get_cached_trait_data(self, name: str) -> Any:
298        """Get the dock type from the status trait or cache."""
299        cache_data = await self._device_cache.get()
300        if cache_data.trait_data is None:
301            cache_data.trait_data = {}
302        _LOGGER.debug("Cached trait data: %s", cache_data.trait_data)
303        return cache_data.trait_data.get(name)
304
305    async def _set_cached_trait_data(self, name: str, value: Any) -> None:
306        """Set trait-specific cached data."""
307        cache_data = await self._device_cache.get()
308        if cache_data.trait_data is None:
309            cache_data.trait_data = {}
310        cache_data.trait_data[name] = value
311        _LOGGER.debug("Updating cached trait data: %s", cache_data.trait_data)
312        await self._device_cache.set(cache_data)
313
314    def as_dict(self) -> dict[str, Any]:
315        """Return the trait data as a dictionary."""
316        result: dict[str, Any] = {}
317        for item in fields(self):
318            trait = getattr(self, item.name, None)
319            if trait is None or not isinstance(trait, RoborockBase):
320                continue
321            data = trait.as_dict()
322            if data:  # Don't omit unset traits
323                result[item.name] = data
324        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, region: str | None = None)
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        region: str | 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        self._region = region
193
194        self.device_features = DeviceFeaturesTrait(product, self._device_cache)
195        self.status = StatusTrait(self.device_features, region=self._region)
196        self.consumables = ConsumableTrait()
197        self.rooms = RoomsTrait(home_data, web_api)
198        self.maps = MapsTrait(self.status)
199        self.map_content = MapContentTrait(map_parser_config)
200        self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
201        self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
202        self.routines = RoutinesTrait(device_uid, web_api)
203
204        # Dynamically create any traits that need to be populated
205        for item in fields(self):
206            if (trait := getattr(self, item.name, None)) is None:
207                # We exclude optional features and them via discover_features
208                if (union_args := get_args(item.type)) is None or len(union_args) > 0:
209                    continue
210                _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
211                if not callable(item.type):
212                    continue
213                trait = item.type()
214                setattr(self, item.name, trait)
215            # This is a hack to allow setting the rpc_channel on all traits. This is
216            # used so we can preserve the dataclass behavior when the values in the
217            # traits are updated, but still want to allow them to have a reference
218            # to the rpc channel for sending commands.
219            trait._rpc_channel = self._get_rpc_channel(trait)

Initialize the V1TraitProps.

maps: <function mqtt_rpc_channel.<locals>.wrapper at 0x7f94ec13b7e0>
map_content: <function map_rpc_channel.<locals>.wrapper at 0x7f94e7f75b20>
async def discover_features(self) -> None:
231    async def discover_features(self) -> None:
232        """Populate any supported traits that were not initialized in __init__."""
233        _LOGGER.debug("Starting optional trait discovery")
234        await self.device_features.refresh()
235        # Dock type also acts like a device feature for some traits.
236        dock_type = await self._dock_type()
237
238        # Initialize traits with special arguments before the generic loop
239        if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_type):
240            wash_towel_mode = WashTowelModeTrait(self.device_features)
241            wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode)  # type: ignore[assignment]
242            self.wash_towel_mode = wash_towel_mode
243
244        # Dynamically create any traits that need to be populated
245        for item in fields(self):
246            if (trait := getattr(self, item.name, None)) is not None:
247                continue
248            if (union_args := get_args(item.type)) is None:
249                raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
250            if len(union_args) != 2 or type(None) not in union_args:
251                raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
252
253            # Union args may not be in declared order
254            item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
255            if not self._is_supported(item_type, item.name, dock_type):
256                _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
257                continue
258            _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
259            trait = item_type()
260            setattr(self, item.name, trait)
261            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]:
314    def as_dict(self) -> dict[str, Any]:
315        """Return the trait data as a dictionary."""
316        result: dict[str, Any] = {}
317        for item in fields(self):
318            trait = getattr(self, item.name, None)
319            if trait is None or not isinstance(trait, RoborockBase):
320                continue
321            data = trait.as_dict()
322            if data:  # Don't omit unset traits
323                result[item.name] = data
324        return result

Return the trait data as a dictionary.