diff --git a/deebot_client/commands/xml/__init__.py b/deebot_client/commands/xml/__init__.py
index b0cfdcdd2..a2e3c9e07 100644
--- a/deebot_client/commands/xml/__init__.py
+++ b/deebot_client/commands/xml/__init__.py
@@ -9,6 +9,7 @@
from .battery import GetBatteryInfo
from .charge import Charge
from .charge_state import GetChargeState
+from .clean import Clean, CleanArea, GetCleanState
from .clean_logs import GetCleanLogs
from .clean_speed import GetCleanSpeed, SetCleanSpeed
from .error import GetError
@@ -23,11 +24,14 @@
__all__ = [
"Charge",
+ "Clean",
+ "CleanArea",
"GetBatteryInfo",
"GetChargeState",
"GetChargerPos",
"GetCleanLogs",
"GetCleanSpeed",
+ "GetCleanState",
"GetCleanSum",
"GetError",
"GetLifeSpan",
@@ -51,6 +55,10 @@
Charge,
+ Clean,
+ CleanArea,
+ GetCleanState,
+
GetCleanLogs,
GetCleanSpeed,
diff --git a/deebot_client/commands/xml/clean.py b/deebot_client/commands/xml/clean.py
new file mode 100644
index 000000000..68bdd9365
--- /dev/null
+++ b/deebot_client/commands/xml/clean.py
@@ -0,0 +1,90 @@
+"""Clean commands."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from deebot_client.events import FanSpeedEvent, FanSpeedLevel, StateEvent
+from deebot_client.message import HandlingResult
+from deebot_client.models import CleanAction, CleanMode, State
+
+from .common import ExecuteCommand, XmlCommandWithMessageHandling
+
+if TYPE_CHECKING:
+ from xml.etree.ElementTree import Element
+
+ from deebot_client.event_bus import EventBus
+
+
+class Clean(ExecuteCommand):
+ """Generic start/pause/stop cleaning command."""
+
+ NAME = "Clean"
+ HAS_SUB_ELEMENT = True
+
+ def __init__(
+ self, action: CleanAction, speed: FanSpeedLevel = FanSpeedLevel.NORMAL
+ ) -> None:
+ super().__init__(
+ {
+ "type": CleanMode.AUTO.xml_value,
+ "act": action.xml_value,
+ "speed": speed.xml_value,
+ }
+ )
+
+
+class CleanArea(ExecuteCommand):
+ """Clean area command."""
+
+ NAME = "Clean"
+ HAS_SUB_ELEMENT = True
+
+ def __init__(
+ self,
+ mode: CleanMode,
+ area: str,
+ cleanings: int = 1,
+ speed: FanSpeedLevel = FanSpeedLevel.NORMAL,
+ ) -> None:
+ super().__init__(
+ {
+ "type": mode.xml_value,
+ "act": CleanAction.START.xml_value,
+ "speed": speed.xml_value,
+ "deep": str(cleanings),
+ "mid": area,
+ }
+ )
+
+
+class GetCleanState(XmlCommandWithMessageHandling):
+ """GetCleanState command."""
+
+ NAME = "GetCleanState"
+
+ @classmethod
+ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
+ """Handle xml message and notify the correct event subscribers.
+
+ :return: A message response
+ """
+ if xml.attrib.get("ret") != "ok" or (clean := xml.find("clean")) is None:
+ return HandlingResult.analyse()
+
+ speed_attrib = clean.attrib.get("speed")
+ if speed_attrib is not None:
+ fan_speed_level = FanSpeedLevel.from_xml(speed_attrib)
+ event_bus.notify(FanSpeedEvent(fan_speed_level))
+
+ clean_attrib = clean.attrib.get("st")
+ if clean_attrib is not None:
+ clean_action = CleanAction.from_xml(clean_attrib)
+ if clean_action == CleanAction.START:
+ event_bus.notify(StateEvent(State.CLEANING))
+ elif clean_action == CleanAction.PAUSE:
+ event_bus.notify(StateEvent(State.PAUSED))
+ elif clean_action in (CleanAction.RESUME, CleanAction.STOP):
+ event_bus.notify(StateEvent(State.IDLE))
+
+ return HandlingResult.success()
diff --git a/tests/commands/xml/test_clean.py b/tests/commands/xml/test_clean.py
new file mode 100644
index 000000000..731dad556
--- /dev/null
+++ b/tests/commands/xml/test_clean.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+from deebot_client.command import CommandResult
+from deebot_client.commands.xml import Clean, CleanArea, GetCleanState
+from deebot_client.events import FanSpeedEvent, FanSpeedLevel, StateEvent
+from deebot_client.message import HandlingState
+from deebot_client.models import CleanAction, CleanMode, State
+from tests.commands import assert_command
+
+from . import get_request_xml
+
+if TYPE_CHECKING:
+ from deebot_client.events.base import Event
+
+
+@pytest.mark.parametrize(
+ "command",
+ [
+ Clean(CleanAction.START, speed=FanSpeedLevel.MAX),
+ Clean(CleanAction.PAUSE),
+ ],
+)
+async def test_Clean(command: Clean) -> None:
+ json = get_request_xml("")
+ await assert_command(
+ command, json, None, command_result=CommandResult(HandlingState.SUCCESS)
+ )
+
+
+@pytest.mark.parametrize(
+ "command",
+ [
+ CleanArea(CleanMode.SPOT_AREA, "4", 1),
+ ],
+)
+async def test_CleanArea(command: CleanArea) -> None:
+ json = get_request_xml("")
+ await assert_command(
+ command, json, None, command_result=CommandResult(HandlingState.SUCCESS)
+ )
+
+
+@pytest.mark.parametrize(
+ ("params", "expected_events"),
+ [
+ (
+ "speed='standard' st='s'",
+ [FanSpeedEvent(FanSpeedLevel.NORMAL), StateEvent(State.CLEANING)],
+ ),
+ (
+ "speed='strong' st='s'",
+ [FanSpeedEvent(FanSpeedLevel.MAX), StateEvent(State.CLEANING)],
+ ),
+ (
+ "speed='standard' st='p'",
+ [FanSpeedEvent(FanSpeedLevel.NORMAL), StateEvent(State.PAUSED)],
+ ),
+ (
+ "st='r'",
+ [StateEvent(State.IDLE)],
+ ),
+ (
+ "st='h'",
+ [StateEvent(State.IDLE)],
+ ),
+ (
+ "speed='strong'",
+ [FanSpeedEvent(FanSpeedLevel.MAX)],
+ ),
+ ],
+ ids=[
+ "standard_cleaning",
+ "strong_cleaning",
+ "paused",
+ "resume/idle",
+ "stop/idle",
+ "fanspeed_only",
+ ],
+)
+async def test_get_clean_state(
+ params: str,
+ expected_events: list[Event],
+) -> None:
+ json = get_request_xml(
+ f""
+ )
+ await assert_command(
+ GetCleanState(),
+ json,
+ expected_events,
+ )
+
+
+@pytest.mark.parametrize(
+ "xml",
+ ["", ""],
+ ids=["error", "no_state"],
+)
+async def test_get_clean_state_error(xml: str) -> None:
+ json = get_request_xml(xml)
+ await assert_command(
+ GetCleanState(),
+ json,
+ None,
+ command_result=CommandResult(HandlingState.ANALYSE_LOGGED),
+ )