Skip to content

Commit ea5b144

Browse files
committed
Initial Ozmo 900 support
1 parent 11f2fa2 commit ea5b144

15 files changed

Lines changed: 582 additions & 44 deletions

File tree

deebot_client/command.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,15 @@ def _pop_or_raise(name: str, type_: type, data: dict[str, Any]) -> Any:
324324
try:
325325
return type_(value)
326326
except ValueError as err:
327-
msg = f'Could not convert "{value}" of {name} into {type_}'
328-
raise DeebotError(msg) from err
327+
if hasattr(type_, "from_xml"):
328+
try:
329+
return type_.from_xml(value)
330+
except ValueError as err2:
331+
msg = f'Could not convert "{value}" of {name} into {type_}'
332+
raise DeebotError(msg) from err2
333+
else:
334+
msg = f'Could not convert "{value}" of {name} into {type_}'
335+
raise DeebotError(msg) from err
329336

330337

331338
class GetCommand(CommandWithMessageHandling, ABC):

deebot_client/commands/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111
COMMANDS as JSON_COMMANDS,
1212
COMMANDS_WITH_MQTT_P2P_HANDLING as JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
1313
)
14+
from .xml import (
15+
COMMANDS_WITH_MQTT_P2P_HANDLING as XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
16+
)
1417

1518
if TYPE_CHECKING:
1619
from deebot_client.command import Command, CommandMqttP2P
1720

1821
COMMANDS: dict[DataType, dict[str, type[Command]]] = {DataType.JSON: JSON_COMMANDS}
1922

2023
COMMANDS_WITH_MQTT_P2P_HANDLING: dict[DataType, dict[str, type[CommandMqttP2P]]] = {
21-
DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING
24+
DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
25+
DataType.XML: XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
2226
}
2327

2428

deebot_client/commands/xml/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
from deebot_client.command import Command, CommandMqttP2P
88

9+
from .battery import GetBatteryInfo
910
from .charge import Charge
1011
from .charge_state import GetChargeState
12+
from .clean import Clean, CleanArea, GetCleanState
1113
from .error import GetError
12-
from .fan_speed import GetFanSpeed
14+
from .fan_speed import GetCleanSpeed, SetCleanSpeed
1315
from .life_span import GetLifeSpan
1416
from .play_sound import PlaySound
1517
from .pos import GetPos
@@ -20,20 +22,31 @@
2022

2123
__all__ = [
2224
"Charge",
25+
"Clean",
26+
"CleanArea",
27+
"GetBatteryInfo",
2328
"GetChargeState",
29+
"GetCleanSpeed",
30+
"GetCleanState",
2431
"GetCleanSum",
2532
"GetError",
26-
"GetFanSpeed",
2733
"GetLifeSpan",
2834
"GetPos",
2935
"PlaySound",
36+
"SetCleanSpeed",
3037
]
3138

3239
# fmt: off
3340
# ordered by file asc
3441
_COMMANDS: list[type[XmlCommand]] = [
42+
Clean,
43+
CleanArea,
3544
GetError,
45+
GetBatteryInfo,
46+
GetCleanSpeed,
47+
GetCleanState,
3648
GetLifeSpan,
49+
SetCleanSpeed,
3750
PlaySound,
3851
]
3952
# fmt: on
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Battery Info command."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from deebot_client.events import BatteryEvent
8+
from deebot_client.message import HandlingResult
9+
10+
from .common import XmlCommandWithMessageHandling
11+
12+
if TYPE_CHECKING:
13+
from xml.etree.ElementTree import Element
14+
15+
from deebot_client.event_bus import EventBus
16+
17+
18+
class GetBatteryInfo(XmlCommandWithMessageHandling):
19+
"""GetBatteryInfo command."""
20+
21+
NAME = "GetBatteryInfo"
22+
23+
@classmethod
24+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
25+
"""Handle xml message and notify the correct event subscribers.
26+
27+
:return: A message response
28+
"""
29+
if (
30+
xml.attrib.get("ret") != "ok"
31+
or (battery := xml.find("battery")) is None
32+
or (power := battery.attrib.get("power")) is None
33+
):
34+
return HandlingResult.analyse()
35+
36+
if power:
37+
event_bus.notify(BatteryEvent(int(power)))
38+
return HandlingResult.success()
39+
40+
return HandlingResult.analyse()
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Clean commands."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from deebot_client.events import FanSpeedEvent, FanSpeedLevel, StateEvent
8+
from deebot_client.message import HandlingResult
9+
from deebot_client.models import CleanAction, CleanMode, State
10+
11+
from .common import ExecuteCommand, XmlCommandWithMessageHandling
12+
13+
if TYPE_CHECKING:
14+
from xml.etree.ElementTree import Element
15+
16+
from deebot_client.event_bus import EventBus
17+
18+
19+
class Clean(ExecuteCommand):
20+
"""Generic start/pause/stop cleaning command."""
21+
22+
NAME = "Clean"
23+
HAS_SUB_ELEMENT = True
24+
25+
def __init__(
26+
self, action: CleanAction, speed: FanSpeedLevel = FanSpeedLevel.NORMAL
27+
) -> None:
28+
# <ctl><clean type='SpotArea' act='s' speed='standard' deep='1' mid='4,5'/></ctl>
29+
30+
super().__init__(
31+
{
32+
"type": CleanMode.AUTO.xml_value,
33+
"act": action.xml_value,
34+
"speed": speed.xml_value,
35+
}
36+
)
37+
38+
39+
class CleanArea(ExecuteCommand):
40+
"""Clean area command."""
41+
42+
NAME = "Clean"
43+
HAS_SUB_ELEMENT = True
44+
45+
def __init__(
46+
self,
47+
mode: CleanMode,
48+
area: str,
49+
cleanings: int = 1,
50+
speed: FanSpeedLevel = FanSpeedLevel.NORMAL,
51+
) -> None:
52+
# <ctl><clean type='SpotArea' act='s' speed='standard' deep='1' mid='4,5'/></ctl>
53+
54+
super().__init__(
55+
{
56+
"type": mode.xml_value,
57+
"act": CleanAction.START.xml_value,
58+
"speed": speed.xml_value,
59+
"deep": str(cleanings),
60+
"mid": area,
61+
}
62+
)
63+
64+
65+
class GetCleanState(XmlCommandWithMessageHandling):
66+
"""GetCleanState command."""
67+
68+
NAME = "GetCleanState"
69+
70+
@classmethod
71+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
72+
"""Handle xml message and notify the correct event subscribers.
73+
74+
:return: A message response
75+
"""
76+
if xml.attrib.get("ret") != "ok" or (clean := xml.find("clean")) is None:
77+
return HandlingResult.analyse()
78+
79+
speed_attrib = clean.attrib.get("speed")
80+
if speed_attrib is not None:
81+
fan_speed_level = FanSpeedLevel.from_xml(speed_attrib)
82+
event_bus.notify(FanSpeedEvent(fan_speed_level))
83+
84+
clean_attrib = clean.attrib.get("st")
85+
if clean_attrib is not None:
86+
clean_action = CleanAction.from_xml(clean_attrib)
87+
if clean_action == CleanAction.START:
88+
event_bus.notify(StateEvent(State.CLEANING))
89+
return HandlingResult.success()

deebot_client/commands/xml/common.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
from __future__ import annotations
44

55
from abc import ABC, abstractmethod
6-
from typing import TYPE_CHECKING, cast
6+
from typing import TYPE_CHECKING, Any, cast
77
from xml.etree.ElementTree import Element, SubElement
88

99
from defusedxml import ElementTree # type: ignore[import-untyped]
1010

11-
from deebot_client.command import Command, CommandWithMessageHandling, SetCommand
11+
from deebot_client.command import (
12+
Command,
13+
CommandMqttP2P,
14+
CommandWithMessageHandling,
15+
GetCommand,
16+
SetCommand,
17+
)
1218
from deebot_client.const import DataType
1319
from deebot_client.logging_filter import get_logger
1420
from deebot_client.message import HandlingResult, HandlingState, MessageStr
@@ -81,8 +87,41 @@ def _handle_xml(cls, _: EventBus, xml: Element) -> HandlingResult:
8187
return HandlingResult(HandlingState.FAILED)
8288

8389

84-
class XmlSetCommand(ExecuteCommand, SetCommand, ABC):
90+
class XmlCommandMqttP2P(XmlCommand, CommandMqttP2P, ABC):
91+
"""Json base command for mqtt p2p channel."""
92+
93+
@classmethod
94+
def create_from_mqtt(cls, payload: str | bytes | bytearray) -> CommandMqttP2P:
95+
"""Create a command from the mqtt data."""
96+
xml = ElementTree.fromstring(payload)
97+
return cls._create_from_mqtt(xml.attrib)
98+
99+
def handle_mqtt_p2p(
100+
self, event_bus: EventBus, response_payload: str | bytes | bytearray
101+
) -> None:
102+
"""Handle response received over the mqtt channel "p2p"."""
103+
self._handle_mqtt_p2p(event_bus, str(response_payload))
104+
105+
@abstractmethod
106+
def _handle_mqtt_p2p(
107+
self, event_bus: EventBus, response: dict[str, Any] | str
108+
) -> None:
109+
"""Handle response received over the mqtt channel "p2p"."""
110+
111+
112+
class XmlSetCommand(ExecuteCommand, SetCommand, XmlCommandMqttP2P, ABC):
85113
"""Xml base set command.
86114
87115
Command needs to be linked to the "get" command, for handling (updating) the sensors.
88116
"""
117+
118+
119+
class XmlGetCommand(XmlCommandWithMessageHandling, GetCommand, ABC):
120+
"""Xml get command."""
121+
122+
@classmethod
123+
@abstractmethod
124+
def handle_set_args(
125+
cls, event_bus: EventBus, args: dict[str, Any]
126+
) -> HandlingResult:
127+
"""Handle arguments of set command."""

deebot_client/commands/xml/fan_speed.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,37 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING
5+
from types import MappingProxyType
6+
from typing import TYPE_CHECKING, Any
67

8+
from deebot_client.command import InitParam
79
from deebot_client.events import FanSpeedEvent, FanSpeedLevel
810
from deebot_client.message import HandlingResult
911

10-
from .common import XmlCommandWithMessageHandling
12+
from .common import XmlGetCommand, XmlSetCommand
1113

1214
if TYPE_CHECKING:
1315
from xml.etree.ElementTree import Element
1416

1517
from deebot_client.event_bus import EventBus
1618

1719

18-
class GetFanSpeed(XmlCommandWithMessageHandling):
19-
"""GetFanSpeed command."""
20+
class GetCleanSpeed(XmlGetCommand):
21+
"""GetCleanSpeed command."""
2022

2123
NAME = "GetCleanSpeed"
2224

25+
@classmethod
26+
def handle_set_args(
27+
cls, event_bus: EventBus, args: dict[str, Any]
28+
) -> HandlingResult:
29+
"""Handle message->body->data and notify the correct event subscribers.
30+
31+
:return: A message response
32+
"""
33+
event_bus.notify(FanSpeedEvent(FanSpeedLevel.from_xml(str(args["speed"]))))
34+
return HandlingResult.success()
35+
2336
@classmethod
2437
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
2538
"""Handle xml message and notify the correct event subscribers.
@@ -29,16 +42,18 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
2942
if xml.attrib.get("ret") != "ok" or not (speed := xml.attrib.get("speed")):
3043
return HandlingResult.analyse()
3144

32-
event: FanSpeedEvent | None = None
45+
event_bus.notify(FanSpeedEvent(FanSpeedLevel.from_xml(speed)))
46+
return HandlingResult.success()
47+
3348

34-
match speed.lower():
35-
case "standard":
36-
event = FanSpeedEvent(FanSpeedLevel.NORMAL)
37-
case "strong":
38-
event = FanSpeedEvent(FanSpeedLevel.MAX)
49+
class SetCleanSpeed(XmlSetCommand):
50+
"""SetCleanSpeed command."""
3951

40-
if event:
41-
event_bus.notify(event)
42-
return HandlingResult.success()
52+
NAME = "SetCleanSpeed"
53+
get_command = GetCleanSpeed
54+
_mqtt_params = MappingProxyType({"speed": InitParam(FanSpeedLevel)})
4355

44-
return HandlingResult.analyse()
56+
def __init__(self, speed: FanSpeedLevel | str) -> None:
57+
if isinstance(speed, FanSpeedLevel):
58+
speed = speed.xml_value
59+
super().__init__({"speed": speed})

deebot_client/events/fan_speed.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from enum import IntEnum, unique
6+
from enum import unique
7+
8+
from deebot_client.util.enum import IntEnumWithXml
79

810
from .base import Event
911

1012

1113
@unique
12-
class FanSpeedLevel(IntEnum):
14+
class FanSpeedLevel(IntEnumWithXml):
1315
"""Enum class for all possible fan speed levels."""
1416

1517
# Values should be sort from low to high on their meanings
16-
QUIET = 1000
17-
NORMAL = 0
18-
MAX = 1
19-
MAX_PLUS = 2
18+
QUIET = 1000, ""
19+
NORMAL = 0, "standard"
20+
MAX = 1, "strong"
21+
MAX_PLUS = 2, ""
2022

2123

2224
@dataclass(frozen=True)

0 commit comments

Comments
 (0)