Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions roborock/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,20 @@ async def reset_consumable(ctx, device_id: str, consumable: str):
click.echo(f"Reset {consumable} for device {device_id}")


@session.command()
@click.option("--device_id", required=True)
@click.option("--error_code", type=int, help="Error code to resolve. Defaults to the current dock or robot error.")
@click.pass_context
@async_command
async def resolve_error(ctx, device_id: str, error_code: int | None):
"""Resolve the current dock or robot error, like "Resolved" in the app."""
context: RoborockContext = ctx.obj
trait = await _v1_trait(context, device_id, lambda v1: v1.status)
await trait.refresh()
await trait.resolve_error(error_code)
click.echo(f"Dock error: {trait.dock_error_status}, error: {trait.error_code}")


@session.command()
@click.option("--device_id", required=True)
@click.option("--enabled", type=bool, help="Enable (True) or disable (False) the child lock.")
Expand Down Expand Up @@ -1369,6 +1383,7 @@ def write_markdown_table(product_features: dict[str, dict[str, any]], all_featur
cli.add_command(q10_position)
cli.add_command(consumables)
cli.add_command(reset_consumable)
cli.add_command(resolve_error)
cli.add_command(rooms)
cli.add_command(home)
cli.add_command(features)
Expand Down
17 changes: 17 additions & 0 deletions roborock/devices/traits/v1/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@ async def set_cleaning_mode(self, cleaning_mode: str | CleaningMode) -> None:
params=get_cleaning_mode_parameters(resolve_cleaning_mode(cleaning_mode), self._device_features_trait),
)

async def resolve_error(self, error_code: int | None = None) -> None:
"""Resolve an error, like tapping "Resolved" in the Roborock app.

Without an explicit code this resolves the current dock error if there is one,
otherwise the current robot error. A dock error such as water_empty stays latched
on the device until it is resolved, even after the cause has been fixed.
"""
if error_code is None:
if self.dock_error_status:
error_code = int(self.dock_error_status)
elif self.error_code:
error_code = int(self.error_code)
else:
return
await self.rpc_channel.send_command(RoborockCommand.RESOLVE_ERROR, params={"error_code": error_code})
await self.refresh()

def update_from_dps(self, decoded_dps: dict[RoborockDataProtocol, Any]) -> None:
"""Update the trait from data protocol push message data.

Expand Down
71 changes: 70 additions & 1 deletion tests/devices/traits/v1/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import asyncio
from typing import cast
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, call

import pytest

Expand All @@ -17,6 +17,8 @@
)
from roborock.data import SHORT_MODEL_TO_ENUM, RoborockProductNickname
from roborock.data.v1 import (
RoborockDockErrorCode,
RoborockErrorCode,
RoborockStateCode,
)
from roborock.device_features import DeviceFeatures
Expand Down Expand Up @@ -90,6 +92,73 @@ async def test_refresh_status_propagates_exception(status_trait: StatusTrait, mo
await status_trait.refresh()


async def test_resolve_error_explicit_code(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
"""Test resolving an explicitly given error code."""
mock_rpc_channel.send_command.side_effect = ["ok", STATUS]

await status_trait.resolve_error(38)

assert mock_rpc_channel.send_command.mock_calls == [
call(RoborockCommand.RESOLVE_ERROR, params={"error_code": 38}),
call(RoborockCommand.GET_STATUS),
]
assert status_trait.dock_error_status == RoborockDockErrorCode.ok


async def test_resolve_error_defaults_to_dock_error(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
"""Test that the current dock error is resolved when no code is given."""
mock_rpc_channel.send_command.side_effect = [
{**STATUS, "dock_error_status": RoborockDockErrorCode.water_empty.value, "error_code": 38},
"ok",
STATUS,
]
await status_trait.refresh()
assert status_trait.dock_error_status == RoborockDockErrorCode.water_empty

await status_trait.resolve_error()

assert mock_rpc_channel.send_command.mock_calls[1:] == [
call(RoborockCommand.RESOLVE_ERROR, params={"error_code": 38}),
call(RoborockCommand.GET_STATUS),
]
assert status_trait.dock_error_status == RoborockDockErrorCode.ok


async def test_resolve_error_defaults_to_robot_error(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
"""Test that the robot error is resolved when there is no dock error."""
mock_rpc_channel.send_command.side_effect = [
{**STATUS, "error_code": RoborockErrorCode.clear_water_box_exception.value},
"ok",
STATUS,
]
await status_trait.refresh()

await status_trait.resolve_error()

assert mock_rpc_channel.send_command.mock_calls[1:] == [
call(RoborockCommand.RESOLVE_ERROR, params={"error_code": RoborockErrorCode.clear_water_box_exception.value}),
call(RoborockCommand.GET_STATUS),
]


async def test_resolve_error_no_error(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
"""Test that nothing is sent when there is no error to resolve."""
mock_rpc_channel.send_command.return_value = STATUS
await status_trait.refresh()

await status_trait.resolve_error()

mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_STATUS)


async def test_resolve_error_propagates_exception(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
"""Test that exceptions from the RPC channel are propagated."""
mock_rpc_channel.send_command.side_effect = RoborockException("invalid params")

with pytest.raises(RoborockException, match="invalid params"):
await status_trait.resolve_error(38)


async def test_refresh_status_invalid_format(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
"""Test that invalid response format raises RoborockParsingException."""
mock_rpc_channel.send_command.return_value = "invalid"
Expand Down