diff --git a/roborock/data/zeo/zeo_code_mappings.py b/roborock/data/zeo/zeo_code_mappings.py index 4e5c79d16..76b8d7b8f 100644 --- a/roborock/data/zeo/zeo_code_mappings.py +++ b/roborock/data/zeo/zeo_code_mappings.py @@ -1,142 +1,320 @@ +"""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 +and ```` is the user-facing display value +(RPM, degrees Celsius, minutes, etc.). + +Enums commented out at the bottom of this file are App-internal lookup +tables and UI state machines — they are not DP protocol enums and are +never sent to the device. +""" + from ..code_mappings import RoborockEnum +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. + """ + + smart_hosting = 0 + silent_mode = 1 + new_custom_program = 2 + dry_care = 3 + set_uvc_in_appointment = 4 + detect_door_status = 5 + expand_softener = 6 + set_params_in_working = 7 + thirty_min_soak = 8 + smile_light = 9 + set_uvc_in_pause = 10 + concentrated_detergent = 11 + wool_detergent = 12 + voice_assistant = 13 + adapted_custom_program = 14 + voice_assistant_record = 15 + fluff_clean_notification = 16 + power_button_indicator_light = 17 + dirt_detection = 18 + deep_self_clean = 19 + save_panel_program_params = 20 + steam_care = 21 + wash_dry_linkage = 22 + ion_deodorization = 23 + + class ZeoMode(RoborockEnum): - null = 0 + null = 0 # (not found in app bundle) wash = 1 wash_and_dry = 2 dry = 3 + treatment = 4 class ZeoState(RoborockEnum): standby = 1 - weighing = 2 + weighing = 2 # Checking soaking = 3 washing = 4 rinsing = 5 - spinning = 6 + spinning = 6 # Dewatering drying = 7 cooling = 8 - under_delay_start = 9 - done = 10 - aftercare = 12 - waiting_for_aftercare = 13 + under_delay_start = 9 # Appointment + done = 10 # Complete + updating = 11 + aftercare = 12 # SmartHosting + waiting_for_aftercare = 13 # SmartHostingWaiting + steam_caring = 14 + descaling = 15 + cloth_ready = 16 + waiting_for_drying = 17 + pre_heating = 18 + pre_heat_complete = 19 + in_care = 20 class ZeoProgram(RoborockEnum): - null = 0 - standard = 1 + null = 0 # (not found in app bundle) + standard = 1 # Mixed quick = 2 - sanitize = 3 + sanitize = 3 # Sterilization wool = 4 - air_refresh = 5 - custom = 6 - bedding = 7 + air_refresh = 5 # Air + custom = 6 # CloudProgram + bedding = 7 # HomeTextile down = 8 silk = 9 - rinse_and_spin = 10 - spin = 11 - down_clean = 12 - baby_care = 13 - anti_allergen = 14 - sportswear = 15 + rinse_and_spin = 10 # RinseAndDehydrate + spin = 11 # Dehydrate + down_clean = 12 # SelfClean + baby_care = 13 # Baby + anti_allergen = 14 # MitesRemoval + sportswear = 15 # Sports night = 16 - new_clothes = 17 - shirts = 18 - synthetics = 19 + new_clothes = 17 # New + shirts = 18 # Shirt + synthetics = 19 # ChemicalFiber underwear = 20 - gentle = 21 - intensive = 22 - cotton_linen = 23 + gentle = 21 # Soft + intensive = 22 # Strong + cotton_linen = 23 # CottonOrLinen season = 24 - warming = 25 + warming = 25 # Warm bra = 26 - panties = 27 - boiling_wash = 28 + panties = 27 # Underpants + boiling_wash = 28 # Boiling + soaking = 29 socks = 30 - towels = 31 - anti_mite = 32 - exo_40_60 = 33 - twenty_c = 34 - t_shirts = 35 - stain_removal = 36 + towels = 31 # Towel + anti_mite = 32 # MitesRemoval2 + exo_40_60 = 33 # Eco + twenty_c = 34 # TwentyDegrees + t_shirts = 35 # TShirt + stain_removal = 36 # Dirt + small_things = 37 + mixing = 39 + bath_towel = 40 + jeans = 41 + outdoors = 42 + timed_drying = 43 + outdoor_jackets = 44 + yoga = 45 + quilt_drying = 46 + flax = 47 + suit = 48 + sport_shoes = 49 + rack_drying = 50 + summer_quilt = 51 + wind_breaker = 52 + steam_care = 53 + descaling = 54 + deep_self_clean = 55 + pet_care = 56 + small_things_drying = 57 class ZeoSoak(RoborockEnum): - normal = 0 - low = 1 - medium = 2 - high = 3 - max = 4 + normal = 0 # L0, 0min + low = 1 # L1, 5min + medium = 2 # L2, 10min + high = 3 # L3, 15min + max = 4 # L4, 20min + very_max = 5 # L5, 30min class ZeoTemperature(RoborockEnum): - normal = 1 - low = 2 - medium = 3 - high = 4 - max = 5 - twenty_c = 6 - ninety_c = 7 + normal = 1 # L1, 0 C + low = 2 # L2, 30 C + medium = 3 # L3, 40 C + high = 4 # L4, 60 C + max = 5 # L5, 90 C + twenty_c = 6 # L6, 20 C + ninety_c = 7 # L7, 95 C class ZeoRinse(RoborockEnum): - none = 0 - min = 1 - low = 2 - mid = 3 - high = 4 - max = 5 + none = 0 # L0, None + min = 1 # L1, Min + low = 2 # L2, Low + mid = 3 # L3, Mid + high = 4 # L4, High + max = 5 # L5, Max class ZeoSpin(RoborockEnum): - null = 0 - none = 1 - very_low = 2 - low = 3 - mid = 4 - high = 5 - very_high = 6 - max = 7 + null = 0 # (not found in app bundle) + none = 1 # L1, 0 RPM + very_low = 2 # L2, 400 RPM + low = 3 # L3, 600 RPM + mid = 4 # L4, 800 RPM + high = 5 # L5, 1000 RPM + very_high = 6 # L6, 1200 RPM + max = 7 # L7, 1400 RPM class ZeoDryingMode(RoborockEnum): none = 0 - quick = 1 - iron = 2 - store = 3 + quick = 1 # Quick, Mid + iron = 2 # Iron, Low + store = 3 # Store, High class ZeoDetergentType(RoborockEnum): - empty = 0 - low = 1 - medium = 2 - high = 3 + empty = 0 # T0 + low = 1 # T1 + medium = 2 # T2 + high = 3 # T3 class ZeoSoftenerType(RoborockEnum): - empty = 0 - low = 1 - medium = 2 - high = 3 + empty = 0 # T0 + low = 1 # T1 + medium = 2 # T2 + high = 3 # T3 + + +class ZeoDetergentExpansionType(RoborockEnum): + concentrated_detergent = 1 + detergent = 2 + + +class ZeoSoftenerExpansionType(RoborockEnum): + softener = 1 + softener_expansion = 2 + wool_detergent = 3 + + +class ZeoDirtDetectionStatus(RoborockEnum): + idle = 0 + detecting = 1 + detection_completed = 2 class ZeoError(RoborockEnum): - none = 0 - refill_error = 1 - drain_error = 2 - door_lock_error = 3 - water_level_error = 4 - inverter_error = 5 - heating_error = 6 - temperature_error = 7 - communication_error = 10 - drying_error = 11 - drying_error_e_12 = 12 - drying_error_e_13 = 13 - drying_error_e_14 = 14 - drying_error_e_15 = 15 - drying_error_e_16 = 16 - drying_error_water_flow = 17 # Check for normal water flow - drying_error_restart = 18 # Restart the washer and try again - spin_error = 19 # re-arrange clothes + none = 0 # No error + refill_error = 1 # Refill error (E1). Check if the water tap is turned on. + drain_error = 2 # Drain error (E2). Check the drain hose. + door_lock_error = 3 # Door lock error (E3). Close the door properly. + water_level_error = 4 # Drum water level error (E4). + inverter_error = 5 # DD motor variable-frequency drive error (E5). + heating_error = 6 # Water heater error (E6). + temperature_error = 7 # Drum water temperature error (E7). + communication_error = 10 # Communication error (E10). + drying_error = 11 # Temperature error (E11). + drying_error_e_12 = 12 # Temperature error (E12). + drying_error_e_13 = 13 # Temperature error (E13). + drying_error_e_14 = 14 # Temperature error (E14). + drying_error_e_15 = 15 # Drying air heater error (E15). + drying_error_e_16 = 16 # Fan RPM error (E16). + drying_error_water_flow = 17 # Drying temperature protection (E17). + drying_error_restart = 18 # Fan RPM error (E18). + spin_error = 19 # Balance error (Unb). + + +class ZeoDryingMethod(RoborockEnum): + l1 = 1 # L1, Saving + l2 = 2 # L2, Standard + l3 = 3 # L3, SuperFast + + +class ZeoSteamVolume(RoborockEnum): + none = 0 # L0, None + low = 1 # L1, Min + medium = 2 # L2, Low + high = 3 # L3, Mid + max = 4 # L4, High + + +class ZeoDryAndCare(RoborockEnum): + soft = 1 + normal = 2 + + +class ZeoDryerStartError(RoborockEnum): + 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. + dryer_waiting_hosting = 4 # Dryer is waiting for smart hosting. + dryer_smart_hosting = 5 # Dryer is in smart hosting mode. + dryer_countdown = 6 # Dryer is in preset countdown. + dryer_network_fail = 7 # Please check the dryer network connection. + + +# The following are App-internal lookup tables extracted from the bundle. +# They are NOT DP protocol enums — the device never receives these values. +# They control how the official app adjusts recommended dosages or UI visibility. +# +# class Cleanser(RoborockEnum): +# detergent = 0 +# additions = 1 +# +# class Detergents(RoborockEnum): +# concentrated = 1 +# regular = 2 +# baby = 3 +# +# class Additions(RoborockEnum): +# softener = 1 +# disinfectant = 2 +# fragrance = 3 +# +# The following are App UI state enums — internal state machines used by the +# official app for page rendering and progress display. +# +# class HomePageStatus(RoborockEnum): +# updating = 0 +# loading = 1 +# load_failed = 2 +# idle = 3 +# working = 4 +# smart_hosting = 5 +# preset = 6 +# descaling = 7 +# cloth_ready = 8 +# wait_to_dry = 9 +# +# class Progress(RoborockEnum): +# soak = 0 +# wash = 1 +# rinse = 2 +# spin = 3 +# dry = 4 +# steam_care = 5 +# +# class ProgressStatus(RoborockEnum): +# done = 0 +# doing = 1 +# will_do = 2 +# +# class SmartHostStatus(RoborockEnum): +# smart_host_waiting = 0 +# smart_hosting = 1 diff --git a/roborock/data/zeo/zeo_containers.py b/roborock/data/zeo/zeo_containers.py index e69de29bb..5711cc1f9 100644 --- a/roborock/data/zeo/zeo_containers.py +++ b/roborock/data/zeo/zeo_containers.py @@ -0,0 +1,140 @@ +"""Data containers for Zeo (washing machine / dryer) devices.""" + +from dataclasses import dataclass + +from ..containers import RoborockBase +from .zeo_code_mappings import ( + ZeoDryAndCare, + ZeoDryingMethod, + ZeoDryingMode, + ZeoMode, + ZeoProgram, + ZeoRinse, + ZeoSoak, + ZeoSpin, + ZeoSteamVolume, + ZeoTemperature, +) + + +@dataclass +class ZeoStartParams(RoborockBase): + """Parameters that must be bundled with a START command. + + All Zeo devices require ``mode`` and ``program`` to be sent together + with the start signal. The remaining fields are optional and only + included when the device reports a non-None value. + """ + + mode: ZeoMode + program: ZeoProgram + temperature: ZeoTemperature | None = None + rinse: ZeoRinse | None = None + spin: ZeoSpin | None = None + drying_mode: ZeoDryingMode | None = None + + +# ── DP 222 (LoadCloudProgram) bitfield decoder ────────────────────────── +# The official app packs all custom-program parameters into a single +# 32-bit integer at DP 222. This mirrors WasherDpsCache.customMode in +# module 725 of the React Native plugin bundle. + + +@dataclass +class ZeoCustomMode(RoborockBase): + """Decoded custom programme parameters from DP 222 (LoadCloudProgram). + + Null / absent fields are represented as ``0`` which matches the + official app's behaviour (the right-shifted-and-masked value is + always non-negative). + """ + + program: ZeoProgram + """Wash program (bits 0-7).""" + + mode: ZeoMode + """Wash mode (bits 8-9).""" + + temperature: ZeoTemperature + """Temperature (bits 10-12).""" + + rinse: ZeoRinse + """Rinse cycle (bits 13-15).""" + + spin: ZeoSpin + """Spin speed (bits 16-18).""" + + drying_mode: ZeoDryingMode + """Drying mode (bits 19-21).""" + + soak: ZeoSoak + """Soak (bits 22-24).""" + + dry_and_care: ZeoDryAndCare + """Dry-care mode (bits 25-27).""" + + steam_volume: ZeoSteamVolume + """Steam volume (bits 28-30).""" + + total_time_min: int = 0 + """Total programme time in minutes (from DP 239).""" + + @classmethod + def from_raw(cls, raw: int, total_time_min: int | None = None) -> "ZeoCustomMode": + """Decode a raw 32-bit custom-programme value.""" + return cls( + program=ZeoProgram(raw & 0xFF), + mode=ZeoMode((raw >> 8) & 0x3), + temperature=ZeoTemperature((raw >> 10) & 0x7), + rinse=ZeoRinse((raw >> 13) & 0x7), + spin=ZeoSpin((raw >> 16) & 0x7), + drying_mode=ZeoDryingMode((raw >> 19) & 0x7), + soak=ZeoSoak((raw >> 22) & 0x7), + dry_and_care=ZeoDryAndCare((raw >> 25) & 0x7), + steam_volume=ZeoSteamVolume((raw >> 28) & 0x7), + total_time_min=total_time_min or 0, + ) + + +@dataclass +class ZeoDryerCustomMode(RoborockBase): + """Decoded custom programme from DP 222 for a standalone dryer. + + Dryers pack a different (shorter) bitfield than washers — only 5 + fields after program/mode. Mirrors ``WasherDpsCache.dryerCustomMode`` + in module 725 of the plugin bundle. + """ + + program: ZeoProgram + """Drying program (bits 0-7).""" + + mode: ZeoMode + """Drying mode (bits 8-10).""" + + drying_mode: ZeoDryingMode + """Drying level (bits 11-13).""" + + drying_method: ZeoDryingMethod + """Drying method (bits 14-16).""" + + steam_volume: ZeoSteamVolume + """Steam volume (bits 17-19).""" + + total_time_min: int = 0 + """Total programme time in minutes (from DP 239).""" + + @classmethod + def from_raw(cls, raw: int, total_time_min: int | None = None) -> "ZeoDryerCustomMode": + """Decode a raw 32-bit dryer custom-programme value.""" + return cls( + program=ZeoProgram(raw & 0xFF), + # Dryer mode spans bits 8-10 (0x700, 3 bits) because the + # temperature field is absent from the dryer bitfield and + # bit 10 is re-allocated to mode. Washer uses 2 bits + # (0x300, bits 8-9) to make room for temperature at 10-12. + mode=ZeoMode((raw >> 8) & 0x7), + drying_mode=ZeoDryingMode((raw >> 11) & 0x7), + drying_method=ZeoDryingMethod((raw >> 14) & 0x7), + steam_volume=ZeoSteamVolume((raw >> 17) & 0x7), + total_time_min=total_time_min or 0, + ) diff --git a/roborock/devices/device.py b/roborock/devices/device.py index f6226f07a..eaf2d6306 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() + if 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 537c84377..ad15ef646 100644 --- a/roborock/devices/traits/a01/__init__.py +++ b/roborock/devices/traits/a01/__init__.py @@ -20,6 +20,7 @@ """ import json +import logging from collections.abc import Callable from datetime import time from typing import Any @@ -40,6 +41,7 @@ ZeoDetergentType, ZeoDryingMode, ZeoError, + ZeoFeatureBits, ZeoMode, ZeoProgram, ZeoRinse, @@ -50,8 +52,18 @@ ) from roborock.devices.rpc.a01_channel import send_decoded_command from roborock.devices.traits import Trait +from roborock.devices.traits.common import TraitUpdateListener from roborock.devices.transport.mqtt_channel import MqttChannel -from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol +from roborock.exceptions import RoborockException +from roborock.protocols.a01_protocol import decode_rpc_response +from roborock.roborock_message import ( + RoborockDyadDataProtocol, + RoborockMessage, + RoborockMessageProtocol, + RoborockZeoProtocol, +) + +_LOGGER = logging.getLogger(__name__) __init__ = [ "DyadApi", @@ -156,14 +168,74 @@ async def set_value(self, protocol: RoborockDyadDataProtocol, value: Any) -> dic return await send_decoded_command(self._channel, params) -class ZeoApi(Trait): +class ZeoApi(Trait, TraitUpdateListener): """API for interacting with Zeo devices.""" name = "zeo" def __init__(self, channel: MqttChannel) -> 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 + + async def start(self) -> None: + """Subscribe to MQTT push and discover device features. + + Subscribes to the DPS MQTT topic, then queries FEATURE_BITS + (DP 237) to wake the device and cache supported capabilities. + """ + await self._ensure_subscribed() + await self._discover_features() + + 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 _discover_features(self) -> None: + """Query FEATURE_BITS to wake the device and cache capabilities. + + Sending an RPC query after subscribing triggers the device to + start pushing its full state — equivalent to how V1's + ``discover_features()`` uses ``device_features.refresh()`` to + initiate the push cycle. + """ + try: + result = await self.query_values([RoborockZeoProtocol.FEATURE_BITS]) + self._feature_bits = result.get(RoborockZeoProtocol.FEATURE_BITS, 0) + except Exception: + self._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). + + Zeo devices broadcast status changes as ``{"dps": {...}}`` JSON + payloads. This callback decodes them and feeds the cache so + that ``query_values`` can skip the device round-trip when the + requested DPs are already up to date. + """ + if message.protocol != RoborockMessageProtocol.RPC_RESPONSE: + return + try: + decoded = decode_rpc_response(message) + self._dps_cache.update(decoded) + self._notify_update() + except RoborockException: + _LOGGER.debug("Failed to decode push message, skipping: %s", message, exc_info=True) async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[RoborockZeoProtocol, Any]: """Query the device for the values of the given protocols.""" @@ -172,6 +244,9 @@ async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[Robor {RoborockZeoProtocol.ID_QUERY: protocols}, value_encoder=json.dumps, ) + for protocol, value in response.items(): + if value is not None: + self._dps_cache[int(protocol)] = value return {protocol: convert_zeo_value(protocol, response.get(protocol)) for protocol in protocols} async def set_value(self, protocol: RoborockZeoProtocol, value: Any) -> dict[RoborockZeoProtocol, Any]: diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index b78ce5bab..9fab9357a 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -125,45 +125,101 @@ class RoborockDyadDataProtocol(RoborockEnum): class RoborockZeoProtocol(RoborockEnum): - START = 200 # rw + """Zeo device Data Point protocol IDs (200-266) and meta/RPC commands (10000+). + + Comment tags: + ro — read-only + rw — read-write + wo — write-only + [startWith] — must be bundled with START via start() + [independent] — immediate effect, works via set_value() + """ + + # ── DP 200-266 ───────────────────────────────────────────────────── + START = 200 # rw [action → start()] PAUSE = 201 # rw SHUTDOWN = 202 # rw STATE = 203 # ro - MODE = 204 # rw - PROGRAM = 205 # rw - CHILD_LOCK = 206 # rw - TEMP = 207 # rw - RINSE_TIMES = 208 # rw - SPIN_LEVEL = 209 # rw - DRYING_MODE = 210 # rw - DETERGENT_SET = 211 # rw - SOFTENER_SET = 212 # rw - DETERGENT_TYPE = 213 # rw - SOFTENER_TYPE = 214 # rw - COUNTDOWN = 217 # rw + MODE = 204 # rw [startWith] + PROGRAM = 205 # rw [startWith] + CHILD_LOCK = 206 # rw [independent] + TEMP = 207 # rw [startWith] + RINSE_TIMES = 208 # rw [startWith] + SPIN_LEVEL = 209 # rw [startWith] + DRYING_MODE = 210 # rw [startWith] + DETERGENT_SET = 211 # rw [independent] + SOFTENER_SET = 212 # rw [independent] + DETERGENT_TYPE = 213 # rw [independent] + SOFTENER_TYPE = 214 # rw [independent] + DIRT_DETECTION_SWITCH = 215 # rw [independent] + DIRT_DETECTION_STATUS = 216 # ro + COUNTDOWN = 217 # rw [independent] also used in start_with_preset() WASHING_LEFT = 218 # ro DOORLOCK_STATE = 219 # ro ERROR = 220 # ro - CUSTOM_PARAM_SAVE = 221 # rw - CUSTOM_PARAM_GET = 222 # ro - SOUND_SET = 223 # rw + CUSTOM_PARAM_SAVE = 221 # rw [independent] see save_cloud_program() + CUSTOM_PARAM_GET = 222 # rw [independent] read via get_custom_mode(), write via load_cloud_program() + SOUND_SET = 223 # rw [independent] TIMES_AFTER_CLEAN = 224 # ro - DEFAULT_SETTING = 225 # rw + DEFAULT_SETTING = 225 # rw [independent] DETERGENT_EMPTY = 226 # ro SOFTENER_EMPTY = 227 # ro - LIGHT_SETTING = 229 # rw - DETERGENT_VOLUME = 230 # rw - SOFTENER_VOLUME = 231 # rw - APP_AUTHORIZATION = 232 # rw - ID_QUERY = 10000 - F_C = 10001 - SND_STATE = 10004 - PRODUCT_INFO = 10005 - PRIVACY_INFO = 10006 - OTA_NFO = 10007 - WASHING_LOG = 10008 - RPC_REQ = 10101 - RPC_RESp = 10102 + UV_LIGHT = 228 # rw [independent] + LIGHT_SETTING = 229 # rw [independent] server schema only, not found in bundle + DETERGENT_VOLUME = 230 # rw [independent] server schema only, not found in bundle + SOFTENER_VOLUME = 231 # rw [independent] server schema only, not found in bundle + APP_AUTHORIZATION = 232 # ro + SOAK = 233 # rw [startWith] + TOTAL_TIME = 234 # ro used in dryer startWith payload + SMART_HOSTING = 235 # rw [independent] + SMART_HOSTING_TIME = 236 # ro + FEATURE_BITS = 237 # ro decoded by ZeoFeatureBits + SMART_HOSTING_WAITED_TIME = 238 # ro + CUSTOM_PROGRAM_CLEANING_TIME = 239 # ro + 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] + DETERGENT_EXPANSION_TYPE = 248 # rw [independent] + FLUFF_CLEANED = 249 # rw [independent] + IS_NEED_FLUFF_CLEAN = 250 # ro + POWER_LIGHT = 251 # rw [independent] + PANEL_PROGRAM_PARAMS_SET = 252 # rw [independent] + PANEL_PROGRAM_PARAMS_SET_RESULT = 253 # ro + SAVE_ADAPTED_CLOUD_PROGRAM = 254 # rw [independent] + WASH_DRY_LINKED = 255 # rw [startWith / feature-gated] + DRYING_METHOD = 256 # rw [startWith] + STEAM_VOLUME = 257 # rw [startWith] + ION_DEODORIZATION = 258 # rw [startWith / feature-gated] + PANEL_TIMING_PROGRAM_PARAMS = 260 # ro + STEAM_CARE_TIME = 261 # ro + DEVICE_BOUND = 262 # ro + CLOTH_PUT_IN = 263 # ro + CLOTH_READY_TO_DRY_COUNT_DOWN = 264 # ro + START_DRYER_ERROR = 265 # ro + WIFI_LINKAGE_RESET = 266 # rw [independent] + + # ── Meta / RPC / Voice (10000+) ──────────────────────────────────── + ID_QUERY = 10000 # -- multi-DP query request (not a device DP) + F_C = 10001 # ro query via checkFCCState() + SET_SOUND_PACKAGE = 10003 # wo setSoundPackage(JSON) + SND_STATE = 10004 # ro query via updateSoundPackageInfo() + PRODUCT_INFO = 10005 # ro query via loadGeneralInfo() (10s timeout) + PRIVACY_INFO = 10006 # wo syncPrivacyToDevice(agreed) + OTA_NFO = 10007 # ro forceLoad only + WASHING_LOG = 10008 # ro forceLoad only, JSON + VOICE_VOLUME = 10009 # wo [independent] setVoiceVolume(int) → JSON + RPC_REQUEST = 10101 # wo rpcRequest(method) → JSON + RPC_RESPONSE = 10102 # -- MQTT push protocol 102, not a device DP + VOICE_SWITCH = 10301 # wo [independent] setVoiceSwitchStatus(bool) → JSON + VOICE_RECORD_INFO = 10302 # ro cache-derived, auto JSON decoded + VOICE_RECORD = 10303 # ro query via getVoiceControlRecord(), JSON + VOICE_RECORD_DELETE = 10304 # wo [independent] deleteVoiceControlRecord(id) → JSON class RoborockB01Protocol(RoborockEnum): diff --git a/tests/e2e/__snapshots__/test_device_manager.ambr b/tests/e2e/__snapshots__/test_device_manager.ambr index 157f89c38..b13ebad91 100644 --- a/tests/e2e/__snapshots__/test_device_manager.ambr +++ b/tests/e2e/__snapshots__/test_device_manager.ambr @@ -16,17 +16,32 @@ 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| + 00000030 68 a6 a2 25 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 a3 e2 16 16 7e 83 5f |ks...g..}....~._| + 00000050 bb 3d 2b 79 6b d0 52 9b 60 17 2d f4 06 3b a8 66 |.=+yk.R.`.-..;.f| + 00000060 f7 20 c0 6a c2 94 1e 84 f8 91 41 67 |. .j......Ag| [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 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 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 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 951c2abc2..70f17679f 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)