Skip to content

Commit 44465af

Browse files
author
NOisi-X
committed
feat(zeo): add _discover_features() and supports() with FEATURE_BITS; add DP 243
fix test snapshot for FEATURE_BITS query; pass ruff lint
1 parent 55189c6 commit 44465af

4 files changed

Lines changed: 57 additions & 12 deletions

File tree

roborock/devices/traits/a01/__init__.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@
2525
from datetime import time
2626
from typing import Any
2727

28-
from roborock.exceptions import RoborockException
29-
from roborock.protocols.a01_protocol import decode_rpc_response
30-
3128
from roborock.data import DyadProductInfo, DyadSndState, HomeDataProduct, RoborockCategory
3229
from roborock.data.dyad.dyad_code_mappings import (
3330
DyadBrushSpeed,
@@ -44,6 +41,7 @@
4441
ZeoDetergentType,
4542
ZeoDryingMode,
4643
ZeoError,
44+
ZeoFeatureBits,
4745
ZeoMode,
4846
ZeoProgram,
4947
ZeoRinse,
@@ -56,6 +54,8 @@
5654
from roborock.devices.traits import Trait
5755
from roborock.devices.traits.common import TraitUpdateListener
5856
from roborock.devices.transport.mqtt_channel import MqttChannel
57+
from roborock.exceptions import RoborockException
58+
from roborock.protocols.a01_protocol import decode_rpc_response
5959
from roborock.roborock_message import (
6060
RoborockDyadDataProtocol,
6161
RoborockMessage,
@@ -179,10 +179,16 @@ def __init__(self, channel: MqttChannel) -> None:
179179
self._channel = channel
180180
self._dps_cache: dict[int, Any] = {}
181181
self._dps_unsub: Callable[[], None] | None = None
182+
self._feature_bits: int = 0
182183

183184
async def start(self) -> None:
184-
"""Subscribe to MQTT push (called once after device connects)."""
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+
"""
185190
await self._ensure_subscribed()
191+
await self._discover_features()
186192

187193
def close(self) -> None:
188194
"""Unsubscribe from MQTT push and release resources."""
@@ -196,6 +202,24 @@ async def _ensure_subscribed(self) -> None:
196202
return
197203
self._dps_unsub = await self._channel.subscribe(self._on_dps_message)
198204

205+
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+
"""
213+
try:
214+
result = await self.query_values([RoborockZeoProtocol.FEATURE_BITS])
215+
self._feature_bits = result.get(RoborockZeoProtocol.FEATURE_BITS, 0)
216+
except Exception:
217+
self._feature_bits = 0
218+
219+
def supports(self, feature: ZeoFeatureBits) -> bool:
220+
"""Check whether the device supports a given feature bit."""
221+
return bool(self._feature_bits & (1 << feature.value))
222+
199223
def _on_dps_message(self, message: RoborockMessage) -> None:
200224
"""Handle unsolicited MQTT push (protocol 102 — RPC_RESPONSE).
201225

roborock/roborock_message.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ class RoborockZeoProtocol(RoborockEnum):
179179
SILENT_MODE_ON = 240 # rw [independent] use set_silent_mode() for bundled set
180180
SILENT_MODE_START_TIME = 241 # rw [independent] minute-of-day
181181
SILENT_MODE_END_TIME = 242 # rw [independent] minute-of-day
182+
UNKNOWN_243 = (
183+
243 # unknown, not found in plugin bundle; present in MQTT push from some devices, increments with each push
184+
)
182185
DRY_CARE_MODE = 244 # rw [startWith]
183186
SOFTENER_EXPANSION_TYPE = 245 # rw [independent]
184187
SMILE_LIGHT_STATUS = 247 # rw [independent]

tests/e2e/__snapshots__/test_device_manager.ambr

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,35 @@
1313
[mqtt <]
1414
00000000 90 04 00 01 00 00 |......|
1515
[mqtt >]
16-
00000000 30 5a 00 20 72 72 2f 6d 2f 69 2f 75 73 65 72 31 |0Z. rr/m/i/user1|
16+
00000000 30 6a 00 20 72 72 2f 6d 2f 69 2f 75 73 65 72 31 |0j. rr/m/i/user1|
1717
00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_|
1818
00000020 64 75 69 64 00 41 30 31 00 00 23 82 00 00 23 83 |duid.A01..#...#.|
19-
00000030 68 a6 a2 24 00 65 00 20 c5 de 2b f6 a9 ba 32 7e |h..$.e. ..+...2~|
20-
00000040 6b 73 82 bb d8 67 d4 db 7e cd 61 aa 8c 38 56 53 |ks...g..~.a..8VS|
21-
00000050 ca 4e 15 0d b1 b7 80 a2 0f 16 58 36 |.N........X6|
19+
00000030 68 a6 a2 25 00 65 00 30 c5 de 2b f6 a9 ba 32 7e |h..%.e.0..+...2~|
20+
00000040 6b 73 82 bb d8 67 d4 db 7d a3 e2 16 16 7e 83 5f |ks...g..}....~._|
21+
00000050 bb 3d 2b 79 6b d0 52 9b 60 17 2d f4 06 3b a8 66 |.=+yk.R.`.-..;.f|
22+
00000060 f7 20 c0 6a c2 94 1e 84 f8 91 41 67 |. .j......Ag|
2223
[mqtt <]
2324
00000000 30 5e 00 20 72 72 2f 6d 2f 6f 2f 75 73 65 72 31 |0^. rr/m/o/user1|
2425
00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_|
2526
00000020 64 75 69 64 00 00 00 00 37 41 30 31 00 00 00 00 |duid....7A01....|
26-
00000030 00 00 00 17 68 a6 a2 23 00 66 00 20 c6 d0 06 0c |....h..#.f. ....|
27+
00000030 00 00 00 17 68 a6 a2 23 00 66 00 20 fe 9c 2a 27 |....h..#.f. ..*'|
28+
00000040 da c4 6b 9f 0e cf 2c 56 ba 5b e6 99 1f a1 29 78 |..k...,V.[....)x|
29+
00000050 37 37 42 66 bb d5 70 0e d4 c0 a7 d1 7f ef 8b 33 |77Bf..p........3|
30+
[mqtt >]
31+
00000000 30 6a 00 20 72 72 2f 6d 2f 69 2f 75 73 65 72 31 |0j. rr/m/i/user1|
32+
00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_|
33+
00000020 64 75 69 64 00 41 30 31 00 00 23 84 00 00 23 85 |duid.A01..#...#.|
34+
00000030 68 a6 a2 26 00 65 00 30 00 5e b7 20 93 7b 53 a7 |h..&.e.0.^. .{S.|
35+
00000040 f9 47 d4 53 49 51 cd 59 c7 92 65 09 f3 7d 20 58 |.G.SIQ.Y..e..} X|
36+
00000050 c6 9e 25 54 cd 4f ab c5 fc 48 0e 67 4a 97 28 59 |..%T.O...H.gJ.(Y|
37+
00000060 14 a6 c6 77 39 ab 2a ef 77 d9 9d 63 |...w9.*.w..c|
38+
[mqtt <]
39+
00000000 30 5e 00 20 72 72 2f 6d 2f 6f 2f 75 73 65 72 31 |0^. rr/m/o/user1|
40+
00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_|
41+
00000020 64 75 69 64 00 00 00 00 37 41 30 31 00 00 00 00 |duid....7A01....|
42+
00000030 00 00 00 17 68 a6 a2 24 00 66 00 20 c6 d0 06 0c |....h..$.f. ....|
2743
00000040 04 eb 86 8c 96 8c 51 45 4f 8e 96 93 9e 3d de 35 |......QEO....=.5|
28-
00000050 bb a3 92 cf 68 49 69 ba 83 25 cc 5d 77 e8 62 8a |....hIi..%.]w.b.|
44+
00000050 bb a3 92 cf 68 49 69 ba 83 25 cc 5d 43 da 0b 55 |....hIi..%.]C..U|
2945
# ---
3046
# name: test_l01_device
3147
[mqtt >]

tests/e2e/test_device_manager.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,10 @@ async def test_a01_device(
527527
test_topic = TEST_TOPIC_FORMAT.format(duid="zeo_duid")
528528
mqtt_responses: list[bytes] = [
529529
*MQTT_DEFAULT_RESPONSES,
530-
# ACK the Query state call sent below. id is deterministic based on deterministic_message_fixtures
531-
mqtt_packet.gen_publish(test_topic, mid=2, payload=response_builder.build_a01_rpc({"203": 6})),
530+
# ACK the FEATURE_BITS query sent by _discover_features()
531+
mqtt_packet.gen_publish(test_topic, mid=2, payload=response_builder.build_a01_rpc({"237": 1})),
532+
# ACK the Query state call sent below
533+
mqtt_packet.gen_publish(test_topic, mid=3, payload=response_builder.build_a01_rpc({"203": 6})),
532534
]
533535
for response in mqtt_responses:
534536
push_mqtt_response(response)

0 commit comments

Comments
 (0)