roborock.devices.traits.a01

Create traits for A01 devices.

This module provides the API implementations for A01 protocol devices, which include Dyad (Wet/Dry Vacuums) and Zeo (Washing Machines).

Using A01 APIs

A01 devices expose a single API object that handles all device interactions. This API is available on the device instance (typically via device.a01_properties).

The API provides two main methods:

  1. query_values(protocols): Fetches current state for specific data points. You must pass a list of protocol enums (e.g. RoborockDyadDataProtocol or RoborockZeoProtocol) to request specific data.
  2. set_value(protocol, value): Sends a command to the device to change a setting or perform an action.

Note that these APIs fetch data directly from the device upon request and do not cache state internally.

  1"""Create traits for A01 devices.
  2
  3This module provides the API implementations for A01 protocol devices, which include
  4Dyad (Wet/Dry Vacuums) and Zeo (Washing Machines).
  5
  6Using A01 APIs
  7--------------
  8A01 devices expose a single API object that handles all device interactions. This API is
  9available on the device instance (typically via `device.a01_properties`).
 10
 11The API provides two main methods:
 121.  **query_values(protocols)**: Fetches current state for specific data points.
 13    You must pass a list of protocol enums (e.g. `RoborockDyadDataProtocol` or
 14    `RoborockZeoProtocol`) to request specific data.
 152.  **set_value(protocol, value)**: Sends a command to the device to change a setting
 16    or perform an action.
 17
 18Note that these APIs fetch data directly from the device upon request and do not
 19cache state internally.
 20"""
 21
 22import json
 23from collections.abc import Callable
 24from datetime import time
 25from typing import Any
 26
 27from roborock.data import DyadProductInfo, DyadSndState, HomeDataProduct, RoborockCategory
 28from roborock.data.dyad.dyad_code_mappings import (
 29    DyadBrushSpeed,
 30    DyadCleanMode,
 31    DyadError,
 32    DyadSelfCleanLevel,
 33    DyadSelfCleanMode,
 34    DyadSuction,
 35    DyadWarmLevel,
 36    DyadWaterLevel,
 37    RoborockDyadStateCode,
 38)
 39from roborock.data.zeo.zeo_code_mappings import (
 40    ZeoDetergentType,
 41    ZeoDryingMode,
 42    ZeoError,
 43    ZeoMode,
 44    ZeoProgram,
 45    ZeoRinse,
 46    ZeoSoftenerType,
 47    ZeoSpin,
 48    ZeoState,
 49    ZeoTemperature,
 50)
 51from roborock.devices.a01_channel import send_decoded_command
 52from roborock.devices.mqtt_channel import MqttChannel
 53from roborock.devices.traits import Trait
 54from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
 55
 56__init__ = [
 57    "DyadApi",
 58    "ZeoApi",
 59]
 60
 61
 62DYAD_PROTOCOL_ENTRIES: dict[RoborockDyadDataProtocol, Callable] = {
 63    RoborockDyadDataProtocol.STATUS: lambda val: RoborockDyadStateCode(val).name,
 64    RoborockDyadDataProtocol.SELF_CLEAN_MODE: lambda val: DyadSelfCleanMode(val).name,
 65    RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: lambda val: DyadSelfCleanLevel(val).name,
 66    RoborockDyadDataProtocol.WARM_LEVEL: lambda val: DyadWarmLevel(val).name,
 67    RoborockDyadDataProtocol.CLEAN_MODE: lambda val: DyadCleanMode(val).name,
 68    RoborockDyadDataProtocol.SUCTION: lambda val: DyadSuction(val).name,
 69    RoborockDyadDataProtocol.WATER_LEVEL: lambda val: DyadWaterLevel(val).name,
 70    RoborockDyadDataProtocol.BRUSH_SPEED: lambda val: DyadBrushSpeed(val).name,
 71    RoborockDyadDataProtocol.POWER: lambda val: int(val),
 72    RoborockDyadDataProtocol.AUTO_DRY: lambda val: bool(val),
 73    RoborockDyadDataProtocol.MESH_LEFT: lambda val: int(360000 - val * 60),
 74    RoborockDyadDataProtocol.BRUSH_LEFT: lambda val: int(360000 - val * 60),
 75    RoborockDyadDataProtocol.ERROR: lambda val: DyadError(val).name,
 76    RoborockDyadDataProtocol.VOLUME_SET: lambda val: int(val),
 77    RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: lambda val: bool(val),
 78    RoborockDyadDataProtocol.AUTO_DRY_MODE: lambda val: bool(val),
 79    RoborockDyadDataProtocol.SILENT_DRY_DURATION: lambda val: int(val),  # in minutes
 80    RoborockDyadDataProtocol.SILENT_MODE: lambda val: bool(val),
 81    RoborockDyadDataProtocol.SILENT_MODE_START_TIME: lambda val: time(
 82        hour=int(val / 60), minute=val % 60
 83    ),  # in minutes since 00:00
 84    RoborockDyadDataProtocol.SILENT_MODE_END_TIME: lambda val: time(
 85        hour=int(val / 60), minute=val % 60
 86    ),  # in minutes since 00:00
 87    RoborockDyadDataProtocol.RECENT_RUN_TIME: lambda val: [
 88        int(v) for v in val.split(",")
 89    ],  # minutes of cleaning in past few days.
 90    RoborockDyadDataProtocol.TOTAL_RUN_TIME: lambda val: int(val),
 91    RoborockDyadDataProtocol.SND_STATE: lambda val: DyadSndState.from_dict(val),
 92    RoborockDyadDataProtocol.PRODUCT_INFO: lambda val: DyadProductInfo.from_dict(val),
 93}
 94
 95ZEO_PROTOCOL_ENTRIES: dict[RoborockZeoProtocol, Callable] = {
 96    # read-only
 97    RoborockZeoProtocol.STATE: lambda val: ZeoState(val).name,
 98    RoborockZeoProtocol.COUNTDOWN: lambda val: int(val),
 99    RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val),
100    RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name,
101    RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val),
102    RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val),
103    RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val),
104    # read-write
105    RoborockZeoProtocol.MODE: lambda val: ZeoMode(val).name,
106    RoborockZeoProtocol.PROGRAM: lambda val: ZeoProgram(val).name,
107    RoborockZeoProtocol.TEMP: lambda val: ZeoTemperature(val).name,
108    RoborockZeoProtocol.RINSE_TIMES: lambda val: ZeoRinse(val).name,
109    RoborockZeoProtocol.SPIN_LEVEL: lambda val: ZeoSpin(val).name,
110    RoborockZeoProtocol.DRYING_MODE: lambda val: ZeoDryingMode(val).name,
111    RoborockZeoProtocol.DETERGENT_TYPE: lambda val: ZeoDetergentType(val).name,
112    RoborockZeoProtocol.SOFTENER_TYPE: lambda val: ZeoSoftenerType(val).name,
113    RoborockZeoProtocol.SOUND_SET: lambda val: bool(val),
114}
115
116
117def convert_dyad_value(protocol_value: RoborockDyadDataProtocol, value: Any) -> Any:
118    """Convert a dyad protocol value to its corresponding type."""
119    if (converter := DYAD_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
120        try:
121            return converter(value)
122        except (ValueError, TypeError):
123            return None
124    return None
125
126
127def convert_zeo_value(protocol_value: RoborockZeoProtocol, value: Any) -> Any:
128    """Convert a zeo protocol value to its corresponding type."""
129    if (converter := ZEO_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
130        try:
131            return converter(value)
132        except (ValueError, TypeError):
133            return None
134    return None
135
136
137class DyadApi(Trait):
138    """API for interacting with Dyad devices."""
139
140    def __init__(self, channel: MqttChannel) -> None:
141        """Initialize the Dyad API."""
142        self._channel = channel
143
144    async def query_values(self, protocols: list[RoborockDyadDataProtocol]) -> dict[RoborockDyadDataProtocol, Any]:
145        """Query the device for the values of the given Dyad protocols."""
146        response = await send_decoded_command(
147            self._channel,
148            {RoborockDyadDataProtocol.ID_QUERY: protocols},
149            value_encoder=json.dumps,
150        )
151        return {protocol: convert_dyad_value(protocol, response.get(protocol)) for protocol in protocols}
152
153    async def set_value(self, protocol: RoborockDyadDataProtocol, value: Any) -> dict[RoborockDyadDataProtocol, Any]:
154        """Set a value for a specific protocol on the device."""
155        params = {protocol: value}
156        return await send_decoded_command(self._channel, params)
157
158
159class ZeoApi(Trait):
160    """API for interacting with Zeo devices."""
161
162    name = "zeo"
163
164    def __init__(self, channel: MqttChannel) -> None:
165        """Initialize the Zeo API."""
166        self._channel = channel
167
168    async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[RoborockZeoProtocol, Any]:
169        """Query the device for the values of the given protocols."""
170        response = await send_decoded_command(
171            self._channel,
172            {RoborockZeoProtocol.ID_QUERY: protocols},
173            value_encoder=json.dumps,
174        )
175        return {protocol: convert_zeo_value(protocol, response.get(protocol)) for protocol in protocols}
176
177    async def set_value(self, protocol: RoborockZeoProtocol, value: Any) -> dict[RoborockZeoProtocol, Any]:
178        """Set a value for a specific protocol on the device."""
179        params = {protocol: value}
180        return await send_decoded_command(self._channel, params, value_encoder=lambda x: x)
181
182
183def create(product: HomeDataProduct, mqtt_channel: MqttChannel) -> DyadApi | ZeoApi:
184    """Create traits for A01 devices."""
185    match product.category:
186        case RoborockCategory.WET_DRY_VAC:
187            return DyadApi(mqtt_channel)
188        case RoborockCategory.WASHING_MACHINE:
189            return ZeoApi(mqtt_channel)
190        case _:
191            raise NotImplementedError(f"Unsupported category {product.category}")
DYAD_PROTOCOL_ENTRIES: dict[roborock.roborock_message.RoborockDyadDataProtocol, Callable] = {<RoborockDyadDataProtocol.STATUS: 201>: <function <lambda>>, <RoborockDyadDataProtocol.SELF_CLEAN_MODE: 202>: <function <lambda>>, <RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: 203>: <function <lambda>>, <RoborockDyadDataProtocol.WARM_LEVEL: 204>: <function <lambda>>, <RoborockDyadDataProtocol.CLEAN_MODE: 205>: <function <lambda>>, <RoborockDyadDataProtocol.SUCTION: 206>: <function <lambda>>, <RoborockDyadDataProtocol.WATER_LEVEL: 207>: <function <lambda>>, <RoborockDyadDataProtocol.BRUSH_SPEED: 208>: <function <lambda>>, <RoborockDyadDataProtocol.POWER: 209>: <function <lambda>>, <RoborockDyadDataProtocol.AUTO_DRY: 213>: <function <lambda>>, <RoborockDyadDataProtocol.MESH_LEFT: 214>: <function <lambda>>, <RoborockDyadDataProtocol.BRUSH_LEFT: 215>: <function <lambda>>, <RoborockDyadDataProtocol.ERROR: 216>: <function <lambda>>, <RoborockDyadDataProtocol.VOLUME_SET: 221>: <function <lambda>>, <RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: 222>: <function <lambda>>, <RoborockDyadDataProtocol.AUTO_DRY_MODE: 224>: <function <lambda>>, <RoborockDyadDataProtocol.SILENT_DRY_DURATION: 225>: <function <lambda>>, <RoborockDyadDataProtocol.SILENT_MODE: 226>: <function <lambda>>, <RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 227>: <function <lambda>>, <RoborockDyadDataProtocol.SILENT_MODE_END_TIME: 228>: <function <lambda>>, <RoborockDyadDataProtocol.RECENT_RUN_TIME: 229>: <function <lambda>>, <RoborockDyadDataProtocol.TOTAL_RUN_TIME: 230>: <function <lambda>>, <RoborockDyadDataProtocol.SND_STATE: 10004>: <function <lambda>>, <RoborockDyadDataProtocol.PRODUCT_INFO: 10005>: <function <lambda>>}
ZEO_PROTOCOL_ENTRIES: dict[roborock.roborock_message.RoborockZeoProtocol, Callable] = {<RoborockZeoProtocol.STATE: 203>: <function <lambda>>, <RoborockZeoProtocol.COUNTDOWN: 217>: <function <lambda>>, <RoborockZeoProtocol.WASHING_LEFT: 218>: <function <lambda>>, <RoborockZeoProtocol.ERROR: 220>: <function <lambda>>, <RoborockZeoProtocol.TIMES_AFTER_CLEAN: 224>: <function <lambda>>, <RoborockZeoProtocol.DETERGENT_EMPTY: 226>: <function <lambda>>, <RoborockZeoProtocol.SOFTENER_EMPTY: 227>: <function <lambda>>, <RoborockZeoProtocol.MODE: 204>: <function <lambda>>, <RoborockZeoProtocol.PROGRAM: 205>: <function <lambda>>, <RoborockZeoProtocol.TEMP: 207>: <function <lambda>>, <RoborockZeoProtocol.RINSE_TIMES: 208>: <function <lambda>>, <RoborockZeoProtocol.SPIN_LEVEL: 209>: <function <lambda>>, <RoborockZeoProtocol.DRYING_MODE: 210>: <function <lambda>>, <RoborockZeoProtocol.DETERGENT_TYPE: 213>: <function <lambda>>, <RoborockZeoProtocol.SOFTENER_TYPE: 214>: <function <lambda>>, <RoborockZeoProtocol.SOUND_SET: 223>: <function <lambda>>}
def convert_dyad_value( protocol_value: roborock.roborock_message.RoborockDyadDataProtocol, value: Any) -> Any:
118def convert_dyad_value(protocol_value: RoborockDyadDataProtocol, value: Any) -> Any:
119    """Convert a dyad protocol value to its corresponding type."""
120    if (converter := DYAD_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
121        try:
122            return converter(value)
123        except (ValueError, TypeError):
124            return None
125    return None

Convert a dyad protocol value to its corresponding type.

def convert_zeo_value( protocol_value: roborock.roborock_message.RoborockZeoProtocol, value: Any) -> Any:
128def convert_zeo_value(protocol_value: RoborockZeoProtocol, value: Any) -> Any:
129    """Convert a zeo protocol value to its corresponding type."""
130    if (converter := ZEO_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
131        try:
132            return converter(value)
133        except (ValueError, TypeError):
134            return None
135    return None

Convert a zeo protocol value to its corresponding type.

class DyadApi(roborock.devices.traits.Trait):
138class DyadApi(Trait):
139    """API for interacting with Dyad devices."""
140
141    def __init__(self, channel: MqttChannel) -> None:
142        """Initialize the Dyad API."""
143        self._channel = channel
144
145    async def query_values(self, protocols: list[RoborockDyadDataProtocol]) -> dict[RoborockDyadDataProtocol, Any]:
146        """Query the device for the values of the given Dyad protocols."""
147        response = await send_decoded_command(
148            self._channel,
149            {RoborockDyadDataProtocol.ID_QUERY: protocols},
150            value_encoder=json.dumps,
151        )
152        return {protocol: convert_dyad_value(protocol, response.get(protocol)) for protocol in protocols}
153
154    async def set_value(self, protocol: RoborockDyadDataProtocol, value: Any) -> dict[RoborockDyadDataProtocol, Any]:
155        """Set a value for a specific protocol on the device."""
156        params = {protocol: value}
157        return await send_decoded_command(self._channel, params)

API for interacting with Dyad devices.

DyadApi(channel: roborock.devices.mqtt_channel.MqttChannel)
141    def __init__(self, channel: MqttChannel) -> None:
142        """Initialize the Dyad API."""
143        self._channel = channel

Initialize the Dyad API.

async def query_values( self, protocols: list[roborock.roborock_message.RoborockDyadDataProtocol]) -> dict[roborock.roborock_message.RoborockDyadDataProtocol, typing.Any]:
145    async def query_values(self, protocols: list[RoborockDyadDataProtocol]) -> dict[RoborockDyadDataProtocol, Any]:
146        """Query the device for the values of the given Dyad protocols."""
147        response = await send_decoded_command(
148            self._channel,
149            {RoborockDyadDataProtocol.ID_QUERY: protocols},
150            value_encoder=json.dumps,
151        )
152        return {protocol: convert_dyad_value(protocol, response.get(protocol)) for protocol in protocols}

Query the device for the values of the given Dyad protocols.

async def set_value( self, protocol: roborock.roborock_message.RoborockDyadDataProtocol, value: Any) -> dict[roborock.roborock_message.RoborockDyadDataProtocol, typing.Any]:
154    async def set_value(self, protocol: RoborockDyadDataProtocol, value: Any) -> dict[RoborockDyadDataProtocol, Any]:
155        """Set a value for a specific protocol on the device."""
156        params = {protocol: value}
157        return await send_decoded_command(self._channel, params)

Set a value for a specific protocol on the device.

class ZeoApi(roborock.devices.traits.Trait):
160class ZeoApi(Trait):
161    """API for interacting with Zeo devices."""
162
163    name = "zeo"
164
165    def __init__(self, channel: MqttChannel) -> None:
166        """Initialize the Zeo API."""
167        self._channel = channel
168
169    async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[RoborockZeoProtocol, Any]:
170        """Query the device for the values of the given protocols."""
171        response = await send_decoded_command(
172            self._channel,
173            {RoborockZeoProtocol.ID_QUERY: protocols},
174            value_encoder=json.dumps,
175        )
176        return {protocol: convert_zeo_value(protocol, response.get(protocol)) for protocol in protocols}
177
178    async def set_value(self, protocol: RoborockZeoProtocol, value: Any) -> dict[RoborockZeoProtocol, Any]:
179        """Set a value for a specific protocol on the device."""
180        params = {protocol: value}
181        return await send_decoded_command(self._channel, params, value_encoder=lambda x: x)

API for interacting with Zeo devices.

ZeoApi(channel: roborock.devices.mqtt_channel.MqttChannel)
165    def __init__(self, channel: MqttChannel) -> None:
166        """Initialize the Zeo API."""
167        self._channel = channel

Initialize the Zeo API.

name = 'zeo'
async def query_values( self, protocols: list[roborock.roborock_message.RoborockZeoProtocol]) -> dict[roborock.roborock_message.RoborockZeoProtocol, typing.Any]:
169    async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[RoborockZeoProtocol, Any]:
170        """Query the device for the values of the given protocols."""
171        response = await send_decoded_command(
172            self._channel,
173            {RoborockZeoProtocol.ID_QUERY: protocols},
174            value_encoder=json.dumps,
175        )
176        return {protocol: convert_zeo_value(protocol, response.get(protocol)) for protocol in protocols}

Query the device for the values of the given protocols.

async def set_value( self, protocol: roborock.roborock_message.RoborockZeoProtocol, value: Any) -> dict[roborock.roborock_message.RoborockZeoProtocol, typing.Any]:
178    async def set_value(self, protocol: RoborockZeoProtocol, value: Any) -> dict[RoborockZeoProtocol, Any]:
179        """Set a value for a specific protocol on the device."""
180        params = {protocol: value}
181        return await send_decoded_command(self._channel, params, value_encoder=lambda x: x)

Set a value for a specific protocol on the device.

def create( product: roborock.data.containers.HomeDataProduct, mqtt_channel: roborock.devices.mqtt_channel.MqttChannel) -> DyadApi | ZeoApi:
184def create(product: HomeDataProduct, mqtt_channel: MqttChannel) -> DyadApi | ZeoApi:
185    """Create traits for A01 devices."""
186    match product.category:
187        case RoborockCategory.WET_DRY_VAC:
188            return DyadApi(mqtt_channel)
189        case RoborockCategory.WASHING_MACHINE:
190            return ZeoApi(mqtt_channel)
191        case _:
192            raise NotImplementedError(f"Unsupported category {product.category}")

Create traits for A01 devices.