Skip to content

Commit d52c36e

Browse files
committed
feat: add GOAT A3000 LiDAR support (cr0e4u / 51rcxt)
Add support for the Ecovacs GOAT A3000 LiDAR mower (cr0e4u) and its Pro variant (51rcxt). New hardware definitions: - deebot_client/hardware/cr0e4u.py: full capability definition for the GOAT A3000 LiDAR including CleanMower, CleanMowerArea, CapabilityMap, blade + lens-brush life-span, and all mower-specific settings - deebot_client/hardware/51rcxt.py: delegates to cr0e4u (identical FW) New command: - deebot_client/commands/json/mow.py: CleanMowerArea sends the cleanMower command with a specific zone_id for targeted area mowing New message handlers: - deebot_client/messages/json/mow.py: OnMI + OnArI handlers reassemble chunked LZMA zone-polygon payloads and fire MapSubsetEvent / MapChangedEvent so the HA integration can render the zone map - deebot_client/messages/json/mower_telemetry.py: OnPos, OnCleanInfo, and OnStats handle mower-specific push telemetry (position, mow state, and progress statistics) mapping to PositionsEvent, StateEvent, and StatsEvent respectively Updated: - deebot_client/messages/json/__init__.py: register OnMI, OnArI, OnPos, OnCleanInfo, and OnStats in the MESSAGES dispatch table
1 parent 846a10f commit d52c36e

10 files changed

Lines changed: 1043 additions & 136 deletions

File tree

deebot_client/commands/json/clean.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,29 @@ def _get_args(self, action: CleanAction) -> dict[str, Any]:
108108
return args
109109

110110

111+
class CleanMower(Clean):
112+
"""Auto-mow command for lawn mower devices (GOAT A3000 LiDAR and similar).
113+
114+
Uses the ``clean`` endpoint with a V2-style ``content`` dict, matching
115+
the protocol confirmed via traffic analysis of the GOAT A3000 (cr0e4u):
116+
117+
.. code-block:: json
118+
119+
{"act": "start", "content": {"type": "auto"}}
120+
{"act": "stop", "content": {"type": ""}}
121+
"""
122+
123+
def _get_args(self, action: CleanAction) -> dict[str, Any]:
124+
content: dict[str, str] = {}
125+
args = {"act": action.value, "content": content}
126+
match action:
127+
case CleanAction.START | CleanAction.RESUME:
128+
content["type"] = CleanMode.AUTO.value
129+
case CleanAction.STOP | CleanAction.PAUSE:
130+
content["type"] = ""
131+
return args
132+
133+
111134
class GetCleanInfo(JsonCommandWithMessageHandling, MessageBodyDataDict):
112135
"""Get clean info command."""
113136

deebot_client/commands/json/mow.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Mow commands for GOAT A3000 LiDAR mower (cr0e4u).
2+
3+
This module contributes:
4+
5+
* :class:`CleanMowerArea` — zone/area mowing targeting a specific set of zone IDs
6+
* :class:`GetMI` — triggers the mower to stream zone-map chunks via ``onMI``
7+
8+
:class:`CleanMower` (the auto-mow base command) lives in
9+
:mod:`deebot_client.commands.json.clean` and is imported from there.
10+
11+
All payloads confirmed via traffic analysis of the Ecovacs GOAT A3000 (cr0e4u).
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from typing import Any
17+
18+
from deebot_client.commands.json.common import ExecuteCommand
19+
from deebot_client.models import CleanAction, CleanMode
20+
21+
from .clean import CleanMower
22+
23+
24+
class CleanMowerArea(CleanMower):
25+
"""Zone / area mow command for GOAT mower devices.
26+
27+
Extends :class:`CleanMower` to target a specific zone or set of zones::
28+
29+
{"act": "start", "content": {"type": "spotArea", "value": "0,1"}}
30+
31+
``mode`` is typically ``CleanMode.SPOT_AREA`` for standard zone mowing.
32+
``area`` is the list of integer zone IDs to mow (0-indexed).
33+
"""
34+
35+
def __init__(
36+
self,
37+
mode: CleanMode,
38+
area: list[int | float],
39+
_cleanings: int = 1,
40+
) -> None:
41+
self._additional_content: dict[str, str] = {
42+
"type": mode.value,
43+
"value": ",".join(str(i) for i in area),
44+
}
45+
super().__init__(CleanAction.START)
46+
47+
def _get_args(self, action: CleanAction) -> dict[str, Any]:
48+
args = super()._get_args(action)
49+
if action == CleanAction.START:
50+
args["content"].update(self._additional_content)
51+
return args
52+
53+
54+
class GetMI(ExecuteCommand):
55+
"""Request the mower to push zone-map chunks via ``onMI``.
56+
57+
Sending ``getMI`` causes the robot to re-stream all ``onMI`` chunks
58+
from index 0, equivalent to what the Ecovacs app does on every connect.
59+
The resulting push messages are handled by :class:`OnMI` in
60+
``deebot_client.messages.json.mow``.
61+
62+
Confirmed via traffic analysis of the GOAT A3000 (cr0e4u):
63+
64+
.. code-block:: json
65+
66+
{"body": {"data": {"type": "ar"}}}
67+
68+
Args:
69+
area_type: Map data type to request.
70+
``"ar"`` — zone polygons (default, used for initial map load)
71+
``"vw"`` — virtual walls
72+
``"nc"`` — navigation coordinates
73+
"""
74+
75+
NAME = "getMI"
76+
77+
def __init__(self, area_type: str = "ar") -> None:
78+
super().__init__({"type": area_type})

deebot_client/hardware/51rcxt.py

Lines changed: 14 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,21 @@
1-
"""GOAT A3000 LiDAR Pro."""
1+
"""Ecovacs GOAT A3000 LiDAR Pro (51rcxt) capabilities.
2+
3+
The GOAT A3000 LiDAR Pro shares the same firmware stack, protocol and
4+
capabilities as the GOAT A3000 LiDAR (cr0e4u). Both models use the
5+
``cleanMower`` command format, the ``onMI``/``onArI`` zone-map protocol,
6+
and identical mower settings.
7+
8+
This module delegates to the cr0e4u definition so both device classes
9+
share a single source of truth.
10+
"""
211

312
from __future__ import annotations
413

5-
from deebot_client.capabilities import (
6-
Capabilities,
7-
CapabilityClean,
8-
CapabilityCleanAction,
9-
CapabilityCustomCommand,
10-
CapabilityEvent,
11-
CapabilityExecute,
12-
CapabilityLifeSpan,
13-
CapabilitySet,
14-
CapabilitySetEnable,
15-
CapabilitySettings,
16-
CapabilityStats,
17-
DeviceType,
18-
)
19-
from deebot_client.commands.json import (
20-
GetBorderSwitch,
21-
GetChildLock,
22-
GetCrossMapBorderWarning,
23-
GetCutDirection,
24-
GetMoveUpWarning,
25-
GetSafeProtect,
26-
SetBorderSwitch,
27-
SetChildLock,
28-
SetCrossMapBorderWarning,
29-
SetCutDirection,
30-
SetMoveUpWarning,
31-
SetSafeProtect,
32-
)
33-
from deebot_client.commands.json.advanced_mode import GetAdvancedMode, SetAdvancedMode
34-
from deebot_client.commands.json.battery import GetBattery
35-
from deebot_client.commands.json.charge import Charge
36-
from deebot_client.commands.json.charge_state import GetChargeState
37-
from deebot_client.commands.json.clean import CleanV2, GetCleanInfoV2
38-
from deebot_client.commands.json.custom import CustomCommand
39-
from deebot_client.commands.json.error import GetError
40-
from deebot_client.commands.json.life_span import GetLifeSpan, ResetLifeSpan
41-
from deebot_client.commands.json.network import GetNetInfo
42-
from deebot_client.commands.json.play_sound import PlaySound
43-
from deebot_client.commands.json.stats import GetStats, GetTotalStats
44-
from deebot_client.commands.json.true_detect import GetTrueDetect, SetTrueDetect
45-
from deebot_client.commands.json.volume import GetVolume, SetVolume
46-
from deebot_client.const import DataType
47-
from deebot_client.events import (
48-
AdvancedModeEvent,
49-
AvailabilityEvent,
50-
BatteryEvent,
51-
BorderSwitchEvent,
52-
ChildLockEvent,
53-
CrossMapBorderWarningEvent,
54-
CustomCommandEvent,
55-
CutDirectionEvent,
56-
ErrorEvent,
57-
LifeSpan,
58-
LifeSpanEvent,
59-
MoveUpWarningEvent,
60-
NetworkInfoEvent,
61-
ReportStatsEvent,
62-
SafeProtectEvent,
63-
StateEvent,
64-
StatsEvent,
65-
TotalStatsEvent,
66-
TrueDetectEvent,
67-
VolumeEvent,
68-
)
6914
from deebot_client.models import StaticDeviceInfo
7015

16+
from .cr0e4u import get_device_info as _cr0e4u_get_device_info
17+
7118

7219
def get_device_info() -> StaticDeviceInfo:
73-
"""Get device info for this model."""
74-
return StaticDeviceInfo(
75-
DataType.JSON,
76-
Capabilities(
77-
device_type=DeviceType.MOWER,
78-
availability=CapabilityEvent(
79-
AvailabilityEvent, [GetBattery(is_available_check=True)]
80-
),
81-
battery=CapabilityEvent(BatteryEvent, [GetBattery()]),
82-
charge=CapabilityExecute(Charge),
83-
clean=CapabilityClean(
84-
action=CapabilityCleanAction(command=CleanV2),
85-
),
86-
custom=CapabilityCustomCommand(
87-
event=CustomCommandEvent, get=[], set=CustomCommand
88-
),
89-
error=CapabilityEvent(ErrorEvent, [GetError()]),
90-
life_span=CapabilityLifeSpan(
91-
types=(LifeSpan.BLADE, LifeSpan.LENS_BRUSH),
92-
event=LifeSpanEvent,
93-
get=[
94-
GetLifeSpan(
95-
[
96-
LifeSpan.BLADE,
97-
LifeSpan.LENS_BRUSH,
98-
]
99-
)
100-
],
101-
reset=ResetLifeSpan,
102-
),
103-
network=CapabilityEvent(NetworkInfoEvent, [GetNetInfo()]),
104-
play_sound=CapabilityExecute(PlaySound),
105-
settings=CapabilitySettings(
106-
advanced_mode=CapabilitySetEnable(
107-
AdvancedModeEvent, [GetAdvancedMode()], SetAdvancedMode
108-
),
109-
border_switch=CapabilitySetEnable(
110-
BorderSwitchEvent, [GetBorderSwitch()], SetBorderSwitch
111-
),
112-
cut_direction=CapabilitySet(
113-
CutDirectionEvent, [GetCutDirection()], SetCutDirection
114-
),
115-
child_lock=CapabilitySetEnable(
116-
ChildLockEvent, [GetChildLock()], SetChildLock
117-
),
118-
moveup_warning=CapabilitySetEnable(
119-
MoveUpWarningEvent, [GetMoveUpWarning()], SetMoveUpWarning
120-
),
121-
cross_map_border_warning=CapabilitySetEnable(
122-
CrossMapBorderWarningEvent,
123-
[GetCrossMapBorderWarning()],
124-
SetCrossMapBorderWarning,
125-
),
126-
safe_protect=CapabilitySetEnable(
127-
SafeProtectEvent, [GetSafeProtect()], SetSafeProtect
128-
),
129-
true_detect=CapabilitySetEnable(
130-
TrueDetectEvent, [GetTrueDetect()], SetTrueDetect
131-
),
132-
volume=CapabilitySet(VolumeEvent, [GetVolume()], SetVolume),
133-
),
134-
state=CapabilityEvent(StateEvent, [GetChargeState(), GetCleanInfoV2()]),
135-
stats=CapabilityStats(
136-
clean=CapabilityEvent(StatsEvent, [GetStats()]),
137-
report=CapabilityEvent(ReportStatsEvent, []),
138-
total=CapabilityEvent(TotalStatsEvent, [GetTotalStats()]),
139-
),
140-
),
141-
)
20+
"""Get device info for the GOAT A3000 LiDAR Pro (51rcxt)."""
21+
return _cr0e4u_get_device_info()

deebot_client/hardware/cr0e4u.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)