5454from roborock .devices .traits import Trait
5555from roborock .devices .traits .common import TraitUpdateListener
5656from roborock .devices .transport .mqtt_channel import MqttChannel
57- from roborock .exceptions import RoborockException
57+ from roborock .exceptions import RoborockException , RoborockTimeout
5858from roborock .protocols .a01_protocol import decode_rpc_response
5959from roborock .roborock_message import (
6060 RoborockDyadDataProtocol ,
104104 RoborockDyadDataProtocol .PRODUCT_INFO : lambda val : DyadProductInfo .from_dict (val ),
105105}
106106
107+ # Devices known to lack FEATURE_BITS (DP 237).
108+ _UNSUPPORTED_FEATURE_BITS : frozenset [str ] = frozenset (
109+ {
110+ "roborock.wm.a63" , # H1
111+ "roborock.wm.a90" , # H1 Lite
112+ }
113+ )
114+
107115ZEO_PROTOCOL_ENTRIES : dict [RoborockZeoProtocol , Callable ] = {
108116 # read-only
109117 RoborockZeoProtocol .STATE : lambda val : ZeoState (val ).name ,
@@ -173,13 +181,14 @@ class ZeoApi(Trait, TraitUpdateListener):
173181
174182 name = "zeo"
175183
176- def __init__ (self , channel : MqttChannel ) -> None :
184+ def __init__ (self , channel : MqttChannel , product_id : str | None = None ) -> None :
177185 """Initialize the Zeo API."""
178186 TraitUpdateListener .__init__ (self , _LOGGER )
179187 self ._channel = channel
180188 self ._dps_cache : dict [int , Any ] = {}
181189 self ._dps_unsub : Callable [[], None ] | None = None
182190 self ._feature_bits : int = 0
191+ self ._product_id = product_id
183192
184193 async def start (self ) -> None :
185194 """Subscribe to MQTT push and discover device features.
@@ -205,37 +214,34 @@ async def _ensure_subscribed(self) -> None:
205214 async def _discover_features (self ) -> None :
206215 """Query FEATURE_BITS to wake the device and cache capabilities.
207216
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 .
217+ 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 .
212221 """
222+ if self ._product_id in _UNSUPPORTED_FEATURE_BITS :
223+ return
213224 try :
214225 result = await self .query_values ([RoborockZeoProtocol .FEATURE_BITS ])
215226 self ._feature_bits = result .get (RoborockZeoProtocol .FEATURE_BITS , 0 )
216- except Exception :
227+ except RoborockTimeout :
217228 self ._feature_bits = 0
218229
219230 def supports (self , feature : ZeoFeatureBits ) -> bool :
220231 """Check whether the device supports a given feature bit."""
221232 return bool (self ._feature_bits & (1 << feature .value ))
222233
223234 def _on_dps_message (self , message : RoborockMessage ) -> None :
224- """Handle unsolicited MQTT push (protocol 102 — RPC_RESPONSE).
225-
226- Zeo devices broadcast status changes as ``{"dps": {...}}`` JSON
227- payloads. This callback decodes them and feeds the cache so
228- that ``query_values`` can skip the device round-trip when the
229- requested DPs are already up to date.
230- """
235+ """Handle unsolicited MQTT push (protocol 102 — RPC_RESPONSE)."""
231236 if message .protocol != RoborockMessageProtocol .RPC_RESPONSE :
232237 return
233238 try :
234239 decoded = decode_rpc_response (message )
235- self ._dps_cache .update (decoded )
236- self ._notify_update ()
237240 except RoborockException :
238- _LOGGER .debug ("Failed to decode push message, skipping: %s" , message , exc_info = True )
241+ _LOGGER .debug ("Dropped malformed push message" , exc_info = True )
242+ return
243+ self ._dps_cache .update (decoded )
244+ self ._notify_update ()
239245
240246 async def query_values (self , protocols : list [RoborockZeoProtocol ]) -> dict [RoborockZeoProtocol , Any ]:
241247 """Query the device for the values of the given protocols."""
@@ -244,9 +250,6 @@ async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[Robor
244250 {RoborockZeoProtocol .ID_QUERY : protocols },
245251 value_encoder = json .dumps ,
246252 )
247- for protocol , value in response .items ():
248- if value is not None :
249- self ._dps_cache [int (protocol )] = value
250253 return {protocol : convert_zeo_value (protocol , response .get (protocol )) for protocol in protocols }
251254
252255 async def set_value (self , protocol : RoborockZeoProtocol , value : Any ) -> dict [RoborockZeoProtocol , Any ]:
@@ -261,6 +264,6 @@ def create(product: HomeDataProduct, mqtt_channel: MqttChannel) -> DyadApi | Zeo
261264 case RoborockCategory .WET_DRY_VAC :
262265 return DyadApi (mqtt_channel )
263266 case RoborockCategory .WASHING_MACHINE :
264- return ZeoApi (mqtt_channel )
267+ return ZeoApi (mqtt_channel , product_id = product . id )
265268 case _:
266269 raise NotImplementedError (f"Unsupported category { product .category } " )
0 commit comments