Skip to content

Commit 169a6a9

Browse files
author
NOisi-X
committed
fix(zeo): narrow exceptions, tighten try/except scope
except Exception → except RoborockException (aligns with Bundle's silent fallback to 0) try/except only wraps decode_rpc_response — cache updates and notify must propagate
1 parent d57b81e commit 169a6a9

2 files changed

Lines changed: 25 additions & 22 deletions

File tree

roborock/devices/device.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ async def connect(self) -> None:
202202
await self.v1_properties.start()
203203
elif self.b01_q10_properties is not None:
204204
await self.b01_q10_properties.start()
205-
if self.zeo is not None:
205+
elif self.zeo is not None:
206206
await self.zeo.start()
207207
except RoborockException:
208208
# Expected: start() can fail transiently. Unsubscribe before propagating

roborock/devices/traits/a01/__init__.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from roborock.devices.traits import Trait
5555
from roborock.devices.traits.common import TraitUpdateListener
5656
from roborock.devices.transport.mqtt_channel import MqttChannel
57-
from roborock.exceptions import RoborockException
57+
from roborock.exceptions import RoborockException, RoborockTimeout
5858
from roborock.protocols.a01_protocol import decode_rpc_response
5959
from roborock.roborock_message import (
6060
RoborockDyadDataProtocol,
@@ -104,6 +104,14 @@
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+
107115
ZEO_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

Comments
 (0)