Skip to content

Commit 5b6d68b

Browse files
authored
Add XML Clean Messages (#930)
1 parent 5830188 commit 5b6d68b

6 files changed

Lines changed: 397 additions & 28 deletions

File tree

deebot_client/commands/xml/clean.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
from typing import TYPE_CHECKING
66

7-
from deebot_client.events import FanSpeedEvent, FanSpeedLevel, StateEvent
7+
from deebot_client.events import FanSpeedLevel
88
from deebot_client.message import HandlingResult
9-
from deebot_client.models import CleanAction, CleanMode, State
9+
from deebot_client.messages.xml import CleanReport
10+
from deebot_client.models import CleanAction, CleanMode
1011

1112
from .common import ExecuteCommand, XmlCommandWithMessageHandling
1213

@@ -58,7 +59,7 @@ def __init__(
5859
)
5960

6061

61-
class GetCleanState(XmlCommandWithMessageHandling):
62+
class GetCleanState(XmlCommandWithMessageHandling, CleanReport):
6263
"""GetCleanState command."""
6364

6465
NAME = "GetCleanState"
@@ -69,22 +70,7 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
6970
7071
:return: A message response
7172
"""
72-
if xml.attrib.get("ret") != "ok" or (clean := xml.find("clean")) is None:
73+
if xml.attrib.get("ret") != "ok":
7374
return HandlingResult.analyse()
7475

75-
speed_attrib = clean.attrib.get("speed")
76-
if speed_attrib is not None:
77-
fan_speed_level = FanSpeedLevel.from_xml(speed_attrib)
78-
event_bus.notify(FanSpeedEvent(fan_speed_level))
79-
80-
clean_attrib = clean.attrib.get("st")
81-
if clean_attrib is not None:
82-
clean_action = CleanAction.from_xml(clean_attrib)
83-
if clean_action == CleanAction.START:
84-
event_bus.notify(StateEvent(State.CLEANING))
85-
elif clean_action == CleanAction.PAUSE:
86-
event_bus.notify(StateEvent(State.PAUSED))
87-
elif clean_action in (CleanAction.RESUME, CleanAction.STOP):
88-
event_bus.notify(StateEvent(State.IDLE))
89-
90-
return HandlingResult.success()
76+
return cls._parse_xml(event_bus, xml)

deebot_client/messages/xml/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
from deebot_client.messages.xml.battery import BatteryInfo
88
from deebot_client.messages.xml.charge_state import ChargeState
9+
from deebot_client.messages.xml.clean import (
10+
CleanedPos,
11+
CleanReport,
12+
CleanReportServer,
13+
CleanSt,
14+
)
915
from deebot_client.messages.xml.map import MapP, Trace
1016
from deebot_client.messages.xml.pos import Pos
1117

@@ -14,14 +20,29 @@
1420

1521
from deebot_client.message import Message
1622

17-
__all__: Sequence[str] = ["BatteryInfo", "ChargeState", "MapP", "Pos", "Trace"]
23+
__all__: Sequence[str] = [
24+
"BatteryInfo",
25+
"ChargeState",
26+
"CleanReport",
27+
"CleanReportServer",
28+
"CleanSt",
29+
"CleanedPos",
30+
"MapP",
31+
"Pos",
32+
"Trace",
33+
]
1834
# fmt: off
1935
# ordered by file asc
2036
_MESSAGES: list[type[Message]] = [
2137
BatteryInfo,
2238

2339
ChargeState,
2440

41+
CleanedPos,
42+
CleanReport,
43+
CleanReportServer,
44+
CleanSt,
45+
2546
MapP,
2647
Trace,
2748

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Clean messages."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from deebot_client.events import (
8+
CleanJobStatus,
9+
FanSpeedEvent,
10+
FanSpeedLevel,
11+
ReportStatsEvent,
12+
StateEvent,
13+
StatsEvent,
14+
)
15+
from deebot_client.logging_filter import get_logger
16+
from deebot_client.message import HandlingResult
17+
from deebot_client.messages.xml.common import XmlMessage
18+
from deebot_client.messages.xml.pos import Pos
19+
from deebot_client.models import CleanAction, State
20+
21+
if TYPE_CHECKING:
22+
from xml.etree.ElementTree import Element
23+
24+
from deebot_client.event_bus import EventBus
25+
26+
_LOGGER = get_logger(__name__)
27+
28+
29+
class CleanSt(XmlMessage):
30+
"""CleanSt message."""
31+
32+
NAME = "CleanSt"
33+
34+
@classmethod
35+
def _handle_xml(cls, _event_bus: EventBus, _xml: Element) -> HandlingResult:
36+
"""Handle xml message and notify the correct event subscribers.
37+
38+
b"<ctl td='CleanSt' a='21' s='1743945874' l='1595' t='' type='auto'/>"
39+
40+
We currently ignore this message as we prefer to use CleanReport
41+
:return: A message response
42+
"""
43+
return HandlingResult.success()
44+
45+
46+
class CleanReport(XmlMessage):
47+
"""CleanReport message."""
48+
49+
NAME = "CleanReport"
50+
51+
@classmethod
52+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
53+
"""Handle xml message and notify the correct event subscribers.
54+
55+
b"<ctl ts='1744467249311' td='CleanReport'><clean type='auto' speed='standard' st='s' rsn='a' a='' l='' sts=''/></ctl>"
56+
57+
:return: A message response
58+
"""
59+
return cls._parse_xml(event_bus, xml)
60+
61+
@classmethod
62+
def _parse_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
63+
if (clean := xml.find("clean")) is None:
64+
return HandlingResult.analyse()
65+
66+
event_reported = False
67+
speed_attrib = clean.attrib.get("speed")
68+
if speed_attrib is not None:
69+
fan_speed_level = FanSpeedLevel.from_xml(speed_attrib)
70+
event_bus.notify(FanSpeedEvent(fan_speed_level))
71+
event_reported = True
72+
73+
clean_attrib = clean.attrib.get("st")
74+
if clean_attrib is not None:
75+
clean_action = CleanAction.from_xml(clean_attrib)
76+
match clean_action:
77+
case CleanAction.START | CleanAction.RESUME:
78+
event_bus.notify(StateEvent(State.CLEANING))
79+
event_reported = True
80+
case CleanAction.PAUSE:
81+
event_bus.notify(StateEvent(State.PAUSED))
82+
event_reported = True
83+
case CleanAction.STOP:
84+
event_bus.notify(StateEvent(State.IDLE))
85+
event_reported = True
86+
87+
if event_reported:
88+
return HandlingResult.success()
89+
return HandlingResult.analyse()
90+
91+
92+
class CleanReportServer(XmlMessage):
93+
"""CleanReportServer message."""
94+
95+
NAME = "CleanReportServer"
96+
97+
@classmethod
98+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
99+
"""Handle xml message and notify the correct event subscribers.
100+
101+
b"<ctl ts='1744467262312' td='CleanReportServer' act='s' type='auto' cs='1134230540'/>"
102+
b"<ctl ts='1744467393682' td='CleanReportServer' act='h' type='auto' sts='1744467262' cs='1134230540' area='1' last='76' mapCount='6'/>"
103+
104+
:return: A message response
105+
"""
106+
event_reported = False
107+
if (act := xml.attrib.get("act")) is not None:
108+
if (last := xml.attrib.get("last")) is not None and last.isdecimal():
109+
last_int = int(last)
110+
else:
111+
last_int = None
112+
113+
if (area := xml.attrib.get("area")) is not None and area.isdecimal():
114+
area_int = int(area)
115+
else:
116+
area_int = None
117+
118+
clean_session = xml.attrib.get("cs")
119+
clean_type = xml.attrib.get("type")
120+
121+
clean_action = CleanAction.from_xml(act)
122+
if clean_action == CleanAction.STOP:
123+
event_bus.notify(
124+
StatsEvent(area=area_int, time=last_int, type=clean_type)
125+
)
126+
event_reported = True
127+
if clean_session:
128+
job_status = CleanJobStatus.NO_STATUS
129+
match clean_action:
130+
case CleanAction.STOP:
131+
job_status = CleanJobStatus.FINISHED
132+
case CleanAction.START | CleanAction.RESUME:
133+
job_status = CleanJobStatus.CLEANING
134+
case CleanAction.PAUSE:
135+
job_status = CleanJobStatus.MANUALLY_STOPPED
136+
137+
event_bus.notify(
138+
ReportStatsEvent(
139+
area=area_int,
140+
time=last_int,
141+
type=clean_type,
142+
cleaning_id=clean_session,
143+
status=job_status,
144+
content=[],
145+
)
146+
)
147+
event_reported = True
148+
149+
if event_reported:
150+
return HandlingResult.success()
151+
return HandlingResult.analyse()
152+
153+
154+
class CleanedPos(Pos):
155+
"""CleanedPos message.
156+
157+
We treat it as an alias of a standard Pos message.
158+
It is emitted while the bot is actively cleaning
159+
"""
160+
161+
NAME = "CleanedPos"

tests/commands/xml/test_clean.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async def test_CleanArea(command: CleanArea) -> None:
6161
),
6262
(
6363
"st='r'",
64-
[StateEvent(State.IDLE)],
64+
[StateEvent(State.CLEANING)],
6565
),
6666
(
6767
"st='h'",
@@ -76,7 +76,7 @@ async def test_CleanArea(command: CleanArea) -> None:
7676
"standard_cleaning",
7777
"strong_cleaning",
7878
"paused",
79-
"resume/idle",
79+
"resume/cleaning",
8080
"stop/idle",
8181
"fanspeed_only",
8282
],

tests/messages/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@
1414
def assert_message(
1515
message: type[Message],
1616
data: MessagePayloadType,
17-
expected_events: Event | Sequence[Event],
17+
expected_events: Event | None | Sequence[Event],
1818
) -> None:
1919
event_bus = Mock(spec_set=EventBus)
2020

2121
result = message.handle(event_bus, data)
2222

2323
assert result.state == HandlingState.SUCCESS
24-
if isinstance(expected_events, Sequence):
25-
event_bus.notify.assert_has_calls([call(x) for x in expected_events])
26-
assert event_bus.notify.call_count == len(expected_events)
24+
if expected_events:
25+
if isinstance(expected_events, Sequence):
26+
event_bus.notify.assert_has_calls([call(x) for x in expected_events])
27+
assert event_bus.notify.call_count == len(expected_events)
28+
else:
29+
event_bus.notify.assert_called_once_with(expected_events)
2730
else:
28-
event_bus.notify.assert_called_once_with(expected_events)
31+
event_bus.notify.assert_not_called()
2932

3033

3134
def assert_message_failure(

0 commit comments

Comments
 (0)