diff --git a/roborock/data/zeo/zeo_code_mappings.py b/roborock/data/zeo/zeo_code_mappings.py index 76b8d7b8..ab68bd0e 100644 --- a/roborock/data/zeo/zeo_code_mappings.py +++ b/roborock/data/zeo/zeo_code_mappings.py @@ -1,8 +1,5 @@ """Zeo (washing machine) device enums. -Member-level comments show the manufacturer's official identifiers -extracted from the React Native app plugin bundle. - For enums that map between protocol-level positions and physical units (e.g. spin speed, temperature), the comment format is ``# L, `` where ``L`` is the protocol position @@ -18,12 +15,7 @@ class ZeoFeatureBits(RoborockEnum): - """Bit positions in DP 237 (FEATURE_BITS). - - Extracted from the official Roborock Washer app plugin bundle - (index.ios.bundle, module 726). Each member's integer value - is the exact bit offset the device reports at DP 237. - """ + """Bit positions in DP 237 (FEATURE_BITS).""" smart_hosting = 0 silent_mode = 1 @@ -52,7 +44,7 @@ class ZeoFeatureBits(RoborockEnum): class ZeoMode(RoborockEnum): - null = 0 # (not found in app bundle) + null = 0 wash = 1 wash_and_dry = 2 dry = 3 @@ -60,6 +52,7 @@ class ZeoMode(RoborockEnum): class ZeoState(RoborockEnum): + null = 0 standby = 1 weighing = 2 # Checking soaking = 3 @@ -83,7 +76,7 @@ class ZeoState(RoborockEnum): class ZeoProgram(RoborockEnum): - null = 0 # (not found in app bundle) + null = 0 standard = 1 # Mixed quick = 2 sanitize = 3 # Sterilization @@ -152,6 +145,7 @@ class ZeoSoak(RoborockEnum): class ZeoTemperature(RoborockEnum): + null = 0 normal = 1 # L1, 0 C low = 2 # L2, 30 C medium = 3 # L3, 40 C @@ -203,11 +197,13 @@ class ZeoSoftenerType(RoborockEnum): class ZeoDetergentExpansionType(RoborockEnum): + null = 0 concentrated_detergent = 1 detergent = 2 class ZeoSoftenerExpansionType(RoborockEnum): + null = 0 softener = 1 softener_expansion = 2 wool_detergent = 3 @@ -241,6 +237,7 @@ class ZeoError(RoborockEnum): class ZeoDryingMethod(RoborockEnum): + null = 0 l1 = 1 # L1, Saving l2 = 2 # L2, Standard l3 = 3 # L3, SuperFast @@ -255,11 +252,13 @@ class ZeoSteamVolume(RoborockEnum): class ZeoDryAndCare(RoborockEnum): + null = 0 soft = 1 normal = 2 class ZeoDryerStartError(RoborockEnum): + null = 0 dryer_running = 1 # Washer-Dryer Pairing cannot start: Dryer is running. dryer_error = 2 # Washer-Dryer Pairing cannot start: Dryer has an error. dryer_done = 3 # Dryer drying is complete, please remove the clothes first. @@ -269,7 +268,7 @@ class ZeoDryerStartError(RoborockEnum): dryer_network_fail = 7 # Please check the dryer network connection. -# The following are App-internal lookup tables extracted from the bundle. +# The following are App-internal lookup tables. # They are NOT DP protocol enums — the device never receives these values. # They control how the official app adjusts recommended dosages or UI visibility. # diff --git a/roborock/devices/device.py b/roborock/devices/device.py index f6226f07..3e6c7df6 100644 --- a/roborock/devices/device.py +++ b/roborock/devices/device.py @@ -202,6 +202,8 @@ async def connect(self) -> None: await self.v1_properties.start() elif self.b01_q10_properties is not None: await self.b01_q10_properties.start() + elif self.zeo is not None: + await self.zeo.start() except RoborockException: # Expected: start() can fail transiently. Unsubscribe before propagating # so the retry by connect_loop() gets a clean channel. diff --git a/roborock/devices/traits/a01/__init__.py b/roborock/devices/traits/a01/__init__.py index 3108cc95..cc4c924a 100644 --- a/roborock/devices/traits/a01/__init__.py +++ b/roborock/devices/traits/a01/__init__.py @@ -22,6 +22,7 @@ """ import json +import logging from collections.abc import Callable from datetime import time from typing import Any @@ -42,6 +43,7 @@ ZeoDetergentType, ZeoDryingMode, ZeoError, + ZeoFeatureBits, ZeoMode, ZeoProgram, ZeoRinse, @@ -52,10 +54,19 @@ ) from roborock.devices.rpc.a01_channel import send_decoded_command from roborock.devices.traits import Trait +from roborock.devices.traits.a01.device_feature import build_force_load_dp_list +from roborock.devices.traits.common import TraitUpdateListener from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.exceptions import RoborockException from roborock.protocols.a01_protocol import decode_rpc_response -from roborock.roborock_message import RoborockDyadDataProtocol, RoborockMessage, RoborockZeoProtocol +from roborock.roborock_message import ( + RoborockDyadDataProtocol, + RoborockMessage, + RoborockMessageProtocol, + RoborockZeoProtocol, +) + +_LOGGER = logging.getLogger(__name__) __init__ = [ "DyadApi", @@ -115,6 +126,7 @@ RoborockZeoProtocol.DETERGENT_TYPE: lambda val: ZeoDetergentType(val).name, RoborockZeoProtocol.SOFTENER_TYPE: lambda val: ZeoSoftenerType(val).name, RoborockZeoProtocol.SOUND_SET: lambda val: bool(val), + RoborockZeoProtocol.FEATURE_BITS: lambda val: int(val), } @@ -187,14 +199,71 @@ def on_message(message: RoborockMessage) -> None: return await self._channel.subscribe(on_message) -class ZeoApi(Trait): +class ZeoApi(Trait, TraitUpdateListener): """API for interacting with Zeo devices.""" name = "zeo" - def __init__(self, channel: MqttChannel) -> None: + def __init__(self, channel: MqttChannel, model: str | None = None) -> None: """Initialize the Zeo API.""" + TraitUpdateListener.__init__(self, _LOGGER) self._channel = channel + self._dps_cache: dict[int, Any] = {} + self._dps_unsub: Callable[[], None] | None = None + self._feature_bits: int = 0 + self._model = model + + async def start(self) -> None: + """Subscribe to MQTT push and trigger a full state sync. + + Subscribes to the DPS MQTT topic, then sends ID_QUERY with + the base DP list (including FEATURE_BITS). The device responds + with a complete state dump; subsequent changes arrive + via incremental MQTT push. + """ + await self._ensure_subscribed() + await self._force_load() + + def close(self) -> None: + """Unsubscribe from MQTT push and release resources.""" + if self._dps_unsub is not None: + self._dps_unsub() + self._dps_unsub = None + + async def _ensure_subscribed(self) -> None: + """Subscribe to MQTT DPS push (idempotent).""" + if self._dps_unsub is not None: + return + self._dps_unsub = await self._channel.subscribe(self._on_dps_message) + + async def _force_load(self) -> None: + """Send ID_QUERY with the base DP list to trigger a full state push. + + Uses ``build_force_load_dp_list()`` which selects the correct + base list (washer vs dryer) and appends conditional DPs based + on the device's series (softener, soak, smart-clean, etc.). + For devices known to lack FEATURE_BITS, the DP is excluded + from the query list and ``_feature_bits`` stays at 0. + """ + dp_list = build_force_load_dp_list(self._model) + result = await self.query_values(dp_list) + self._feature_bits = result.get(RoborockZeoProtocol.FEATURE_BITS, 0) + + def supports(self, feature: ZeoFeatureBits) -> bool: + """Check whether the device supports a given feature bit.""" + return bool(self._feature_bits & (1 << feature.value)) + + def _on_dps_message(self, message: RoborockMessage) -> None: + """Handle unsolicited MQTT push (protocol 102 — RPC_RESPONSE).""" + if message.protocol != RoborockMessageProtocol.RPC_RESPONSE: + return + try: + decoded = decode_rpc_response(message) + except RoborockException: + _LOGGER.debug("Dropped malformed push message", exc_info=True) + return + self._dps_cache.update(decoded) + self._notify_update() async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[RoborockZeoProtocol, Any]: """Query the device for the values of the given protocols.""" @@ -217,6 +286,6 @@ def create(product: HomeDataProduct, mqtt_channel: MqttChannel) -> DyadApi | Zeo case RoborockCategory.WET_DRY_VAC: return DyadApi(mqtt_channel) case RoborockCategory.WASHING_MACHINE: - return ZeoApi(mqtt_channel) + return ZeoApi(mqtt_channel, model=product.model) case _: raise NotImplementedError(f"Unsupported category {product.category}") diff --git a/roborock/devices/traits/a01/device_feature.py b/roborock/devices/traits/a01/device_feature.py new file mode 100644 index 00000000..faafa57e --- /dev/null +++ b/roborock/devices/traits/a01/device_feature.py @@ -0,0 +1,294 @@ +from __future__ import annotations + +from roborock.roborock_message import RoborockZeoProtocol + +# ── Per-series model ID frozensets ────────────────────────────────────── + +_H1_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a63", # H1 + "roborock.wm.a102", # H1 Overseas + } +) + +_H1_LITE_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a90", # H1 Lite + "roborock.wm.a91", # H1 Lite Overseas + "roborock.wm.a237", # H1 Lite Resupply + } +) + +_H1C_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a114", # H1C + "roborock.wm.a242", # H1C Overseas + } +) + +_M1_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a92", # M1 + "roborock.wm.a93", # M1 Overseas + "roborock.wm.a133", # M1Lite + "roborock.wm.a162", # M1Lite Overseas + "roborock.wm.a233", # M1Lite Plus + "roborock.wm.a234", # M1 Plus + "roborock.wm.a218", # M1 Rev2 + "roborock.wm.a213", # Medusa + "roborock.wm.a276", # M1Lite Plus Rev2 + "roborock.wm.a277", # M1Lite Rev3 + } +) + +_MUSE_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a142", # Muse + "roborock.wm.a215", # Muse Overseas + "roborock.wm.a211", # Mitty + } +) + +_METIS_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a154", # Metis + "roborock.wm.a214", # Metis Overseas + } +) + +_HYPERION_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a141", # Hyperion + "roborock.wm.a149", # HyperionPro + "roborock.wm.a207", # Hyperion Plus + "roborock.wm.a230", # Hyperion Overseas + } +) + +_POSEIDON_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a180", # Pro + "roborock.wm.a181", + "roborock.wm.a201", # Pro+ + "roborock.wm.a255", # Overseas + } +) + +_HERA_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a227", # Hera + "roborock.wm.a261", # DE + "roborock.wm.a268", # KR + "roborock.wm.a269", # TW + "roborock.wm.a273", # NO + } +) + +_PANDORA_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a239", # Pandora + "roborock.wm.a262", # DE + "roborock.wm.a270", # KR + "roborock.wm.a271", # TW + "roborock.wm.a272", # NO + } +) + +_HALIA_SERIES: frozenset[str] = frozenset( + { + "roborock.wm.a240", # Halia + "roborock.wm.a241", # Halia Lite + } +) + +_APOLLO_SERIES: frozenset[str] = frozenset( + { + "roborock.cd.a188", # Apollo Pro+ + "roborock.cd.a204", # Apollo Pro + "roborock.cd.a258", # Apollo Pro Overseas + "roborock.cd.a265", # Apollo Pro+ Overseas + } +) + +# ── Device-type frozensets ────────────────────────────────────────────── + +# All dryer models (cd.* prefix). +_DRYER_PRODUCT_IDS: frozenset[str] = _APOLLO_SERIES + +# Overseas models (supports remote control via DP 232). +_OVERSEAS_PRODUCT_IDS: frozenset[str] = frozenset( + { + "roborock.wm.a102", # H1 Overseas + "roborock.wm.a91", # H1 Lite Overseas + "roborock.wm.a93", # M1 Overseas + "roborock.wm.a162", # M1Lite Overseas + "roborock.wm.a215", # Muse Overseas + "roborock.wm.a214", # Metis Overseas + "roborock.wm.a230", # Hyperion Overseas + "roborock.wm.a242", # H1C Overseas + "roborock.wm.a255", # Poseidon Overseas + "roborock.cd.a258", # Apollo Pro Overseas + "roborock.cd.a265", # Apollo Pro+ Overseas + "roborock.wm.a261", # Hera DE + "roborock.wm.a262", # Pandora DE + } +) + +# Devices known to lack FEATURE_BITS (DP 237). +_UNSUPPORTED_FEATURE_BITS: frozenset[str] = frozenset( + { + "roborock.wm.a63", # H1 + "roborock.wm.a90", # H1 Lite + } +) + +# ── Force-load DP lists ───────────────────────────────────────────────── + +_FORCE_LOAD_BASE_WASHER: list[RoborockZeoProtocol] = [ + RoborockZeoProtocol.START, # 200 + RoborockZeoProtocol.PAUSE, # 201 + RoborockZeoProtocol.SHUTDOWN, # 202 + RoborockZeoProtocol.STATE, # 203 + RoborockZeoProtocol.MODE, # 204 + RoborockZeoProtocol.PROGRAM, # 205 + RoborockZeoProtocol.CHILD_LOCK, # 206 + RoborockZeoProtocol.TEMP, # 207 + RoborockZeoProtocol.RINSE_TIMES, # 208 + RoborockZeoProtocol.SPIN_LEVEL, # 209 + RoborockZeoProtocol.DRYING_MODE, # 210 + RoborockZeoProtocol.DETERGENT_SET, # 211 + RoborockZeoProtocol.DETERGENT_TYPE, # 213 + RoborockZeoProtocol.COUNTDOWN, # 217 + RoborockZeoProtocol.WASHING_LEFT, # 218 + RoborockZeoProtocol.DOORLOCK_STATE, # 219 + RoborockZeoProtocol.ERROR, # 220 + RoborockZeoProtocol.CUSTOM_PARAM_SAVE, # 221 + RoborockZeoProtocol.CUSTOM_PARAM_GET, # 222 + RoborockZeoProtocol.SOUND_SET, # 223 + RoborockZeoProtocol.TIMES_AFTER_CLEAN, # 224 + RoborockZeoProtocol.DETERGENT_EMPTY, # 226 + RoborockZeoProtocol.FEATURE_BITS, # 237 + RoborockZeoProtocol.PRODUCT_INFO, # 10005 + RoborockZeoProtocol.WASHING_LOG, # 10008 + RoborockZeoProtocol.OTA_NFO, # 10007 + RoborockZeoProtocol.F_C, # 10001 +] + +_FORCE_LOAD_BASE_DRYER: list[RoborockZeoProtocol] = [ + RoborockZeoProtocol.START, # 200 + RoborockZeoProtocol.PAUSE, # 201 + RoborockZeoProtocol.SHUTDOWN, # 202 + RoborockZeoProtocol.STATE, # 203 + RoborockZeoProtocol.MODE, # 204 + RoborockZeoProtocol.PROGRAM, # 205 + RoborockZeoProtocol.CHILD_LOCK, # 206 + RoborockZeoProtocol.DRYING_MODE, # 210 + RoborockZeoProtocol.COUNTDOWN, # 217 + RoborockZeoProtocol.WASHING_LEFT, # 218 + RoborockZeoProtocol.DOORLOCK_STATE, # 219 + RoborockZeoProtocol.ERROR, # 220 + RoborockZeoProtocol.CUSTOM_PARAM_GET, # 222 + RoborockZeoProtocol.SOUND_SET, # 223 + RoborockZeoProtocol.DRYING_METHOD, # 256 + RoborockZeoProtocol.STEAM_VOLUME, # 257 + RoborockZeoProtocol.FEATURE_BITS, # 237 + RoborockZeoProtocol.PRODUCT_INFO, # 10005 + RoborockZeoProtocol.WASHING_LOG, # 10008 + RoborockZeoProtocol.OTA_NFO, # 10007 + RoborockZeoProtocol.F_C, # 10001 +] + + +# ── Public helpers ────────────────────────────────────────────────────── + + +def is_dryer(model: str | None) -> bool: + """Return True if *model* belongs to a dryer (cd.*).""" + if model is None: + return False + return model in _DRYER_PRODUCT_IDS + + +def has_softener_compartment(model: str | None) -> bool: + """M1 / Muse / Metis and all dryers lack a softener compartment.""" + if model is None: + return True # conservative: assume yes + if model in _DRYER_PRODUCT_IDS: + return False + if model in _M1_SERIES | _MUSE_SERIES | _METIS_SERIES: + return False + return True + + +def supports_feature_bits(model: str | None) -> bool: + """Older entry-level devices (H1 a63, H1 Lite a90) lack DP 237.""" + if model is None: + return True # conservative: assume yes + return model not in _UNSUPPORTED_FEATURE_BITS + + +def supports_remote_control(model: str | None) -> bool: + """Remote control (DP 232) is supported on all overseas models.""" + if model is None: + return False + return model in _OVERSEAS_PRODUCT_IDS + + +def supports_soak(model: str | None) -> bool: + """Soak (DP 233) is supported on M1 / Muse / Hyperion / Poseidon / Halia / Hera / Pandora.""" + if model is None: + return False + return model in ( + _M1_SERIES | _MUSE_SERIES | _HYPERION_SERIES | _POSEIDON_SERIES | _HALIA_SERIES | _HERA_SERIES | _PANDORA_SERIES + ) + + +def supports_smart_clean(model: str | None) -> bool: + """Smart-clean (DP 239) is supported on M1 / Muse / Hyperion / Apollo / Halia / Hera.""" + if model is None: + return False + return model in (_M1_SERIES | _MUSE_SERIES | _HYPERION_SERIES | _APOLLO_SERIES | _HALIA_SERIES | _HERA_SERIES) + + +def build_force_load_dp_list(model: str | None) -> list[RoborockZeoProtocol]: + """Return the complete DP list for ``_force_load()``.""" + if is_dryer(model): + base = list(_FORCE_LOAD_BASE_DRYER) + else: + base = list(_FORCE_LOAD_BASE_WASHER) + + # ── Softener block (212, 214, 225, 227) ── + if has_softener_compartment(model): + base.extend( + [ + RoborockZeoProtocol.SOFTENER_SET, # 212 + RoborockZeoProtocol.SOFTENER_TYPE, # 214 + RoborockZeoProtocol.DEFAULT_SETTING, # 225 + RoborockZeoProtocol.SOFTENER_EMPTY, # 227 + ] + ) + + # ── Feature-bits-gated DPs (queried immediately, not deferred) ── + if supports_feature_bits(model): + # These are always appended when FEATURE_BITS is supported. + # The actual bitmask is checked later in loadFeatureDps() for + # feature-gated sub-queries, but Bundle appends these upfront. + pass # DP 237 is already in the base list. + + # ── Soak ── + if supports_soak(model): + base.append(RoborockZeoProtocol.SOAK) # 233 + + # ── Smart Clean ── + if supports_smart_clean(model): + base.append(RoborockZeoProtocol.CUSTOM_PROGRAM_CLEANING_TIME) # 239 + + # ── Remote control (overseas-only) ── + if supports_remote_control(model): + base.append(RoborockZeoProtocol.APP_AUTHORIZATION) # 232 + + # ── Strip unsupported FEATURE_BITS ── + if not supports_feature_bits(model): + base = [dp for dp in base if dp != RoborockZeoProtocol.FEATURE_BITS] + + return base diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index 82b14ee5..9fab9357 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -179,6 +179,9 @@ class RoborockZeoProtocol(RoborockEnum): SILENT_MODE_ON = 240 # rw [independent] use set_silent_mode() for bundled set SILENT_MODE_START_TIME = 241 # rw [independent] minute-of-day SILENT_MODE_END_TIME = 242 # rw [independent] minute-of-day + UNKNOWN_243 = ( + 243 # unknown, not found in plugin bundle; present in MQTT push from some devices, increments with each push + ) DRY_CARE_MODE = 244 # rw [startWith] SOFTENER_EXPANSION_TYPE = 245 # rw [independent] SMILE_LIGHT_STATUS = 247 # rw [independent] diff --git a/tests/e2e/__snapshots__/test_device_manager.ambr b/tests/e2e/__snapshots__/test_device_manager.ambr index 157f89c3..9f7967b6 100644 --- a/tests/e2e/__snapshots__/test_device_manager.ambr +++ b/tests/e2e/__snapshots__/test_device_manager.ambr @@ -13,20 +13,45 @@ [mqtt <] 00000000 90 04 00 01 00 00 |......| [mqtt >] + 00000000 30 8a 02 00 20 72 72 2f 6d 2f 69 2f 75 73 65 72 |0... rr/m/i/user| + 00000010 31 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f |123/19648f94/zeo| + 00000020 5f 64 75 69 64 00 41 30 31 00 00 23 82 00 00 23 |_duid.A01..#...#| + 00000030 83 68 a6 a2 25 00 65 00 d0 c5 de 2b f6 a9 ba 32 |.h..%.e....+...2| + 00000040 7e 6b 73 82 bb d8 67 d4 db ec 77 f5 38 85 be 1c |~ks...g...w.8...| + 00000050 32 75 45 d8 0c b8 58 37 e7 69 2f ef 54 4f d9 22 |2uE...X7.i/.TO."| + 00000060 11 9a 38 97 e7 62 a4 b0 cb fd 9e bb 3a c4 89 f2 |..8..b......:...| + 00000070 9b 21 65 9e 94 52 7e 19 0f b7 bb c7 f2 e8 3d 4d |.!e..R~.......=M| + 00000080 67 e1 ab 9a 2b f3 0e 08 e6 2a 38 81 ea 02 e0 5d |g...+....*8....]| + 00000090 da 59 e9 f6 60 b4 de 9f 53 c1 fd 5b 39 c1 a1 fe |.Y..`...S..[9...| + 000000a0 49 a6 b0 b0 72 13 38 9c 66 8e 38 37 65 2f 46 9c |I...r.8.f.87e/F.| + 000000b0 1b ca a2 fe ae 67 f5 e8 7f ed 8e 62 0c 9f fe 05 |.....g.....b....| + 000000c0 4c 85 f7 8f 50 d7 e1 02 bd 5a fd 70 59 e9 36 d5 |L...P....Z.pY.6.| + 000000d0 29 9d 5f 54 79 f9 d6 c4 fa 04 7e c7 eb 58 07 4c |)._Ty.....~..X.L| + 000000e0 53 5e 93 c8 f2 17 b2 1c 8a 14 a4 ec a6 8a 4d 7d |S^............M}| + 000000f0 89 7a 06 93 38 ec 7b 9b 7d ba 6c 12 f7 f4 5a 41 |.z..8.{.}.l...ZA| + 00000100 c3 55 ff 46 8c 9b b4 80 a7 17 1f 29 dc |.U.F.......).| + [mqtt <] + 00000000 30 5e 00 20 72 72 2f 6d 2f 6f 2f 75 73 65 72 31 |0^. rr/m/o/user1| + 00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_| + 00000020 64 75 69 64 00 00 00 00 37 41 30 31 00 00 00 00 |duid....7A01....| + 00000030 00 00 00 17 68 a6 a2 23 00 66 00 20 fe 9c 2a 27 |....h..#.f. ..*'| + 00000040 da c4 6b 9f 0e cf 2c 56 ba 5b e6 99 1f a1 29 78 |..k...,V.[....)x| + 00000050 37 37 42 66 bb d5 70 0e d4 c0 a7 d1 7f ef 8b 33 |77Bf..p........3| + [mqtt >] 00000000 30 6a 00 20 72 72 2f 6d 2f 69 2f 75 73 65 72 31 |0j. rr/m/i/user1| 00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_| - 00000020 64 75 69 64 00 41 30 31 00 00 23 82 00 00 23 83 |duid.A01..#...#.| - 00000030 68 a6 a2 24 00 65 00 30 c5 de 2b f6 a9 ba 32 7e |h..$.e.0..+...2~| - 00000040 6b 73 82 bb d8 67 d4 db 7d 80 60 67 80 96 b8 a1 |ks...g..}.`g....| - 00000050 c6 bc 9e d2 da 07 fb d3 79 f5 6f 6d 04 9c 71 00 |........y.om..q.| - 00000060 48 66 3d 7e 5d fe d3 df 18 e4 26 38 |Hf=~].....&8| + 00000020 64 75 69 64 00 41 30 31 00 00 23 84 00 00 23 85 |duid.A01..#...#.| + 00000030 68 a6 a2 26 00 65 00 30 00 5e b7 20 93 7b 53 a7 |h..&.e.0.^. .{S.| + 00000040 f9 47 d4 53 49 51 cd 59 c7 92 65 09 f3 7d 20 58 |.G.SIQ.Y..e..} X| + 00000050 c6 9e 25 54 cd 4f ab c5 fc 48 0e 67 4a 97 28 59 |..%T.O...H.gJ.(Y| + 00000060 14 a6 c6 77 39 ab 2a ef 77 d9 9d 63 |...w9.*.w..c| [mqtt <] 00000000 30 5e 00 20 72 72 2f 6d 2f 6f 2f 75 73 65 72 31 |0^. rr/m/o/user1| 00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_| 00000020 64 75 69 64 00 00 00 00 37 41 30 31 00 00 00 00 |duid....7A01....| - 00000030 00 00 00 17 68 a6 a2 23 00 66 00 20 c6 d0 06 0c |....h..#.f. ....| + 00000030 00 00 00 17 68 a6 a2 24 00 66 00 20 c6 d0 06 0c |....h..$.f. ....| 00000040 04 eb 86 8c 96 8c 51 45 4f 8e 96 93 9e 3d de 35 |......QEO....=.5| - 00000050 bb a3 92 cf 68 49 69 ba 83 25 cc 5d 77 e8 62 8a |....hIi..%.]w.b.| + 00000050 bb a3 92 cf 68 49 69 ba 83 25 cc 5d 43 da 0b 55 |....hIi..%.]C..U| # --- # name: test_l01_device [mqtt >] diff --git a/tests/e2e/test_device_manager.py b/tests/e2e/test_device_manager.py index 951c2abc..70f17679 100644 --- a/tests/e2e/test_device_manager.py +++ b/tests/e2e/test_device_manager.py @@ -527,8 +527,10 @@ async def test_a01_device( test_topic = TEST_TOPIC_FORMAT.format(duid="zeo_duid") mqtt_responses: list[bytes] = [ *MQTT_DEFAULT_RESPONSES, - # ACK the Query state call sent below. id is deterministic based on deterministic_message_fixtures - mqtt_packet.gen_publish(test_topic, mid=2, payload=response_builder.build_a01_rpc({"203": 6})), + # ACK the FEATURE_BITS query sent by _discover_features() + mqtt_packet.gen_publish(test_topic, mid=2, payload=response_builder.build_a01_rpc({"237": 1})), + # ACK the Query state call sent below + mqtt_packet.gen_publish(test_topic, mid=3, payload=response_builder.build_a01_rpc({"203": 6})), ] for response in mqtt_responses: push_mqtt_response(response)