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))