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
104117 RoborockDyadDataProtocol .PRODUCT_INFO : lambda val : DyadProductInfo .from_dict (val ),
105118}
106119
120+ def _try_json (val : Any ) -> Any :
121+ """Return *val* parsed as JSON when it is a JSON string, else *val*."""
122+ if isinstance (val , str ):
123+ try :
124+ return json .loads (val )
125+ except (json .JSONDecodeError , TypeError ):
126+ pass
127+ return val
128+
129+
107130ZEO_PROTOCOL_ENTRIES : dict [RoborockZeoProtocol , Callable ] = {
108131 # read-only
109132 RoborockZeoProtocol .STATE : lambda val : ZeoState (val ).name ,
113136 RoborockZeoProtocol .TIMES_AFTER_CLEAN : lambda val : int (val ),
114137 RoborockZeoProtocol .DETERGENT_EMPTY : lambda val : bool (val ),
115138 RoborockZeoProtocol .SOFTENER_EMPTY : lambda val : bool (val ),
139+ RoborockZeoProtocol .DIRT_DETECTION_STATUS : lambda val : ZeoDirtDetectionStatus (val ).name ,
140+ RoborockZeoProtocol .TOTAL_TIME : lambda val : int (val ),
141+ RoborockZeoProtocol .FEATURE_BITS : lambda val : int (val ),
142+ RoborockZeoProtocol .SMART_HOSTING_WAITED_TIME : lambda val : int (val ),
143+ RoborockZeoProtocol .IS_NEED_FLUFF_CLEAN : lambda val : bool (val ),
144+ RoborockZeoProtocol .PANEL_PROGRAM_PARAMS_SET_RESULT : lambda val : int (val ),
145+ RoborockZeoProtocol .DEVICE_BOUND : lambda val : bool (val ),
146+ RoborockZeoProtocol .CLOTH_PUT_IN : lambda val : bool (val ),
147+ RoborockZeoProtocol .CLOTH_READY_TO_DRY_COUNT_DOWN : lambda val : int (val ),
148+ RoborockZeoProtocol .START_DRYER_ERROR : lambda val : ZeoDryerStartError (val ).name ,
149+ RoborockZeoProtocol .DOORLOCK_STATE : lambda val : bool (val ),
150+ RoborockZeoProtocol .APP_AUTHORIZATION : lambda val : bool (val ),
151+ RoborockZeoProtocol .SMART_HOSTING_TIME : lambda val : int (val ),
152+ RoborockZeoProtocol .CUSTOM_PROGRAM_CLEANING_TIME : lambda val : int (val ),
153+ RoborockZeoProtocol .PANEL_TIMING_PROGRAM_PARAMS : lambda val : int (val ),
154+ RoborockZeoProtocol .STEAM_CARE_TIME : lambda val : int (val ),
155+ # meta — read-only (JSON)
156+ RoborockZeoProtocol .PRODUCT_INFO : lambda val : _try_json (val ),
157+ RoborockZeoProtocol .WASHING_LOG : lambda val : _try_json (val ),
158+ RoborockZeoProtocol .VOICE_RECORD_INFO : lambda val : _try_json (val ),
159+ RoborockZeoProtocol .VOICE_RECORD : lambda val : _try_json (val ),
116160 # read-write
117161 RoborockZeoProtocol .MODE : lambda val : ZeoMode (val ).name ,
118162 RoborockZeoProtocol .PROGRAM : lambda val : ZeoProgram (val ).name ,
123167 RoborockZeoProtocol .DETERGENT_TYPE : lambda val : ZeoDetergentType (val ).name ,
124168 RoborockZeoProtocol .SOFTENER_TYPE : lambda val : ZeoSoftenerType (val ).name ,
125169 RoborockZeoProtocol .SOUND_SET : lambda val : bool (val ),
170+ RoborockZeoProtocol .DIRT_DETECTION_SWITCH : lambda val : bool (val ),
171+ RoborockZeoProtocol .SOAK : lambda val : ZeoSoak (val ).name ,
172+ RoborockZeoProtocol .SILENT_MODE_ON : lambda val : bool (val ),
173+ RoborockZeoProtocol .SILENT_MODE_START_TIME : lambda val : int (val ),
174+ RoborockZeoProtocol .SILENT_MODE_END_TIME : lambda val : int (val ),
175+ RoborockZeoProtocol .DRY_CARE_MODE : lambda val : ZeoDryAndCare (val ).name ,
176+ RoborockZeoProtocol .WASH_DRY_LINKED : lambda val : bool (val ),
177+ RoborockZeoProtocol .DRYING_METHOD : lambda val : ZeoDryingMethod (val ).name ,
178+ RoborockZeoProtocol .STEAM_VOLUME : lambda val : ZeoSteamVolume (val ).name ,
179+ RoborockZeoProtocol .ION_DEODORIZATION : lambda val : bool (val ),
180+ RoborockZeoProtocol .UV_LIGHT : lambda val : bool (val ),
181+ RoborockZeoProtocol .SMART_HOSTING : lambda val : bool (val ),
182+ RoborockZeoProtocol .SOFTENER_EXPANSION_TYPE : lambda val : ZeoSoftenerExpansionType (val ).name ,
183+ RoborockZeoProtocol .DETERGENT_EXPANSION_TYPE : lambda val : ZeoDetergentExpansionType (val ).name ,
184+ RoborockZeoProtocol .SMILE_LIGHT_STATUS : lambda val : bool (val ),
185+ RoborockZeoProtocol .POWER_LIGHT : lambda val : bool (val ),
186+ RoborockZeoProtocol .PANEL_PROGRAM_PARAMS_SET : lambda val : int (val ),
187+ RoborockZeoProtocol .WIFI_LINKAGE_RESET : lambda val : int (val ),
188+ RoborockZeoProtocol .SAVE_ADAPTED_CLOUD_PROGRAM : lambda val : int (val ),
189+ RoborockZeoProtocol .CHILD_LOCK : lambda val : bool (val ),
190+ RoborockZeoProtocol .DETERGENT_SET : lambda val : bool (val ),
191+ RoborockZeoProtocol .SOFTENER_SET : lambda val : bool (val ),
192+ RoborockZeoProtocol .FLUFF_CLEANED : lambda val : bool (val ),
193+ # read-write (int-valued)
194+ RoborockZeoProtocol .CUSTOM_PARAM_SAVE : lambda val : int (val ),
195+ RoborockZeoProtocol .CUSTOM_PARAM_GET : lambda val : int (val ),
196+ RoborockZeoProtocol .DEFAULT_SETTING : lambda val : int (val ),
197+ RoborockZeoProtocol .LIGHT_SETTING : lambda val : bool (val ),
198+ RoborockZeoProtocol .DETERGENT_VOLUME : lambda val : int (val ),
199+ RoborockZeoProtocol .SOFTENER_VOLUME : lambda val : int (val ),
200+ # meta — read-write
201+ RoborockZeoProtocol .SET_SOUND_PACKAGE : lambda val : val ,
202+ RoborockZeoProtocol .VOICE_VOLUME : lambda val : val ,
203+ RoborockZeoProtocol .VOICE_SWITCH : lambda val : bool (val ),
204+ RoborockZeoProtocol .VOICE_RECORD_DELETE : lambda val : int (val ),
126205}
127206
128207
@@ -173,20 +252,30 @@ class ZeoApi(Trait, TraitUpdateListener):
173252
174253 name = "zeo"
175254
176- def __init__ (self , channel : MqttChannel ) -> None :
255+ def __init__ (self , channel : MqttChannel , product_id : str | None = None ) -> None :
177256 """Initialize the Zeo API."""
178257 TraitUpdateListener .__init__ (self , _LOGGER )
179258 self ._channel = channel
180259 self ._dps_cache : dict [int , Any ] = {}
181260 self ._dps_unsub : Callable [[], None ] | None = None
182261 self ._feature_bits : int = 0
262+ self ._feature_trait = ZeoFeatureTrait (channel , product_id )
263+ self ._command : ZeoCommandTrait | None = None
264+
265+ @property
266+ def command (self ) -> ZeoCommandTrait :
267+ """Lazily-built trait for wash-programme commands."""
268+ if self ._command is None :
269+ self ._command = ZeoCommandTrait (
270+ channel = self ._channel ,
271+ dps_cache = self ._dps_cache ,
272+ feature_trait = self ._feature_trait ,
273+ proto_entries = ZEO_PROTOCOL_ENTRIES ,
274+ )
275+ return self ._command
183276
184277 async def start (self ) -> None :
185- """Subscribe to MQTT push and discover device features.
186-
187- Subscribes to the DPS MQTT topic, then queries FEATURE_BITS
188- (DP 237) to wake the device and cache supported capabilities.
189- """
278+ """Subscribe to MQTT push and discover device capabilities."""
190279 await self ._ensure_subscribed ()
191280 await self ._discover_features ()
192281
@@ -203,13 +292,7 @@ async def _ensure_subscribed(self) -> None:
203292 self ._dps_unsub = await self ._channel .subscribe (self ._on_dps_message )
204293
205294 async def _discover_features (self ) -> None :
206- """Query FEATURE_BITS to wake the device and cache capabilities.
207-
208- Sending an RPC query after subscribing triggers the device to
209- start pushing its full state — equivalent to how V1's
210- ``discover_features()`` uses ``device_features.refresh()`` to
211- initiate the push cycle.
212- """
295+ """Query FEATURE_BITS (DP 237) and cache device capabilities."""
213296 try :
214297 result = await self .query_values ([RoborockZeoProtocol .FEATURE_BITS ])
215298 self ._feature_bits = result .get (RoborockZeoProtocol .FEATURE_BITS , 0 )
@@ -261,6 +344,6 @@ def create(product: HomeDataProduct, mqtt_channel: MqttChannel) -> DyadApi | Zeo
261344 case RoborockCategory .WET_DRY_VAC :
262345 return DyadApi (mqtt_channel )
263346 case RoborockCategory .WASHING_MACHINE :
264- return ZeoApi (mqtt_channel )
347+ return ZeoApi (mqtt_channel , product_id = product . id )
265348 case _:
266349 raise NotImplementedError (f"Unsupported category { product .category } " )
0 commit comments