Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 96
feat(q10): Add Q10 vacuum CLI commands and Status API#759
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
612264798f83f02cf5bd16f4b506dc8ba01d9e226cd742167a86e8f031b2328e1f6ac2f0910593d56591fc076f6353babc1976c84de7cf6934b11605e90a9c4adc9c108de5c228716c98c3345d76605ace24004e48b7fcfd8b759bFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,17 +2,23 @@ | ||
| from __future__ import annotations | ||
| import asyncio | ||
| import logging | ||
| from collections.abc import Iterable | ||
| from typing import Any | ||
| from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP | ||
| from roborock.devices.transport.mqtt_channel import MqttChannel | ||
| from roborock.exceptions import RoborockException | ||
| from roborock.protocols.b01_q10_protocol import ( | ||
| ParamsType, | ||
| decode_rpc_response, | ||
| encode_mqtt_payload, | ||
| ) | ||
| from roborock.roborock_message import RoborockMessage | ||
| _LOGGER = logging.getLogger(__name__) | ||
| _TIMEOUT = 10.0 | ||
| async def send_command( | ||
| @@ -34,3 +40,61 @@ async def send_command( | ||
| ex, | ||
| ) | ||
| raise | ||
| async def send_decoded_command( | ||
| mqtt_channel: MqttChannel, | ||
| command: B01_Q10_DP, | ||
| params: ParamsType, | ||
| expected_dps: Iterable[B01_Q10_DP] | None = None, | ||
| ) -> dict[B01_Q10_DP, Any]: | ||
| """Send a command and await the first decoded response. | ||
| Q10 responses are not correlated with a message id, so we filter on | ||
| expected datapoints when provided. | ||
| """ | ||
Comment on lines
+45
to
+55
CopilotAI | ||
| roborock_message = encode_mqtt_payload(command, params) | ||
| future: asyncio.Future[dict[B01_Q10_DP, Any]] = asyncio.get_running_loop().create_future() | ||
| expected_set = set(expected_dps) if expected_dps is not None else None | ||
| def find_response(response_message: RoborockMessage) -> None: | ||
| try: | ||
| decoded_dps = decode_rpc_response(response_message) | ||
| except RoborockException as ex: | ||
| _LOGGER.debug( | ||
| "Failed to decode B01 Q10 RPC response (expecting %s): %s: %s", | ||
| command, | ||
| response_message, | ||
| ex, | ||
| ) | ||
| return | ||
| if expected_set and not any(dps in decoded_dps for dps in expected_set): | ||
| return | ||
| if not future.done(): | ||
| future.set_result(decoded_dps) | ||
| unsub = await mqtt_channel.subscribe(find_response) | ||
| _LOGGER.debug("Sending MQTT message: %s", roborock_message) | ||
| try: | ||
| await mqtt_channel.publish(roborock_message) | ||
| return await asyncio.wait_for(future, timeout=_TIMEOUT) | ||
| except TimeoutError as ex: | ||
| raise RoborockException(f"B01 Q10 command timed out after {_TIMEOUT}s ({command})") from ex | ||
| except RoborockException as ex: | ||
| _LOGGER.warning( | ||
| "Error sending B01 Q10 decoded command (%s): %s", | ||
| command, | ||
| ex, | ||
| ) | ||
| raise | ||
| except Exception as ex: | ||
| _LOGGER.exception( | ||
| "Error sending B01 Q10 decoded command (%s): %s", | ||
| command, | ||
| ex, | ||
| ) | ||
| raise | ||
| finally: | ||
| unsub() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| """Status trait for Q10 B01 devices.""" | ||
| from __future__ import annotations | ||
| from typing import Any, cast | ||
| from roborock.data.b01_q10.b01_q10_code_mappings import ( | ||
| B01_Q10_DP, | ||
| YXDeviceCleanTask, | ||
| YXDeviceState, | ||
| YXDeviceWorkMode, | ||
| YXFanLevel, | ||
| YXWaterLevel, | ||
| ) | ||
| from roborock.devices.rpc.b01_q10_channel import send_decoded_command | ||
| from roborock.devices.transport.mqtt_channel import MqttChannel | ||
| class StatusTrait: | ||
| """Trait for requesting and holding Q10 status values.""" | ||
| def __init__(self, channel: MqttChannel) -> None: | ||
| self._channel = channel | ||
| self._data: dict[B01_Q10_DP, Any] = {} | ||
| @property | ||
| def data(self) -> dict[B01_Q10_DP, Any]: | ||
| """Return the latest raw status data.""" | ||
| return self._data | ||
| async def refresh(self) -> dict[B01_Q10_DP, Any]: | ||
| """Refresh status values from the device.""" | ||
| decoded = await send_decoded_command( | ||
| self._channel, | ||
| command=B01_Q10_DP.REQUETDPS, | ||
| params={}, | ||
| expected_dps={B01_Q10_DP.STATUS, B01_Q10_DP.BATTERY}, | ||
| ) | ||
| self._data = decoded | ||
| return decoded | ||
Comment on lines
+31
to
+40
CopilotAI | ||
| @property | ||
| def state_code(self) -> int | None: | ||
| return self._data.get(B01_Q10_DP.STATUS) | ||
| @property | ||
| def state(self) -> YXDeviceState | None: | ||
| code = self.state_code | ||
| return cast(YXDeviceState | None, YXDeviceState.from_code_optional(code)) if code is not None else None | ||
Collaborator 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. cast should not be needed after this change #761 | ||
| @property | ||
| def battery(self) -> int | None: | ||
| return self._data.get(B01_Q10_DP.BATTERY) | ||
| @property | ||
| def fan_level(self) -> YXFanLevel | None: | ||
| value = self._data.get(B01_Q10_DP.FUN_LEVEL) | ||
| return cast(YXFanLevel | None, YXFanLevel.from_code_optional(value)) if value is not None else None | ||
| @property | ||
| def water_level(self) -> YXWaterLevel | None: | ||
| value = self._data.get(B01_Q10_DP.WATER_LEVEL) | ||
| return cast(YXWaterLevel | None, YXWaterLevel.from_code_optional(value)) if value is not None else None | ||
| @property | ||
| def clean_mode(self) -> YXDeviceWorkMode | None: | ||
| value = self._data.get(B01_Q10_DP.CLEAN_MODE) | ||
| return cast(YXDeviceWorkMode | None, YXDeviceWorkMode.from_code_optional(value)) if value is not None else None | ||
| @property | ||
| def clean_task(self) -> YXDeviceCleanTask | None: | ||
| value = self._data.get(B01_Q10_DP.CLEAN_TASK_TYPE) | ||
| return ( | ||
| cast(YXDeviceCleanTask | None, YXDeviceCleanTask.from_code_optional(value)) if value is not None else None | ||
| ) | ||
| @property | ||
| def cleaning_progress(self) -> int | None: | ||
| return self._data.get(B01_Q10_DP.CLEANING_PROGRESS) | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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.
Does this work? My impression is that q10 doesn't support this kind of mapping between request and response. We have a different approach in
#709