Skip to content

Commit 475a1e5

Browse files
nanomadedenhaus
andauthored
Add XML Map commands (#909)
Co-authored-by: Robert Resch <robert@resch.dev>
1 parent 8ed90f2 commit 475a1e5

4 files changed

Lines changed: 583 additions & 2 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ repos:
3232
hooks:
3333
- id: codespell
3434
args:
35-
- --ignore-words-list=deebot
35+
- --ignore-words-list=deebot,MapP
3636
- --skip="./.*,*.csv,*.json"
3737
- --quiet-level=2
3838
- --exclude-file=deebot_client/util/continents.py

deebot_client/commands/xml/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .clean_speed import GetCleanSpeed, SetCleanSpeed
1414
from .error import GetError
1515
from .life_span import GetLifeSpan
16+
from .map import GetMapM, GetMapSet, GetMapSt, GetTrM, PullM, PullMP
1617
from .play_sound import PlaySound
1718
from .pos import GetChargerPos, GetPos
1819
from .stats import GetCleanSum
@@ -30,8 +31,14 @@
3031
"GetCleanSum",
3132
"GetError",
3233
"GetLifeSpan",
34+
"GetMapM",
35+
"GetMapSet",
36+
"GetMapSt",
3337
"GetPos",
38+
"GetTrM",
3439
"PlaySound",
40+
"PullM",
41+
"PullMP",
3542
"SetCleanSpeed",
3643
]
3744

@@ -53,12 +60,19 @@
5360

5461
GetLifeSpan,
5562

63+
GetMapM,
64+
GetMapSet,
65+
GetMapSt,
66+
GetTrM,
67+
PullM,
68+
PullMP,
69+
5670
PlaySound,
5771

5872
GetChargerPos,
5973
GetPos,
6074

61-
GetCleanSum
75+
GetCleanSum,
6276
]
6377
# fmt: on
6478

deebot_client/commands/xml/map.py

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
"""Map commands."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from deebot_client.command import Command, CommandResult
8+
from deebot_client.events import MajorMapEvent, MapSetEvent, MapSetType, MinorMapEvent
9+
from deebot_client.events.map import CachedMapInfoEvent, MapSubsetEvent
10+
from deebot_client.message import HandlingResult, HandlingState
11+
12+
from .common import XmlCommandWithMessageHandling
13+
14+
if TYPE_CHECKING:
15+
from typing import Any
16+
from xml.etree.ElementTree import Element
17+
18+
from deebot_client.event_bus import EventBus
19+
20+
21+
class GetMapSt(XmlCommandWithMessageHandling):
22+
"""GetMapSt command.
23+
24+
This command checks whether the current map has been built successfully or not.
25+
"""
26+
27+
NAME = "GetMapSt"
28+
29+
@classmethod
30+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
31+
"""Handle xml message and notify the correct event subscribers.
32+
33+
Sample message response:
34+
"<ctl ret='ok' st='built' method='auto'/>"
35+
36+
:return: A message response
37+
"""
38+
if xml.attrib.get("ret") != "ok" or not (st := xml.attrib.get("st")):
39+
return HandlingResult.analyse()
40+
41+
built = st == "built"
42+
event_bus.notify(CachedMapInfoEvent(name="", active=built))
43+
return HandlingResult.success()
44+
45+
def _handle_response(
46+
self, event_bus: EventBus, response: dict[str, Any]
47+
) -> CommandResult:
48+
"""Handle response from a command.
49+
50+
:return: A message response
51+
"""
52+
result = super()._handle_response(event_bus, response)
53+
if result.state == HandlingState.SUCCESS:
54+
return CommandResult(
55+
result.state,
56+
result.args,
57+
[GetMapSet(entry) for entry in MapSetType],
58+
)
59+
60+
return result
61+
62+
63+
class GetMapSet(XmlCommandWithMessageHandling):
64+
"""GetMapSet command.
65+
66+
This commands gets the list of logical pieces each map is composed of.
67+
XML robots do not support multiple maps, so the mid parameter is ignored.
68+
"""
69+
70+
_ARGS_MSID = "msid"
71+
_ARGS_TYPE = "type"
72+
_ARGS_SUBSETS = "subsets"
73+
74+
NAME = "GetMapSet"
75+
76+
def __init__(
77+
self,
78+
map_set_type: (MapSetType | str) = MapSetType.VIRTUAL_WALLS,
79+
) -> None:
80+
if isinstance(map_set_type, MapSetType):
81+
map_set_type = map_set_type.value
82+
83+
super().__init__({"tp": map_set_type})
84+
85+
@classmethod
86+
def _find_subsets(cls, maps: list[Element]) -> list[int]:
87+
return [int(mid) for map in maps if (mid := map.attrib.get("mid")) is not None]
88+
89+
@classmethod
90+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
91+
"""Handle xml message and notify the correct event subscribers.
92+
93+
Sample message response:
94+
b'<ctl ret="ok" tp="vw" msid="1"><m mid="1" p="1" /><m mid="2" p="1" /></ctl>'
95+
96+
:return: A message response
97+
"""
98+
if (
99+
xml.attrib.get("ret") != "ok"
100+
or not (msid := xml.attrib.get("msid"))
101+
or not (area_type := xml.attrib.get("tp"))
102+
or not MapSetType.has_value(area_type)
103+
):
104+
return HandlingResult.analyse()
105+
106+
xml_subsets = xml.findall("m")
107+
subsets = cls._find_subsets(xml_subsets)
108+
event_bus.notify(MapSetEvent(MapSetType(area_type), subsets=subsets))
109+
args = {
110+
cls._ARGS_MSID: msid,
111+
cls._ARGS_TYPE: area_type,
112+
cls._ARGS_SUBSETS: subsets,
113+
}
114+
return HandlingResult(HandlingState.SUCCESS, args)
115+
116+
def _handle_response(
117+
self, event_bus: EventBus, response: dict[str, Any]
118+
) -> CommandResult:
119+
"""Handle response from a command.
120+
121+
:return: A message response
122+
"""
123+
result = super()._handle_response(event_bus, response)
124+
if result.state == HandlingState.SUCCESS and result.args:
125+
commands: list[Command] = [
126+
PullM(
127+
mid=subset,
128+
msid=result.args[self._ARGS_MSID],
129+
type=result.args[self._ARGS_TYPE],
130+
)
131+
for subset in result.args[self._ARGS_SUBSETS]
132+
]
133+
return CommandResult(result.state, result.args, commands)
134+
135+
return result
136+
137+
138+
class GetMapM(XmlCommandWithMessageHandling):
139+
"""GetMapM command."""
140+
141+
NAME = "GetMapM"
142+
143+
@classmethod
144+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
145+
"""Handle xml message and notify the correct event subscribers.
146+
147+
Sample message response:
148+
b'<ctl i="1245233875" w="100" h="100" r="8" c="8" p="50" m="1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1233223751,1788503751,4083617034,1295764014,1295764014,1295764014,1295764014,1295764014,315201502,2976702022,2972256573,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014" />'
149+
150+
:return: A message response
151+
"""
152+
if not (map_hashes := xml.attrib.get("m")) or not (idx := xml.attrib.get("i")):
153+
return HandlingResult.analyse()
154+
event_bus.notify(
155+
MajorMapEvent(
156+
idx,
157+
values=[int(map_hash.strip()) for map_hash in map_hashes.split(",")],
158+
requested=True,
159+
)
160+
)
161+
return HandlingResult.success()
162+
163+
164+
class PullM(XmlCommandWithMessageHandling):
165+
"""PullM command.
166+
167+
Pulls map subset coordinates
168+
"""
169+
170+
_ARG_COORDS = "coordinates"
171+
172+
NAME = "PullM"
173+
174+
def __init__(
175+
self,
176+
*,
177+
mid: str | int,
178+
msid: str | int,
179+
# pylint: disable=redefined-builtin
180+
type: (MapSetType | str) = MapSetType.ROOMS,
181+
) -> None:
182+
if isinstance(type, MapSetType):
183+
type = type.value
184+
185+
self._map_type = type
186+
self._map_subset_id = int(mid)
187+
188+
super().__init__(
189+
{
190+
"mid": str(mid),
191+
"msid": str(msid),
192+
"tp": type,
193+
"seq": "0",
194+
},
195+
)
196+
197+
@classmethod
198+
def _handle_xml(cls, _event_bus: EventBus, xml: Element) -> HandlingResult:
199+
"""Handle xml message and notify the correct event subscribers.
200+
201+
Sample message responses:
202+
<ctl ret='ok' m='[751,-960,751,-1242,1118,-1242,1118,-960]'/>
203+
<ctl ret='ok' m='-3000,-4400;-3000,-3650;-2450,-3500;-2450,-2400;-2350,-2300;-2350,-1500;-1750,-1500;-1650,-1600;-1250,-1550;-1250,-2300;-1350,-2300;-1600,-2550;-1500,-2750;-1500,-3850;-1750,-3850;-2100,-4200;-2100,-4450;-2150,-4400;-3000,-4400'/>
204+
205+
:return: A message response
206+
"""
207+
if xml.attrib.get("ret") != "ok" or not (coords := xml.attrib.get("m")):
208+
return HandlingResult.analyse()
209+
210+
args = {cls._ARG_COORDS: coords}
211+
return HandlingResult(HandlingState.SUCCESS, args)
212+
213+
def _handle_response(
214+
self, event_bus: EventBus, response: dict[str, Any]
215+
) -> CommandResult:
216+
"""Handle response from a command.
217+
218+
:return: A message response
219+
"""
220+
result = super()._handle_response(event_bus, response)
221+
if result.state == HandlingState.SUCCESS and result.args:
222+
coords = result.args[self._ARG_COORDS]
223+
event_bus.notify(
224+
MapSubsetEvent(
225+
id=self._map_subset_id,
226+
type=MapSetType(self._map_type),
227+
coordinates=coords,
228+
)
229+
)
230+
return CommandResult(result.state, result.args)
231+
232+
return result
233+
234+
235+
class PullMP(XmlCommandWithMessageHandling):
236+
"""PullMP command."""
237+
238+
_ARG_PIECE = "piece"
239+
240+
NAME = "PullMP"
241+
242+
def __init__(self, *, piece_index: int) -> None:
243+
self._piece_index = piece_index
244+
super().__init__({"pid": str(piece_index)})
245+
246+
@classmethod
247+
def _handle_xml(cls, _: EventBus, xml: Element) -> HandlingResult:
248+
"""Handle xml message and notify the correct event subscribers.
249+
250+
Sample message response:
251+
b{'ret': 'ok', 'i': '1839263381', 'p': 'x_q_a_a_b_a_a_q_jw_a_a_a_a_bv/f//o7f/_rz5_i_f_x_i5_y_v_g4kijmo4_y_h+e7k_ho_l_t_l8_u6_p_a_f_ls_x7_jhrz0_kg_a=', 'event': 'pull_m_p'}
252+
253+
:return: A message response
254+
"""
255+
if xml.attrib.get("ret") != "ok" or not (piece := xml.attrib.get("p")):
256+
return HandlingResult.analyse()
257+
args = {cls._ARG_PIECE: piece}
258+
return HandlingResult(HandlingState.SUCCESS, args)
259+
260+
def _handle_response(
261+
self, event_bus: EventBus, response: dict[str, Any]
262+
) -> CommandResult:
263+
"""Handle response from a command.
264+
265+
:return: A message response
266+
"""
267+
result = super()._handle_response(event_bus, response)
268+
if result.state == HandlingState.SUCCESS and result.args:
269+
piece = result.args[self._ARG_PIECE]
270+
event_bus.notify(MinorMapEvent(index=self._piece_index, value=piece))
271+
return CommandResult(result.state, result.args)
272+
273+
return result
274+
275+
276+
class GetTrM(XmlCommandWithMessageHandling):
277+
"""GetTrM command.
278+
279+
Enables trace reporting from the bot.
280+
"""
281+
282+
NAME = "GetTrM"
283+
284+
@classmethod
285+
def _handle_xml(cls, _: EventBus, xml: Element) -> HandlingResult:
286+
"""Handle xml message and notify the correct event subscribers.
287+
288+
:return: A message response
289+
"""
290+
if xml.attrib.get("ret") != "ok":
291+
return HandlingResult.analyse()
292+
return HandlingResult(HandlingState.SUCCESS)

0 commit comments

Comments
 (0)