Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions deebot_client/commands/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .charge import Charge
from .charge_state import GetChargeState
from .child_lock import GetChildLock, SetChildLock
from .clean import Clean, CleanArea, CleanV2, GetCleanInfo, GetCleanInfoV2
from .clean import Clean, CleanArea, CleanMower, CleanV2, GetCleanInfo, GetCleanInfoV2
from .clean_count import GetCleanCount, SetCleanCount
from .clean_logs import GetCleanLogs
from .clean_preference import GetCleanPreference, SetCleanPreference
Expand All @@ -38,6 +38,7 @@
)
from .mop_auto_wash_frequency import GetMopAutoWashFrequency, SetMopAutoWashFrequency
from .moveup_warning import GetMoveUpWarning, SetMoveUpWarning
from .mow import CleanMowerArea, GetMI
from .multimap_state import GetMultimapState, SetMultimapState
from .network import GetNetInfo, GetNetInfoLegacy
from .ota import GetOta, SetOta
Expand All @@ -62,6 +63,8 @@
"Charge",
"Clean",
"CleanArea",
"CleanMower",
"CleanMowerArea",
"CleanV2",
"ClearMap",
"GetAdvancedMode",
Expand All @@ -84,6 +87,7 @@
"GetError",
"GetFanSpeed",
"GetLifeSpan",
"GetMI",
"GetMajorMap",
"GetMapInfoV2",
"GetMapSet",
Expand Down Expand Up @@ -254,7 +258,10 @@
SetWaterInfo,

GetWorkMode,
SetWorkMode
SetWorkMode,

# GOAT A3000 LiDAR mower commands
GetMI,
]
# fmt: on

Expand Down
23 changes: 23 additions & 0 deletions deebot_client/commands/json/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,29 @@ def _get_args(self, action: CleanAction) -> dict[str, Any]:
return args


class CleanMower(Clean):
"""Auto-mow command for lawn mower devices (GOAT A3000 LiDAR and similar).

Uses the ``clean`` endpoint with a V2-style ``content`` dict, matching
the protocol confirmed via traffic analysis of the GOAT A3000 (cr0e4u):

.. code-block:: json

{"act": "start", "content": {"type": "auto"}}
{"act": "stop", "content": {"type": ""}}
"""
Comment thread
adc103 marked this conversation as resolved.
Comment thread
adc103 marked this conversation as resolved.

def _get_args(self, action: CleanAction) -> dict[str, Any]:
content: dict[str, str] = {}
args = {"act": action.value, "content": content}
match action:
case CleanAction.START | CleanAction.RESUME:
content["type"] = CleanMode.AUTO.value
case CleanAction.STOP | CleanAction.PAUSE:
content["type"] = ""
return args

Comment thread
adc103 marked this conversation as resolved.
Comment on lines +111 to +132

class GetCleanInfo(JsonCommandWithMessageHandling, MessageBodyDataDict):
"""Get clean info command."""

Expand Down
78 changes: 78 additions & 0 deletions deebot_client/commands/json/mow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Mow commands for GOAT A3000 LiDAR mower (cr0e4u).

This module contributes:

* :class:`CleanMowerArea` — zone/area mowing targeting a specific set of zone IDs
* :class:`GetMI` — triggers the mower to stream zone-map chunks via ``onMI``

:class:`CleanMower` (the auto-mow base command) lives in
:mod:`deebot_client.commands.json.clean` and is imported from there.

All payloads confirmed via traffic analysis of the Ecovacs GOAT A3000 (cr0e4u).
"""

from __future__ import annotations

from typing import Any

from deebot_client.commands.json.common import ExecuteCommand
from deebot_client.models import CleanAction, CleanMode

from .clean import CleanMower
Comment thread
adc103 marked this conversation as resolved.


class CleanMowerArea(CleanMower):
"""Zone / area mow command for GOAT mower devices.

Extends :class:`CleanMower` to target a specific zone or set of zones::

{"act": "start", "content": {"type": "spotArea", "value": "0,1"}}

``mode`` is typically ``CleanMode.SPOT_AREA`` for standard zone mowing.
``area`` is the list of integer zone IDs to mow (0-indexed).
"""

def __init__(
self,
mode: CleanMode,
area: list[int | float],
_cleanings: int = 1,
) -> None:
self._additional_content: dict[str, str] = {
"type": mode.value,
"value": ",".join(str(i) for i in area),
}
super().__init__(CleanAction.START)

def _get_args(self, action: CleanAction) -> dict[str, Any]:
args = super()._get_args(action)
if action == CleanAction.START:
args["content"].update(self._additional_content)
return args


class GetMI(ExecuteCommand):
"""Request the mower to push zone-map chunks via ``onMI``.

Sending ``getMI`` causes the robot to re-stream all ``onMI`` chunks
from index 0, equivalent to what the Ecovacs app does on every connect.
The resulting push messages are handled by :class:`OnMI` in
``deebot_client.messages.json.mow``.
Comment thread
adc103 marked this conversation as resolved.

Confirmed via traffic analysis of the GOAT A3000 (cr0e4u):

.. code-block:: json

{"body": {"data": {"type": "ar"}}}

Args:
area_type: Map data type to request.
``"ar"`` — zone polygons (default, used for initial map load)
``"vw"`` — virtual walls
``"nc"`` — navigation coordinates
"""

NAME = "getMI"

def __init__(self, area_type: str = "ar") -> None:
super().__init__({"type": area_type})
148 changes: 14 additions & 134 deletions deebot_client/hardware/51rcxt.py
Original file line number Diff line number Diff line change
@@ -1,141 +1,21 @@
"""GOAT A3000 LiDAR Pro."""
"""Ecovacs GOAT A3000 LiDAR Pro (51rcxt) capabilities.

The GOAT A3000 LiDAR Pro shares the same firmware stack, protocol and
capabilities as the GOAT A3000 LiDAR (cr0e4u). Both models use the
``cleanMower`` command format, the ``onMI``/``onArI`` zone-map protocol,
and identical mower settings.

This module delegates to the cr0e4u definition so both device classes
share a single source of truth.
"""

from __future__ import annotations

from deebot_client.capabilities import (
Capabilities,
CapabilityClean,
CapabilityCleanAction,
CapabilityCustomCommand,
CapabilityEvent,
CapabilityExecute,
CapabilityLifeSpan,
CapabilitySet,
CapabilitySetEnable,
CapabilitySettings,
CapabilityStats,
DeviceType,
)
from deebot_client.commands.json import (
GetBorderSwitch,
GetChildLock,
GetCrossMapBorderWarning,
GetCutDirection,
GetMoveUpWarning,
GetSafeProtect,
SetBorderSwitch,
SetChildLock,
SetCrossMapBorderWarning,
SetCutDirection,
SetMoveUpWarning,
SetSafeProtect,
)
from deebot_client.commands.json.advanced_mode import GetAdvancedMode, SetAdvancedMode
from deebot_client.commands.json.battery import GetBattery
from deebot_client.commands.json.charge import Charge
from deebot_client.commands.json.charge_state import GetChargeState
from deebot_client.commands.json.clean import CleanV2, GetCleanInfoV2
from deebot_client.commands.json.custom import CustomCommand
from deebot_client.commands.json.error import GetError
from deebot_client.commands.json.life_span import GetLifeSpan, ResetLifeSpan
from deebot_client.commands.json.network import GetNetInfo
from deebot_client.commands.json.play_sound import PlaySound
from deebot_client.commands.json.stats import GetStats, GetTotalStats
from deebot_client.commands.json.true_detect import GetTrueDetect, SetTrueDetect
from deebot_client.commands.json.volume import GetVolume, SetVolume
from deebot_client.const import DataType
from deebot_client.events import (
AdvancedModeEvent,
AvailabilityEvent,
BatteryEvent,
BorderSwitchEvent,
ChildLockEvent,
CrossMapBorderWarningEvent,
CustomCommandEvent,
CutDirectionEvent,
ErrorEvent,
LifeSpan,
LifeSpanEvent,
MoveUpWarningEvent,
NetworkInfoEvent,
ReportStatsEvent,
SafeProtectEvent,
StateEvent,
StatsEvent,
TotalStatsEvent,
TrueDetectEvent,
VolumeEvent,
)
from deebot_client.models import StaticDeviceInfo

from .cr0e4u import get_device_info as _cr0e4u_get_device_info


def get_device_info() -> StaticDeviceInfo:
"""Get device info for this model."""
return StaticDeviceInfo(
DataType.JSON,
Capabilities(
device_type=DeviceType.MOWER,
availability=CapabilityEvent(
AvailabilityEvent, [GetBattery(is_available_check=True)]
),
battery=CapabilityEvent(BatteryEvent, [GetBattery()]),
charge=CapabilityExecute(Charge),
clean=CapabilityClean(
action=CapabilityCleanAction(command=CleanV2),
),
custom=CapabilityCustomCommand(
event=CustomCommandEvent, get=[], set=CustomCommand
),
error=CapabilityEvent(ErrorEvent, [GetError()]),
life_span=CapabilityLifeSpan(
types=(LifeSpan.BLADE, LifeSpan.LENS_BRUSH),
event=LifeSpanEvent,
get=[
GetLifeSpan(
[
LifeSpan.BLADE,
LifeSpan.LENS_BRUSH,
]
)
],
reset=ResetLifeSpan,
),
network=CapabilityEvent(NetworkInfoEvent, [GetNetInfo()]),
play_sound=CapabilityExecute(PlaySound),
settings=CapabilitySettings(
advanced_mode=CapabilitySetEnable(
AdvancedModeEvent, [GetAdvancedMode()], SetAdvancedMode
),
border_switch=CapabilitySetEnable(
BorderSwitchEvent, [GetBorderSwitch()], SetBorderSwitch
),
cut_direction=CapabilitySet(
CutDirectionEvent, [GetCutDirection()], SetCutDirection
),
child_lock=CapabilitySetEnable(
ChildLockEvent, [GetChildLock()], SetChildLock
),
moveup_warning=CapabilitySetEnable(
MoveUpWarningEvent, [GetMoveUpWarning()], SetMoveUpWarning
),
cross_map_border_warning=CapabilitySetEnable(
CrossMapBorderWarningEvent,
[GetCrossMapBorderWarning()],
SetCrossMapBorderWarning,
),
safe_protect=CapabilitySetEnable(
SafeProtectEvent, [GetSafeProtect()], SetSafeProtect
),
true_detect=CapabilitySetEnable(
TrueDetectEvent, [GetTrueDetect()], SetTrueDetect
),
volume=CapabilitySet(VolumeEvent, [GetVolume()], SetVolume),
),
state=CapabilityEvent(StateEvent, [GetChargeState(), GetCleanInfoV2()]),
stats=CapabilityStats(
clean=CapabilityEvent(StatsEvent, [GetStats()]),
report=CapabilityEvent(ReportStatsEvent, []),
total=CapabilityEvent(TotalStatsEvent, [GetTotalStats()]),
),
),
)
"""Get device info for the GOAT A3000 LiDAR Pro (51rcxt)."""
return _cr0e4u_get_device_info()
1 change: 0 additions & 1 deletion deebot_client/hardware/cr0e4u.py

This file was deleted.

Loading