Skip to content

Commit dbd13d2

Browse files
committed
Map workflow
1 parent 0c0d8a1 commit dbd13d2

7 files changed

Lines changed: 317 additions & 10 deletions

File tree

deebot_client/capabilities.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,9 @@ class CapabilityMap:
176176
changed: CapabilityEvent[MapChangedEvent]
177177
clear: CapabilityExecute[[]] | None = None
178178
major: CapabilityEvent[MajorMapEvent]
179-
multi_state: CapabilitySetEnable[MultimapStateEvent]
179+
multi_state: CapabilitySetEnable[MultimapStateEvent] | None = None
180180
position: CapabilityEvent[PositionsEvent]
181-
relocation: CapabilityExecute[[]]
181+
relocation: CapabilityExecute[[]] | None = None
182182
rooms: CapabilityEvent[RoomsEvent]
183183
trace: CapabilityEvent[MapTraceEvent]
184184

deebot_client/commands/xml/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .error import GetError
1515
from .fan_speed import GetCleanSpeed, SetCleanSpeed
1616
from .life_span import GetLifeSpan
17+
from .map import GetMapM, GetMapSet, GetMapSt, PullM, PullMP
1718
from .play_sound import PlaySound
1819
from .pos import GetPos
1920
from .stats import GetCleanSum
@@ -34,10 +35,15 @@
3435
"GetCleanSum",
3536
"GetError",
3637
"GetLifeSpan",
38+
"GetMapM",
39+
"GetMapSet",
40+
"GetMapSt",
3741
"GetPos",
3842
"GetWaterBoxInfo",
3943
"GetWaterPermeability",
4044
"PlaySound",
45+
"PullM",
46+
"PullMP",
4147
"SetCleanSpeed",
4248
]
4349

@@ -55,10 +61,15 @@
5561
GetCleanSum,
5662
GetError,
5763
GetLifeSpan,
64+
GetMapM,
65+
GetMapSet,
66+
GetMapSt,
5867
GetPos,
5968
GetWaterBoxInfo,
6069
GetWaterPermeability,
6170
PlaySound,
71+
PullM,
72+
PullMP,
6273
SetCleanSpeed,
6374
]
6475
# fmt: on

deebot_client/commands/xml/map.py

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

deebot_client/events/map.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from enum import Enum, unique
77
from typing import TYPE_CHECKING, Any
88

9+
from deebot_client.const import DataType
910
from deebot_client.events import Event
1011

1112
if TYPE_CHECKING:
@@ -47,6 +48,7 @@ class MajorMapEvent(Event):
4748
map_id: str
4849
values: list[int]
4950
requested: bool = field(kw_only=True)
51+
type: DataType = DataType.JSON
5052

5153

5254
@dataclass(frozen=True)

deebot_client/hardware/deebot/2pv572.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
CapabilityEvent,
1111
CapabilityExecute,
1212
CapabilityLifeSpan,
13+
CapabilityMap,
1314
CapabilitySettings,
1415
CapabilitySetTypes,
1516
CapabilityStats,
@@ -32,6 +33,8 @@
3233
)
3334
from deebot_client.commands.xml.charge_state import GetChargeState
3435
from deebot_client.commands.xml.error import GetError
36+
from deebot_client.commands.xml.map import GetMapM, GetMapSt
37+
from deebot_client.commands.xml.pos import GetPos
3538
from deebot_client.commands.xml.stats import GetCleanSum
3639
from deebot_client.commands.xml.water_info import GetWaterBoxInfo, SetWaterPermeability
3740
from deebot_client.const import DataType
@@ -47,12 +50,20 @@
4750
LifeSpanEvent,
4851
NetworkInfoEvent,
4952
ReportStatsEvent,
53+
RoomsEvent,
5054
StateEvent,
5155
StatsEvent,
5256
TotalStatsEvent,
5357
WaterAmount,
5458
WaterInfoEvent,
5559
)
60+
from deebot_client.events.map import (
61+
CachedMapInfoEvent,
62+
MajorMapEvent,
63+
MapChangedEvent,
64+
MapTraceEvent,
65+
PositionsEvent,
66+
)
5667
from deebot_client.models import StaticDeviceInfo
5768
from deebot_client.util import short_name
5869

@@ -92,6 +103,14 @@
92103
],
93104
reset=CustomCommand,
94105
),
106+
map=CapabilityMap(
107+
cached_info=CapabilityEvent(CachedMapInfoEvent, [GetMapSt()]),
108+
changed=CapabilityEvent(MapChangedEvent, []),
109+
major=CapabilityEvent(MajorMapEvent, [GetMapM()]),
110+
position=CapabilityEvent(PositionsEvent, [GetPos()]),
111+
rooms=CapabilityEvent(RoomsEvent, [GetMapSt()]),
112+
trace=CapabilityEvent(MapTraceEvent, []),
113+
),
95114
network=CapabilityEvent(NetworkInfoEvent, [GetNetInfoLegacy()]),
96115
play_sound=CapabilityExecute(PlaySound),
97116
state=CapabilityEvent(StateEvent, [GetChargeState(), GetCleanState()]),

0 commit comments

Comments
 (0)