3838 RoborockDyadStateCode ,
3939)
4040from roborock .data .zeo .zeo_code_mappings import (
41+ ZeoDetergentExpansionType ,
4142 ZeoDetergentType ,
43+ ZeoDirtDetectionStatus ,
44+ ZeoDryAndCare ,
45+ ZeoDryerStartError ,
46+ ZeoDryingMethod ,
4247 ZeoDryingMode ,
4348 ZeoError ,
4449 ZeoFeatureBits ,
4550 ZeoMode ,
4651 ZeoProgram ,
4752 ZeoRinse ,
53+ ZeoSoak ,
54+ ZeoSoftenerExpansionType ,
4855 ZeoSoftenerType ,
4956 ZeoSpin ,
5057 ZeoState ,
58+ ZeoSteamVolume ,
5159 ZeoTemperature ,
5260)
5361from roborock .devices .rpc .a01_channel import send_decoded_command
6371 RoborockZeoProtocol ,
6472)
6573
74+ from .command import ZeoCommandTrait # noqa: F401 — re‑export
75+ from .device_features import ZeoFeatures , ZeoFeatureTrait # noqa: F401 — re‑export
76+
6677_LOGGER = logging .getLogger (__name__ )
6778
6879__init__ = [
6980 "DyadApi" ,
7081 "ZeoApi" ,
82+ "ZeoCommandTrait" ,
83+ "ZeoFeatureTrait" ,
7184]
7285
7386
112125 }
113126)
114127
128+
129+ def _try_json (val : Any ) -> Any :
130+ """Return *val* parsed as JSON when it is a JSON string, else *val*."""
131+ if isinstance (val , str ):
132+ try :
133+ return json .loads (val )
134+ except (json .JSONDecodeError , TypeError ):
135+ pass
136+ return val
137+
115138ZEO_PROTOCOL_ENTRIES : dict [RoborockZeoProtocol , Callable ] = {
116139 # read-only
117140 RoborockZeoProtocol .STATE : lambda val : ZeoState (val ).name ,
121144 RoborockZeoProtocol .TIMES_AFTER_CLEAN : lambda val : int (val ),
122145 RoborockZeoProtocol .DETERGENT_EMPTY : lambda val : bool (val ),
123146 RoborockZeoProtocol .SOFTENER_EMPTY : lambda val : bool (val ),
147+ RoborockZeoProtocol .DIRT_DETECTION_STATUS : lambda val : ZeoDirtDetectionStatus (val ).name ,
148+ RoborockZeoProtocol .TOTAL_TIME : lambda val : int (val ),
149+ RoborockZeoProtocol .FEATURE_BITS : lambda val : int (val ),
150+ RoborockZeoProtocol .SMART_HOSTING_WAITED_TIME : lambda val : int (val ),
151+ RoborockZeoProtocol .IS_NEED_FLUFF_CLEAN : lambda val : bool (val ),
152+ RoborockZeoProtocol .PANEL_PROGRAM_PARAMS_SET_RESULT : lambda val : int (val ),
153+ RoborockZeoProtocol .DEVICE_BOUND : lambda val : bool (val ),
154+ RoborockZeoProtocol .CLOTH_PUT_IN : lambda val : bool (val ),
155+ RoborockZeoProtocol .CLOTH_READY_TO_DRY_COUNT_DOWN : lambda val : int (val ),
156+ RoborockZeoProtocol .START_DRYER_ERROR : lambda val : ZeoDryerStartError (val ).name ,
157+ RoborockZeoProtocol .DOORLOCK_STATE : lambda val : bool (val ),
158+ RoborockZeoProtocol .APP_AUTHORIZATION : lambda val : bool (val ),
159+ RoborockZeoProtocol .SMART_HOSTING_TIME : lambda val : int (val ),
160+ RoborockZeoProtocol .CUSTOM_PROGRAM_CLEANING_TIME : lambda val : int (val ),
161+ RoborockZeoProtocol .PANEL_TIMING_PROGRAM_PARAMS : lambda val : int (val ),
162+ RoborockZeoProtocol .STEAM_CARE_TIME : lambda val : int (val ),
163+ # meta — read-only (JSON)
164+ RoborockZeoProtocol .PRODUCT_INFO : lambda val : _try_json (val ),
165+ RoborockZeoProtocol .WASHING_LOG : lambda val : _try_json (val ),
166+ RoborockZeoProtocol .VOICE_RECORD_INFO : lambda val : _try_json (val ),
167+ RoborockZeoProtocol .VOICE_RECORD : lambda val : _try_json (val ),
124168 # read-write
125169 RoborockZeoProtocol .MODE : lambda val : ZeoMode (val ).name ,
126170 RoborockZeoProtocol .PROGRAM : lambda val : ZeoProgram (val ).name ,
131175 RoborockZeoProtocol .DETERGENT_TYPE : lambda val : ZeoDetergentType (val ).name ,
132176 RoborockZeoProtocol .SOFTENER_TYPE : lambda val : ZeoSoftenerType (val ).name ,
133177 RoborockZeoProtocol .SOUND_SET : lambda val : bool (val ),
178+ RoborockZeoProtocol .DIRT_DETECTION_SWITCH : lambda val : bool (val ),
179+ RoborockZeoProtocol .SOAK : lambda val : ZeoSoak (val ).name ,
180+ RoborockZeoProtocol .SILENT_MODE_ON : lambda val : bool (val ),
181+ RoborockZeoProtocol .SILENT_MODE_START_TIME : lambda val : int (val ),
182+ RoborockZeoProtocol .SILENT_MODE_END_TIME : lambda val : int (val ),
183+ RoborockZeoProtocol .DRY_CARE_MODE : lambda val : ZeoDryAndCare (val ).name ,
184+ RoborockZeoProtocol .WASH_DRY_LINKED : lambda val : bool (val ),
185+ RoborockZeoProtocol .DRYING_METHOD : lambda val : ZeoDryingMethod (val ).name ,
186+ RoborockZeoProtocol .STEAM_VOLUME : lambda val : ZeoSteamVolume (val ).name ,
187+ RoborockZeoProtocol .ION_DEODORIZATION : lambda val : bool (val ),
188+ RoborockZeoProtocol .UV_LIGHT : lambda val : bool (val ),
189+ RoborockZeoProtocol .SMART_HOSTING : lambda val : bool (val ),
190+ RoborockZeoProtocol .SOFTENER_EXPANSION_TYPE : lambda val : ZeoSoftenerExpansionType (val ).name ,
191+ RoborockZeoProtocol .DETERGENT_EXPANSION_TYPE : lambda val : ZeoDetergentExpansionType (val ).name ,
192+ RoborockZeoProtocol .SMILE_LIGHT_STATUS : lambda val : bool (val ),
193+ RoborockZeoProtocol .POWER_LIGHT : lambda val : bool (val ),
194+ RoborockZeoProtocol .PANEL_PROGRAM_PARAMS_SET : lambda val : int (val ),
195+ RoborockZeoProtocol .WIFI_LINKAGE_RESET : lambda val : int (val ),
196+ RoborockZeoProtocol .SAVE_ADAPTED_CLOUD_PROGRAM : lambda val : int (val ),
197+ RoborockZeoProtocol .CHILD_LOCK : lambda val : bool (val ),
198+ RoborockZeoProtocol .DETERGENT_SET : lambda val : bool (val ),
199+ RoborockZeoProtocol .SOFTENER_SET : lambda val : bool (val ),
200+ RoborockZeoProtocol .FLUFF_CLEANED : lambda val : bool (val ),
201+ # read-write (int-valued)
202+ RoborockZeoProtocol .CUSTOM_PARAM_SAVE : lambda val : int (val ),
203+ RoborockZeoProtocol .CUSTOM_PARAM_GET : lambda val : int (val ),
204+ RoborockZeoProtocol .DEFAULT_SETTING : lambda val : int (val ),
205+ RoborockZeoProtocol .LIGHT_SETTING : lambda val : bool (val ),
206+ RoborockZeoProtocol .DETERGENT_VOLUME : lambda val : int (val ),
207+ RoborockZeoProtocol .SOFTENER_VOLUME : lambda val : int (val ),
208+ # meta — read-write
209+ RoborockZeoProtocol .SET_SOUND_PACKAGE : lambda val : val ,
210+ RoborockZeoProtocol .VOICE_VOLUME : lambda val : val ,
211+ RoborockZeoProtocol .VOICE_SWITCH : lambda val : bool (val ),
212+ RoborockZeoProtocol .VOICE_RECORD_DELETE : lambda val : int (val ),
134213}
135214
136215
@@ -189,13 +268,23 @@ def __init__(self, channel: MqttChannel, product_id: str | None = None) -> None:
189268 self ._dps_unsub : Callable [[], None ] | None = None
190269 self ._feature_bits : int = 0
191270 self ._product_id = product_id
271+ self ._feature_trait = ZeoFeatureTrait (channel , product_id )
272+ self ._command : ZeoCommandTrait | None = None
273+
274+ @property
275+ def command (self ) -> ZeoCommandTrait :
276+ """Lazily-built trait for wash-programme commands."""
277+ if self ._command is None :
278+ self ._command = ZeoCommandTrait (
279+ channel = self ._channel ,
280+ dps_cache = self ._dps_cache ,
281+ feature_trait = self ._feature_trait ,
282+ proto_entries = ZEO_PROTOCOL_ENTRIES ,
283+ )
284+ return self ._command
192285
193286 async def start (self ) -> None :
194- """Subscribe to MQTT push and discover device features.
195-
196- Subscribes to the DPS MQTT topic, then queries FEATURE_BITS
197- (DP 237) to wake the device and cache supported capabilities.
198- """
287+ """Subscribe to MQTT push and discover device capabilities."""
199288 await self ._ensure_subscribed ()
200289 await self ._discover_features ()
201290
@@ -215,9 +304,9 @@ async def _discover_features(self) -> None:
215304 """Query FEATURE_BITS to wake the device and cache capabilities.
216305
217306 Only devices that support the FeatureBits DP will respond;
218- For devices known to lack this DP
219- the query is skipped entirely; for all other devices a
220- timeout propagates as a connection error.
307+ For devices known to lack this DP the query is skipped
308+ entirely; for all other devices a timeout propagates as a
309+ connection error.
221310 """
222311 if self ._product_id in _UNSUPPORTED_FEATURE_BITS :
223312 return
0 commit comments