1- """Create traits for V1 devices."""
1+ """Create traits for V1 devices.
2+
3+ Traits are modular components that encapsulate specific features of a Roborock
4+ device. This module provides a factory function to create and initialize the
5+ appropriate traits for V1 devices based on their capabilities. They can also
6+ be considered groups of commands and parsing logic for that command.
7+
8+ Traits have a `refresh()` method that can be called to update their state
9+ from the device. Some traits may also provide additional methods for modifying
10+ the device state.
11+
12+ The most common pattern for a trait is to subclass `V1TraitMixin` and a `RoborockBase`
13+ dataclass, and define a `command` class variable that specifies the `RoborockCommand`
14+ used to fetch the trait data from the device. See `common.py` for more details
15+ on common patterns used across traits.
16+
17+ There are some additional decorators in `common.py` that can be used to specify which
18+ RPC channel to use for the trait (standard, MQTT/cloud, or map-specific).
19+
20+ - `@common.mqtt_rpc_channel` - Use the MQTT RPC channel for this trait.
21+ - `@common.map_rpc_channel` - Use the map RPC channel for this trait.
22+
23+ There are also some attributes that specify device feature dependencies for
24+ optional traits:
25+
26+ - `requires_feature` - The string name of the device feature that must be supported
27+ for this trait to be enabled. See `DeviceFeaturesTrait` for a list of
28+ available features.
29+ - `requires_dock_type` - If set, this trait requires the
30+ device to have any of the specified dock types to be enabled. See
31+ `RoborockDockTypeCode` for a list of available dock types.
32+ """
233
334import logging
435from dataclasses import dataclass , field , fields
5- from typing import get_args
36+ from typing import Any , get_args
637
38+ from roborock .code_mappings import RoborockDockTypeCode
739from roborock .containers import HomeData , HomeDataProduct
840from roborock .devices .cache import Cache
941from roborock .devices .traits import Trait
1042from roborock .devices .v1_rpc_channel import V1RpcChannel
1143from roborock .map .map_parser import MapParserConfig
1244
1345from .child_lock import ChildLockTrait
46+ from .clean_record import CleanRecordTrait
1447from .clean_summary import CleanSummaryTrait
1548from .command import CommandTrait
1649from .common import V1TraitMixin
1750from .consumeable import ConsumableTrait
1851from .device_features import DeviceFeaturesTrait
1952from .do_not_disturb import DoNotDisturbTrait
53+ from .dock_summary import DockSummaryTrait
54+ from .dust_collection_mode import DustCollectionModeTrait
2055from .flow_led_status import FlowLedStatusTrait
2156from .home import HomeTrait
2257from .led_status import LedStatusTrait
2358from .map_content import MapContentTrait
2459from .maps import MapsTrait
2560from .rooms import RoomsTrait
61+ from .smart_wash_params import SmartWashParamsTrait
2662from .status import StatusTrait
2763from .valley_electricity_timer import ValleyElectricityTimerTrait
2864from .volume import SoundVolumeTrait
65+ from .wash_towel_mode import WashTowelModeTrait
2966
3067_LOGGER = logging .getLogger (__name__ )
3168
3572 "StatusTrait" ,
3673 "DoNotDisturbTrait" ,
3774 "CleanSummaryTrait" ,
75+ "CleanRecordTrait" ,
3876 "SoundVolumeTrait" ,
3977 "MapsTrait" ,
4078 "MapContentTrait" ,
4684 "FlowLedStatusTrait" ,
4785 "LedStatusTrait" ,
4886 "ValleyElectricityTimerTrait" ,
87+ "DockSummaryTrait" ,
88+ "DustCollectionModeTrait" ,
89+ "WashTowelModeTrait" ,
90+ "SmartWashParamsTrait" ,
4991]
5092
5193
@@ -61,19 +103,24 @@ class PropertiesApi(Trait):
61103 command : CommandTrait
62104 dnd : DoNotDisturbTrait
63105 clean_summary : CleanSummaryTrait
106+ clean_record : CleanRecordTrait
64107 sound_volume : SoundVolumeTrait
65108 rooms : RoomsTrait
66109 maps : MapsTrait
67110 map_content : MapContentTrait
68111 consumables : ConsumableTrait
69112 home : HomeTrait
70113 device_features : DeviceFeaturesTrait
114+ dust_collection_mode : DustCollectionModeTrait
71115
72116 # Optional features that may not be supported on all devices
73117 child_lock : ChildLockTrait | None = None
74118 led_status : LedStatusTrait | None = None
75119 flow_led_status : FlowLedStatusTrait | None = None
76120 valley_electricity_timer : ValleyElectricityTimerTrait | None = None
121+ wash_towel_mode : WashTowelModeTrait | None = None
122+ smart_wash_params : SmartWashParamsTrait | None = None
123+ dock_summary : DockSummaryTrait | None = None
77124
78125 def __init__ (
79126 self ,
@@ -89,8 +136,12 @@ def __init__(
89136 self ._rpc_channel = rpc_channel
90137 self ._mqtt_rpc_channel = mqtt_rpc_channel
91138 self ._map_rpc_channel = map_rpc_channel
139+ self ._cache = cache
92140
93141 self .status = StatusTrait (product )
142+ self .clean_summary = CleanSummaryTrait ()
143+ self .clean_record = CleanRecordTrait (self .clean_summary )
144+ self .consumables = ConsumableTrait ()
94145 self .rooms = RoomsTrait (home_data )
95146 self .maps = MapsTrait (self .status )
96147 self .map_content = MapContentTrait (map_parser_config )
@@ -103,7 +154,7 @@ def __init__(
103154 # We exclude optional features and them via discover_features
104155 if (union_args := get_args (item .type )) is None or len (union_args ) > 0 :
105156 continue
106- _LOGGER .debug ("Initializing trait %s " , item .name )
157+ _LOGGER .debug ("Trait '%s' is supported, initializing " , item .name )
107158 trait = item .type ()
108159 setattr (self , item .name , trait )
109160 # This is a hack to allow setting the rpc_channel on all traits. This is
@@ -124,8 +175,12 @@ def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel:
124175
125176 async def discover_features (self ) -> None :
126177 """Populate any supported traits that were not initialized in __init__."""
178+ _LOGGER .debug ("Starting optional trait discovery" )
127179 await self .device_features .refresh ()
180+ # Dock type also acts like a device feature for some traits.
181+ dock_type = await self ._dock_type ()
128182
183+ # Dynamically create any traits that need to be populated
129184 for item in fields (self ):
130185 if (trait := getattr (self , item .name , None )) is not None :
131186 continue
@@ -136,22 +191,79 @@ async def discover_features(self) -> None:
136191
137192 # Union args may not be in declared order
138193 item_type = union_args [0 ] if union_args [1 ] is type (None ) else union_args [1 ]
139- trait = item_type ()
140- if not hasattr (trait , "requires_feature" ):
141- _LOGGER .debug ("Trait missing required feature %s" , item .name )
194+ if item_type is DockSummaryTrait :
195+ # DockSummaryTrait is created manually below
142196 continue
143- _LOGGER .debug ("Checking for feature %s" , trait .requires_feature )
144- is_supported = getattr (self .device_features , trait .requires_feature )
145- # _LOGGER.debug("Device features: %s", self.device_features)
146- if is_supported is None :
147- raise ValueError (f"Device feature '{ trait .requires_feature } ' on trait '{ item .name } ' is unknown" )
148- if not is_supported :
149- _LOGGER .debug ("Disabling optional feature trait %s" , item .name )
197+ trait = item_type ()
198+ if not self ._is_supported (trait , item .name , dock_type ):
199+ _LOGGER .debug ("Trait '%s' not supported, skipping" , item .name )
150200 continue
151- _LOGGER .debug ("Enabling optional feature trait %s" , item .name )
201+
202+ _LOGGER .debug ("Trait '%s' is supported, initializing" , item .name )
152203 setattr (self , item .name , trait )
153204 trait ._rpc_channel = self ._get_rpc_channel (trait )
154205
206+ if dock_type is None or dock_type == RoborockDockTypeCode .no_dock :
207+ _LOGGER .debug ("Trait 'dock_summary' not supported, skipping" )
208+ else :
209+ _LOGGER .debug ("Trait 'dock_summary' is supported, initializing" )
210+ self .dock_summary = DockSummaryTrait (
211+ self .dust_collection_mode ,
212+ self .wash_towel_mode ,
213+ self .smart_wash_params ,
214+ )
215+
216+ def _is_supported (self , trait : V1TraitMixin , name : str , dock_type : RoborockDockTypeCode ) -> bool :
217+ """Check if a trait is supported by the device."""
218+
219+ if (requires_dock_type := getattr (trait , "requires_dock_type" , None )) is not None :
220+ return dock_type in requires_dock_type
221+
222+ if (feature_name := getattr (trait , "requires_feature" , None )) is None :
223+ _LOGGER .debug ("Optional trait missing 'requires_feature' attribute %s, skipping" , name )
224+ return False
225+ if (is_supported := getattr (self .device_features , feature_name )) is None :
226+ raise ValueError (f"Device feature '{ feature_name } ' on trait '{ name } ' is unknown" )
227+ return is_supported
228+
229+ async def _dock_type (self ) -> RoborockDockTypeCode :
230+ """Get the dock type from the status trait or cache."""
231+ dock_type = await self ._get_cached_trait_data ("dock_type" )
232+ if dock_type is not None :
233+ _LOGGER .debug ("Using cached dock type: %s" , dock_type )
234+ try :
235+ return RoborockDockTypeCode (dock_type )
236+ except ValueError :
237+ _LOGGER .debug ("Cached dock type %s is invalid, refreshing" , dock_type )
238+
239+ _LOGGER .debug ("Starting dock type discovery" )
240+ await self .status .refresh ()
241+ _LOGGER .debug ("Fetched dock type: %s" , self .status .dock_type )
242+ if self .status .dock_type is None :
243+ # Explicitly set so we reuse cached value next type
244+ dock_type = RoborockDockTypeCode .no_dock
245+ else :
246+ dock_type = self .status .dock_type
247+ await self ._set_cached_trait_data ("dock_type" , dock_type )
248+ return dock_type
249+
250+ async def _get_cached_trait_data (self , name : str ) -> Any :
251+ """Get the dock type from the status trait or cache."""
252+ cache_data = await self ._cache .get ()
253+ if cache_data .trait_data is None :
254+ cache_data .trait_data = {}
255+ _LOGGER .debug ("Cached trait data: %s" , cache_data .trait_data )
256+ return cache_data .trait_data .get (name )
257+
258+ async def _set_cached_trait_data (self , name : str , value : Any ) -> None :
259+ """Set trait-specific cached data."""
260+ cache_data = await self ._cache .get ()
261+ if cache_data .trait_data is None :
262+ cache_data .trait_data = {}
263+ cache_data .trait_data [name ] = value
264+ _LOGGER .debug ("Updating cached trait data: %s" , cache_data .trait_data )
265+ await self ._cache .set (cache_data )
266+
155267
156268def create (
157269 product : HomeDataProduct ,
0 commit comments