diff --git a/deebot_client/command.py b/deebot_client/command.py
index dc0e0cf38..6afb94be7 100644
--- a/deebot_client/command.py
+++ b/deebot_client/command.py
@@ -323,9 +323,11 @@ def _pop_or_raise(name: str, type_: type, data: dict[str, Any]) -> Any:
value = data.pop(name)
try:
return type_(value)
- except ValueError as err:
- msg = f'Could not convert "{value}" of {name} into {type_}'
- raise DeebotError(msg) from err
+ except ValueError:
+ # TODO: Workaround to map out custom enums
+ return type_.from_xml(value)
+ # msg = f'Could not convert "{value}" of {name} into {type_}'
+ # raise DeebotError(msg) from err
class GetCommand(CommandWithMessageHandling, ABC):
diff --git a/deebot_client/commands/__init__.py b/deebot_client/commands/__init__.py
index 5fb527c47..fc4574353 100644
--- a/deebot_client/commands/__init__.py
+++ b/deebot_client/commands/__init__.py
@@ -11,6 +11,9 @@
COMMANDS as JSON_COMMANDS,
COMMANDS_WITH_MQTT_P2P_HANDLING as JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
)
+from .xml import (
+ COMMANDS_WITH_MQTT_P2P_HANDLING as XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
+)
if TYPE_CHECKING:
from deebot_client.command import Command, CommandMqttP2P
@@ -18,7 +21,8 @@
COMMANDS: dict[DataType, dict[str, type[Command]]] = {DataType.JSON: JSON_COMMANDS}
COMMANDS_WITH_MQTT_P2P_HANDLING: dict[DataType, dict[str, type[CommandMqttP2P]]] = {
- DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING
+ DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
+ DataType.XML: XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
}
diff --git a/deebot_client/commands/xml/__init__.py b/deebot_client/commands/xml/__init__.py
index 36b35c8ac..a28f6f2f2 100644
--- a/deebot_client/commands/xml/__init__.py
+++ b/deebot_client/commands/xml/__init__.py
@@ -9,7 +9,7 @@
from .charge import Charge
from .charge_state import GetChargeState
from .error import GetError
-from .fan_speed import GetFanSpeed
+from .fan_speed import GetCleanSpeed, SetCleanSpeed
from .life_span import GetLifeSpan
from .play_sound import PlaySound
from .pos import GetPos
@@ -21,17 +21,20 @@
__all__ = [
"Charge",
"GetChargeState",
+ "GetCleanSpeed",
"GetCleanSum",
"GetError",
- "GetFanSpeed",
"GetLifeSpan",
"GetPos",
"PlaySound",
+ "SetCleanSpeed",
]
# fmt: off
# ordered by file asc
_COMMANDS: list[type[XmlCommand]] = [
+ GetCleanSpeed,
+ SetCleanSpeed,
GetError,
GetLifeSpan,
PlaySound,
diff --git a/deebot_client/commands/xml/common.py b/deebot_client/commands/xml/common.py
index ac026df71..77a78b8e5 100644
--- a/deebot_client/commands/xml/common.py
+++ b/deebot_client/commands/xml/common.py
@@ -3,15 +3,25 @@
from __future__ import annotations
from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING, cast
+from typing import TYPE_CHECKING, Any, cast
from xml.etree.ElementTree import Element, SubElement
from defusedxml import ElementTree # type: ignore[import-untyped]
-from deebot_client.command import Command, CommandWithMessageHandling, SetCommand
+from deebot_client.command import (
+ Command,
+ CommandMqttP2P,
+ CommandWithMessageHandling,
+ GetCommand,
+ SetCommand,
+)
from deebot_client.const import DataType
from deebot_client.logging_filter import get_logger
-from deebot_client.message import HandlingResult, HandlingState, MessageStr
+from deebot_client.message import (
+ HandlingResult,
+ HandlingState,
+ MessageStr,
+)
if TYPE_CHECKING:
from deebot_client.event_bus import EventBus
@@ -81,8 +91,39 @@ def _handle_xml(cls, _: EventBus, xml: Element) -> HandlingResult:
return HandlingResult(HandlingState.FAILED)
-class XmlSetCommand(ExecuteCommand, SetCommand, ABC):
+class XmlCommandMqttP2P(XmlCommand, CommandMqttP2P, ABC):
+ """Json base command for mqtt p2p channel."""
+
+ @classmethod
+ def create_from_mqtt(cls, payload: str | bytes | bytearray) -> CommandMqttP2P:
+ """Create a command from the mqtt data."""
+ xml = ElementTree.fromstring(payload)
+ return cls._create_from_mqtt(xml.attrib)
+
+ def handle_mqtt_p2p(
+ self, event_bus: EventBus, response_payload: str | bytes | bytearray
+ ) -> None:
+ """Handle response received over the mqtt channel "p2p"."""
+ self._handle_mqtt_p2p(event_bus, response_payload)
+
+ @abstractmethod
+ def _handle_mqtt_p2p(self, event_bus: EventBus, response: dict[str, Any]) -> None:
+ """Handle response received over the mqtt channel "p2p"."""
+
+
+class XmlSetCommand(ExecuteCommand, SetCommand, XmlCommandMqttP2P, ABC):
"""Xml base set command.
Command needs to be linked to the "get" command, for handling (updating) the sensors.
"""
+
+
+class XmlGetCommand(XmlCommandWithMessageHandling, GetCommand, ABC):
+ """Xml get command."""
+
+ @classmethod
+ @abstractmethod
+ def handle_set_args(
+ cls, event_bus: EventBus, args: dict[str, Any]
+ ) -> HandlingResult:
+ """Handle arguments of set command."""
diff --git a/deebot_client/commands/xml/fan_speed.py b/deebot_client/commands/xml/fan_speed.py
index 2a834554c..7130b0d68 100644
--- a/deebot_client/commands/xml/fan_speed.py
+++ b/deebot_client/commands/xml/fan_speed.py
@@ -2,12 +2,14 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from types import MappingProxyType
+from typing import TYPE_CHECKING, Any
+from deebot_client.command import InitParam
from deebot_client.events import FanSpeedEvent, FanSpeedLevel
from deebot_client.message import HandlingResult
-from .common import XmlCommandWithMessageHandling
+from .common import XmlGetCommand, XmlSetCommand
if TYPE_CHECKING:
from xml.etree.ElementTree import Element
@@ -15,11 +17,22 @@
from deebot_client.event_bus import EventBus
-class GetFanSpeed(XmlCommandWithMessageHandling):
- """GetFanSpeed command."""
+class GetCleanSpeed(XmlGetCommand):
+ """GetCleanSpeed command."""
NAME = "GetCleanSpeed"
+ @classmethod
+ def handle_set_args(
+ cls, event_bus: EventBus, args: dict[str, Any]
+ ) -> HandlingResult:
+ """Handle message->body->data and notify the correct event subscribers.
+
+ :return: A message response
+ """
+ event_bus.notify(FanSpeedEvent(FanSpeedLevel.from_xml(str(args["speed"]))))
+ return HandlingResult.success()
+
@classmethod
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
"""Handle xml message and notify the correct event subscribers.
@@ -29,16 +42,18 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
if xml.attrib.get("ret") != "ok" or not (speed := xml.attrib.get("speed")):
return HandlingResult.analyse()
- event: FanSpeedEvent | None = None
+ event_bus.notify(FanSpeedEvent(FanSpeedLevel.from_xml(speed)))
+ return HandlingResult.success()
+
- match speed.lower():
- case "standard":
- event = FanSpeedEvent(FanSpeedLevel.NORMAL)
- case "strong":
- event = FanSpeedEvent(FanSpeedLevel.MAX)
+class SetCleanSpeed(XmlSetCommand):
+ """Set clean speed command."""
- if event:
- event_bus.notify(event)
- return HandlingResult.success()
+ NAME = "SetCleanSpeed"
+ get_command = GetCleanSpeed
+ _mqtt_params = MappingProxyType({"speed": InitParam(FanSpeedLevel)})
- return HandlingResult.analyse()
+ def __init__(self, speed: FanSpeedLevel | str) -> None:
+ if isinstance(speed, FanSpeedLevel):
+ speed = speed.xml_value
+ super().__init__({"speed": speed})
diff --git a/deebot_client/events/fan_speed.py b/deebot_client/events/fan_speed.py
index 4a8f599e1..a350f3ba9 100644
--- a/deebot_client/events/fan_speed.py
+++ b/deebot_client/events/fan_speed.py
@@ -4,6 +4,7 @@
from dataclasses import dataclass
from enum import IntEnum, unique
+from typing import Self
from .base import Event
@@ -12,11 +13,30 @@
class FanSpeedLevel(IntEnum):
"""Enum class for all possible fan speed levels."""
+ xml_value: str
+
+ def __new__(cls, value: int, xml_value: str = "") -> Self:
+ """Get new instance."""
+ obj = int.__new__(cls)
+ obj._value_ = value
+ obj.xml_value = xml_value
+ return obj
+
+ @classmethod
+ def from_xml(cls, value: str) -> FanSpeedLevel:
+ """Get FanSpeedLevel from xml value."""
+ for fan_speed_level in FanSpeedLevel:
+ if fan_speed_level.xml_value == value:
+ return fan_speed_level
+
+ msg = f"{value} is not a valid {cls.__name__}"
+ raise ValueError(msg)
+
# Values should be sort from low to high on their meanings
- QUIET = 1000
- NORMAL = 0
- MAX = 1
- MAX_PLUS = 2
+ QUIET = 1000, ""
+ NORMAL = 0, "standard"
+ MAX = 1, "strong"
+ MAX_PLUS = 2, ""
@dataclass(frozen=True)
diff --git a/tests/commands/xml/test_fan_speed.py b/tests/commands/xml/test_fan_speed.py
index bf728ecfc..cafebe990 100644
--- a/tests/commands/xml/test_fan_speed.py
+++ b/tests/commands/xml/test_fan_speed.py
@@ -4,8 +4,8 @@
import pytest
-from deebot_client.command import CommandResult
-from deebot_client.commands.xml import GetFanSpeed
+from deebot_client.command import CommandResult, CommandWithMessageHandling
+from deebot_client.commands.xml import GetCleanSpeed, SetCleanSpeed
from deebot_client.events import FanSpeedEvent, FanSpeedLevel
from deebot_client.message import HandlingState
from tests.commands import assert_command
@@ -24,21 +24,38 @@
],
ids=["standard", "strong"],
)
-async def test_get_fan_speed(speed: str, expected_event: Event) -> None:
+async def test_get_clean_speed(speed: str, expected_event: Event) -> None:
json = get_request_xml(f"")
- await assert_command(GetFanSpeed(), json, expected_event)
+ await assert_command(GetCleanSpeed(), json, expected_event)
@pytest.mark.parametrize(
"xml",
- ["", ""],
- ids=["error", "no_state"],
+ [""],
+ ids=["error"],
)
-async def test_get_fan_speed_error(xml: str) -> None:
+async def test_get_clean_speed_error(xml: str) -> None:
json = get_request_xml(xml)
await assert_command(
- GetFanSpeed(),
+ GetCleanSpeed(),
json,
None,
command_result=CommandResult(HandlingState.ANALYSE_LOGGED),
)
+
+
+@pytest.mark.parametrize(
+ ("command", "xml", "result"),
+ [
+ (
+ SetCleanSpeed(FanSpeedLevel.MAX),
+ "",
+ HandlingState.SUCCESS,
+ ),
+ ],
+)
+async def test_set_clean_speed(
+ command: CommandWithMessageHandling, xml: str, result: HandlingState
+) -> None:
+ json = get_request_xml(xml)
+ await assert_command(command, json, None, command_result=CommandResult(result))