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
113126 RoborockZeoProtocol .TIMES_AFTER_CLEAN : lambda val : int (val ),
114127 RoborockZeoProtocol .DETERGENT_EMPTY : lambda val : bool (val ),
115128 RoborockZeoProtocol .SOFTENER_EMPTY : lambda val : bool (val ),
129+ RoborockZeoProtocol .DIRT_DETECTION_STATUS : lambda val : ZeoDirtDetectionStatus (val ).name ,
130+ RoborockZeoProtocol .TOTAL_TIME : lambda val : int (val ),
131+ RoborockZeoProtocol .FEATURE_BITS : lambda val : int (val ),
132+ RoborockZeoProtocol .SMART_HOSTING_WAITED_TIME : lambda val : int (val ),
133+ RoborockZeoProtocol .IS_NEED_FLUFF_CLEAN : lambda val : bool (val ),
134+ RoborockZeoProtocol .PANEL_PROGRAM_PARAMS_SET_RESULT : lambda val : int (val ),
135+ RoborockZeoProtocol .DEVICE_BOUND : lambda val : bool (val ),
136+ RoborockZeoProtocol .CLOTH_PUT_IN : lambda val : bool (val ),
137+ RoborockZeoProtocol .CLOTH_READY_TO_DRY_COUNT_DOWN : lambda val : int (val ),
138+ RoborockZeoProtocol .START_DRYER_ERROR : lambda val : ZeoDryerStartError (val ).name ,
139+ RoborockZeoProtocol .DOORLOCK_STATE : lambda val : bool (val ),
140+ RoborockZeoProtocol .APP_AUTHORIZATION : lambda val : bool (val ),
141+ RoborockZeoProtocol .SMART_HOSTING_TIME : lambda val : int (val ),
142+ RoborockZeoProtocol .CUSTOM_PROGRAM_CLEANING_TIME : lambda val : int (val ),
143+ RoborockZeoProtocol .PANEL_TIMING_PROGRAM_PARAMS : lambda val : int (val ),
144+ RoborockZeoProtocol .STEAM_CARE_TIME : lambda val : int (val ),
145+ # meta — read-only (JSON)
146+ RoborockZeoProtocol .PRODUCT_INFO : lambda val : _try_json (val ),
147+ RoborockZeoProtocol .WASHING_LOG : lambda val : _try_json (val ),
148+ RoborockZeoProtocol .VOICE_RECORD_INFO : lambda val : _try_json (val ),
149+ RoborockZeoProtocol .VOICE_RECORD : lambda val : _try_json (val ),
116150 # read-write
117151 RoborockZeoProtocol .MODE : lambda val : ZeoMode (val ).name ,
118152 RoborockZeoProtocol .PROGRAM : lambda val : ZeoProgram (val ).name ,
123157 RoborockZeoProtocol .DETERGENT_TYPE : lambda val : ZeoDetergentType (val ).name ,
124158 RoborockZeoProtocol .SOFTENER_TYPE : lambda val : ZeoSoftenerType (val ).name ,
125159 RoborockZeoProtocol .SOUND_SET : lambda val : bool (val ),
160+ RoborockZeoProtocol .DIRT_DETECTION_SWITCH : lambda val : bool (val ),
161+ RoborockZeoProtocol .SOAK : lambda val : ZeoSoak (val ).name ,
162+ RoborockZeoProtocol .SILENT_MODE_ON : lambda val : bool (val ),
163+ RoborockZeoProtocol .SILENT_MODE_START_TIME : lambda val : int (val ),
164+ RoborockZeoProtocol .SILENT_MODE_END_TIME : lambda val : int (val ),
165+ RoborockZeoProtocol .DRY_CARE_MODE : lambda val : ZeoDryAndCare (val ).name ,
166+ RoborockZeoProtocol .WASH_DRY_LINKED : lambda val : bool (val ),
167+ RoborockZeoProtocol .DRYING_METHOD : lambda val : ZeoDryingMethod (val ).name ,
168+ RoborockZeoProtocol .STEAM_VOLUME : lambda val : ZeoSteamVolume (val ).name ,
169+ RoborockZeoProtocol .ION_DEODORIZATION : lambda val : bool (val ),
170+ RoborockZeoProtocol .UV_LIGHT : lambda val : bool (val ),
171+ RoborockZeoProtocol .SMART_HOSTING : lambda val : bool (val ),
172+ RoborockZeoProtocol .SOFTENER_EXPANSION_TYPE : lambda val : ZeoSoftenerExpansionType (val ).name ,
173+ RoborockZeoProtocol .DETERGENT_EXPANSION_TYPE : lambda val : ZeoDetergentExpansionType (val ).name ,
174+ RoborockZeoProtocol .SMILE_LIGHT_STATUS : lambda val : bool (val ),
175+ RoborockZeoProtocol .POWER_LIGHT : lambda val : bool (val ),
176+ RoborockZeoProtocol .PANEL_PROGRAM_PARAMS_SET : lambda val : int (val ),
177+ RoborockZeoProtocol .WIFI_LINKAGE_RESET : lambda val : int (val ),
178+ RoborockZeoProtocol .SAVE_ADAPTED_CLOUD_PROGRAM : lambda val : int (val ),
179+ RoborockZeoProtocol .CHILD_LOCK : lambda val : bool (val ),
180+ RoborockZeoProtocol .DETERGENT_SET : lambda val : bool (val ),
181+ RoborockZeoProtocol .SOFTENER_SET : lambda val : bool (val ),
182+ RoborockZeoProtocol .FLUFF_CLEANED : lambda val : bool (val ),
183+ # read-write (int-valued)
184+ RoborockZeoProtocol .CUSTOM_PARAM_SAVE : lambda val : int (val ),
185+ RoborockZeoProtocol .CUSTOM_PARAM_GET : lambda val : int (val ),
186+ RoborockZeoProtocol .DEFAULT_SETTING : lambda val : int (val ),
187+ RoborockZeoProtocol .LIGHT_SETTING : lambda val : bool (val ),
188+ RoborockZeoProtocol .DETERGENT_VOLUME : lambda val : int (val ),
189+ RoborockZeoProtocol .SOFTENER_VOLUME : lambda val : int (val ),
190+ # meta — read-write
191+ RoborockZeoProtocol .SET_SOUND_PACKAGE : lambda val : val ,
192+ RoborockZeoProtocol .VOICE_VOLUME : lambda val : val ,
193+ RoborockZeoProtocol .VOICE_SWITCH : lambda val : bool (val ),
194+ RoborockZeoProtocol .VOICE_RECORD_DELETE : lambda val : int (val ),
126195}
127196
128197
@@ -173,20 +242,30 @@ class ZeoApi(Trait, TraitUpdateListener):
173242
174243 name = "zeo"
175244
176- def __init__ (self , channel : MqttChannel ) -> None :
245+ def __init__ (self , channel : MqttChannel , product_id : str | None = None ) -> None :
177246 """Initialize the Zeo API."""
178247 TraitUpdateListener .__init__ (self , _LOGGER )
179248 self ._channel = channel
180249 self ._dps_cache : dict [int , Any ] = {}
181250 self ._dps_unsub : Callable [[], None ] | None = None
182251 self ._feature_bits : int = 0
252+ self ._feature_trait = ZeoFeatureTrait (channel , product_id )
253+ self ._command : ZeoCommandTrait | None = None
254+
255+ @property
256+ def command (self ) -> ZeoCommandTrait :
257+ """Lazily-built trait for wash-programme commands."""
258+ if self ._command is None :
259+ self ._command = ZeoCommandTrait (
260+ channel = self ._channel ,
261+ dps_cache = self ._dps_cache ,
262+ feature_trait = self ._feature_trait ,
263+ proto_entries = ZEO_PROTOCOL_ENTRIES ,
264+ )
265+ return self ._command
183266
184267 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- """
268+ """Subscribe to MQTT push and discover device capabilities."""
190269 await self ._ensure_subscribed ()
191270 await self ._discover_features ()
192271
@@ -203,13 +282,7 @@ async def _ensure_subscribed(self) -> None:
203282 self ._dps_unsub = await self ._channel .subscribe (self ._on_dps_message )
204283
205284 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- """
285+ """Query FEATURE_BITS (DP 237) and cache device capabilities."""
213286 try :
214287 result = await self .query_values ([RoborockZeoProtocol .FEATURE_BITS ])
215288 self ._feature_bits = result .get (RoborockZeoProtocol .FEATURE_BITS , 0 )
@@ -261,6 +334,6 @@ def create(product: HomeDataProduct, mqtt_channel: MqttChannel) -> DyadApi | Zeo
261334 case RoborockCategory .WET_DRY_VAC :
262335 return DyadApi (mqtt_channel )
263336 case RoborockCategory .WASHING_MACHINE :
264- return ZeoApi (mqtt_channel )
337+ return ZeoApi (mqtt_channel , product_id = product . id )
265338 case _:
266339 raise NotImplementedError (f"Unsupported category { product .category } " )
0 commit comments