-
Notifications
You must be signed in to change notification settings - Fork 89
feat: complete full Zeo device support #871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6eaf4f9
613db33
9ae9fb7
5399235
a8ac09f
8dd5c3b
99c2b53
e7bcb14
75ecfe1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| """ | ||
|
|
||
| import json | ||
| import logging | ||
| from collections.abc import Callable | ||
| from datetime import time | ||
| from typing import Any | ||
|
|
@@ -38,21 +39,29 @@ | |
| ) | ||
| from roborock.data.zeo.zeo_code_mappings import ( | ||
| ZeoDetergentType, | ||
| ZeoDryAndCare, | ||
| ZeoDryerStartError, | ||
| ZeoDryingMethod, | ||
| ZeoDryingMode, | ||
| ZeoError, | ||
| ZeoMode, | ||
| ZeoProgram, | ||
| ZeoRinse, | ||
| ZeoSoak, | ||
| ZeoSoftenerType, | ||
| ZeoSpin, | ||
| ZeoState, | ||
| ZeoSteamVolume, | ||
| ZeoTemperature, | ||
| ) | ||
| from roborock.devices.rpc.a01_channel import send_decoded_command | ||
| from roborock.devices.traits import Trait | ||
| from roborock.devices.transport.mqtt_channel import MqttChannel | ||
| from roborock.exceptions import RoborockException | ||
| from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| __init__ = [ | ||
| "DyadApi", | ||
| "ZeoApi", | ||
|
|
@@ -101,6 +110,22 @@ | |
| RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val), | ||
| RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val), | ||
| RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val), | ||
| RoborockZeoProtocol.DIRT_DETECTION_STATUS: lambda val: int(val), | ||
| RoborockZeoProtocol.TOTAL_TIME: lambda val: int(val), | ||
| RoborockZeoProtocol.FEATURE_BITS: lambda val: int(val), | ||
| RoborockZeoProtocol.SMART_HOSTING_WAITED_TIME: lambda val: int(val), | ||
| RoborockZeoProtocol.FLUFF_CLEANED: lambda val: bool(val), | ||
| RoborockZeoProtocol.IS_NEED_FLUFF_CLEAN: lambda val: bool(val), | ||
| RoborockZeoProtocol.PANEL_PROGRAM_PARAMS_SET_RESULT: lambda val: int(val), | ||
| RoborockZeoProtocol.DEVICE_BOUND: lambda val: bool(val), | ||
| RoborockZeoProtocol.CLOTH_PUT_IN: lambda val: bool(val), | ||
| RoborockZeoProtocol.CLOTH_READY_TO_DRY_COUNT_DOWN: lambda val: int(val), | ||
| RoborockZeoProtocol.START_DRYER_ERROR: lambda val: ZeoDryerStartError(val).name, | ||
| RoborockZeoProtocol.DOORLOCK_STATE: lambda val: bool(val), | ||
| RoborockZeoProtocol.DEFAULT_SETTING: lambda val: int(val), | ||
| RoborockZeoProtocol.LIGHT_SETTING: lambda val: bool(val), | ||
| RoborockZeoProtocol.DETERGENT_VOLUME: lambda val: int(val), | ||
| RoborockZeoProtocol.SOFTENER_VOLUME: lambda val: int(val), | ||
| # read-write | ||
| RoborockZeoProtocol.MODE: lambda val: ZeoMode(val).name, | ||
| RoborockZeoProtocol.PROGRAM: lambda val: ZeoProgram(val).name, | ||
|
|
@@ -111,6 +136,33 @@ | |
| RoborockZeoProtocol.DETERGENT_TYPE: lambda val: ZeoDetergentType(val).name, | ||
| RoborockZeoProtocol.SOFTENER_TYPE: lambda val: ZeoSoftenerType(val).name, | ||
| RoborockZeoProtocol.SOUND_SET: lambda val: bool(val), | ||
| RoborockZeoProtocol.DIRT_DETECTION_SWITCH: lambda val: bool(val), | ||
| RoborockZeoProtocol.SOAK: lambda val: ZeoSoak(val).name, | ||
| RoborockZeoProtocol.SILENT_MODE_ON: lambda val: bool(val), | ||
| RoborockZeoProtocol.SILENT_MODE_START_TIME: lambda val: int(val), | ||
| RoborockZeoProtocol.SILENT_MODE_END_TIME: lambda val: int(val), | ||
| RoborockZeoProtocol.DRY_CARE_MODE: lambda val: ZeoDryAndCare(val).name, | ||
| RoborockZeoProtocol.WASH_DRY_LINKED: lambda val: bool(val), | ||
| RoborockZeoProtocol.DRYING_METHOD: lambda val: ZeoDryingMethod(val).name, | ||
| RoborockZeoProtocol.STEAM_VOLUME: lambda val: ZeoSteamVolume(val).name, | ||
| RoborockZeoProtocol.ION_DEODORIZATION: lambda val: bool(val), | ||
| RoborockZeoProtocol.UV_LIGHT: lambda val: bool(val), | ||
| RoborockZeoProtocol.SMART_HOSTING: lambda val: bool(val), | ||
| RoborockZeoProtocol.SMART_HOSTING_TIME: lambda val: int(val), | ||
| RoborockZeoProtocol.SOFTENER_EXPANSION_TYPE: lambda val: int(val), | ||
| RoborockZeoProtocol.DETERGENT_EXPANSION_TYPE: lambda val: int(val), | ||
| RoborockZeoProtocol.SMILE_LIGHT_STATUS: lambda val: bool(val), | ||
| RoborockZeoProtocol.POWER_LIGHT: lambda val: bool(val), | ||
| RoborockZeoProtocol.PANEL_PROGRAM_PARAMS_SET: lambda val: int(val), | ||
| RoborockZeoProtocol.PANEL_TIMING_PROGRAM_PARAMS: lambda val: int(val), | ||
| RoborockZeoProtocol.STEAM_CARE_TIME: lambda val: int(val), | ||
| RoborockZeoProtocol.WIFI_LINKAGE_RESET: lambda val: int(val), | ||
| RoborockZeoProtocol.CUSTOM_PROGRAM_CLEANING_TIME: lambda val: int(val), | ||
| RoborockZeoProtocol.SAVE_ADAPTED_CLOUD_PROGRAM: lambda val: int(val), | ||
| RoborockZeoProtocol.CHILD_LOCK: lambda val: bool(val), | ||
| RoborockZeoProtocol.DETERGENT_SET: lambda val: bool(val), | ||
| RoborockZeoProtocol.SOFTENER_SET: lambda val: bool(val), | ||
| RoborockZeoProtocol.APP_AUTHORIZATION: lambda val: bool(val), | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -176,8 +228,47 @@ async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[Robor | |
|
|
||
| async def set_value(self, protocol: RoborockZeoProtocol, value: Any) -> dict[RoborockZeoProtocol, Any]: | ||
| """Set a value for a specific protocol on the device.""" | ||
| params = {protocol: value} | ||
| return await send_decoded_command(self._channel, params, value_encoder=lambda x: x) | ||
| params: dict[RoborockZeoProtocol, Any] = {protocol: value} | ||
| if protocol == RoborockZeoProtocol.START and value == 1: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes me think it may be time to define actual API methods for these specialized commands rather than a single generic query_values/set_values (e.g. a "start" and "stop" methods etc for the washing machine. Do you expect there to be more specialized commands like this or is this the only one? At a minimum: Split out the function that queries the current values to a separate method |
||
| _LOGGER.debug("Start command detected, querying current device state") | ||
| try: | ||
| current = await send_decoded_command( | ||
| self._channel, | ||
| { | ||
| RoborockZeoProtocol.ID_QUERY: [ | ||
| RoborockZeoProtocol.MODE, | ||
| RoborockZeoProtocol.PROGRAM, | ||
| RoborockZeoProtocol.TEMP, | ||
| RoborockZeoProtocol.RINSE_TIMES, | ||
| RoborockZeoProtocol.SPIN_LEVEL, | ||
| RoborockZeoProtocol.DRYING_MODE, | ||
| ] | ||
| }, | ||
| value_encoder=json.dumps, | ||
| ) | ||
| for dp, fallback in ( | ||
| (RoborockZeoProtocol.MODE, 1), | ||
| (RoborockZeoProtocol.PROGRAM, 1), | ||
| (RoborockZeoProtocol.TEMP, None), | ||
| (RoborockZeoProtocol.RINSE_TIMES, None), | ||
| (RoborockZeoProtocol.SPIN_LEVEL, None), | ||
| (RoborockZeoProtocol.DRYING_MODE, None), | ||
| ): | ||
| if dp in current: | ||
| params[dp] = current[dp] | ||
| elif fallback is not None: | ||
| params[dp] = fallback | ||
| except RoborockException: | ||
| _LOGGER.warning( | ||
| "Failed to query device state before start, using defaults", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not sure this fallback is a great idea e.g. if the user is expecting some values to be preserved then silently not preserving them worries me e.g. as a user I would be concerned if my washing machine changed my water temp due to flaky wifi and i might prefer it just fail rather than shrinking cotton clothes or whatever. Can we either remove or provide some rationale here why this is expected or ok? |
||
| exc_info=True, | ||
| ) | ||
| params[RoborockZeoProtocol.MODE] = 1 | ||
| params[RoborockZeoProtocol.PROGRAM] = 1 | ||
| # START commands require QoS 1; all other A01 commands use default QoS 0. | ||
| return await send_decoded_command( | ||
| self._channel, params, value_encoder=lambda x: x, qos=1 if protocol == RoborockZeoProtocol.START else 0 | ||
| ) | ||
|
|
||
|
|
||
| def create(product: HomeDataProduct, mqtt_channel: MqttChannel) -> DyadApi | ZeoApi: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,19 +89,23 @@ async def subscribe_stream(self) -> AsyncGenerator[RoborockMessage, None]: | |
| finally: | ||
| unsub() | ||
|
|
||
| async def publish(self, message: RoborockMessage) -> None: | ||
| async def publish(self, message: RoborockMessage, qos: int = 0) -> None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's define an enum for qos level. Just having numbers 0, 1, 2 doesn't really say what these are, or why they exist. Why would someone set a lower or higher level, etc should be explained in the code and doing it from the enum could be a good way. |
||
| """Publish a command message. | ||
|
|
||
| The caller is responsible for handling any responses and associating them | ||
| with the incoming request. | ||
|
|
||
| Args: | ||
| message: The message to publish. | ||
| qos: The MQTT QoS level. Defaults to 0. | ||
| """ | ||
| try: | ||
| encoded_msg = self._encoder(message) | ||
| except Exception as e: | ||
| self._logger.exception("Error encoding MQTT message: %s", e) | ||
| raise RoborockException(f"Failed to encode MQTT message: {e}") from e | ||
| try: | ||
| return await self._mqtt_session.publish(self._publish_topic, encoded_msg) | ||
| return await self._mqtt_session.publish(self._publish_topic, encoded_msg, qos=qos) | ||
| except MqttSessionException as e: | ||
| self._logger.debug("Error publishing MQTT message: %s", e) | ||
| raise RoborockException(f"Failed to publish MQTT message: {e}") from e | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's tag as a breaking change. i think it's that commits have
feat!:?