Skip to content

Commit a4fd6db

Browse files
committed
Add XML Map commands
1 parent cab2f1b commit a4fd6db

3 files changed

Lines changed: 293 additions & 1 deletion

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .error import GetError
1212
from .fan_speed import GetFanSpeed
1313
from .life_span import GetLifeSpan
14+
from .map import GetMapM, GetMapSet, GetMapSt, GetTrM, PullM, PullMP
1415
from .play_sound import PlaySound
1516
from .pos import GetPos
1617
from .stats import GetCleanSum
@@ -25,16 +26,28 @@
2526
"GetError",
2627
"GetFanSpeed",
2728
"GetLifeSpan",
29+
"GetMapM",
30+
"GetMapSet",
31+
"GetMapSt",
2832
"GetPos",
33+
"GetTrM",
2934
"PlaySound",
35+
"PullM",
36+
"PullMP",
3037
]
3138

3239
# fmt: off
3340
# ordered by file asc
3441
_COMMANDS: list[type[XmlCommand]] = [
3542
GetError,
3643
GetLifeSpan,
44+
GetMapM,
45+
GetMapSet,
46+
GetMapSt,
47+
GetTrM,
3748
PlaySound,
49+
PullM,
50+
PullMP
3851
]
3952
# fmt: on
4053

deebot_client/commands/xml/map.py

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

0 commit comments

Comments
 (0)