Skip to content

Feat/ls1ok3 v4 #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ repos:
hooks:
- id: codespell
args:
- --ignore-words-list=deebot
- --ignore-words-list=deebot,MapP
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
- --exclude-file=deebot_client/util/continents.py
Expand Down
11 changes: 9 additions & 2 deletions deebot_client/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,15 @@ def _pop_or_raise(name: str, type_: type, data: dict[str, Any]) -> Any:
try:
return type_(value)
except ValueError as err:
msg = f'Could not convert "{value}" of {name} into {type_}'
raise DeebotError(msg) from err
if hasattr(type_, "from_xml"):
try:
return type_.from_xml(value)
except ValueError as err2:
msg = f'Could not convert "{value}" of {name} into {type_}'
raise DeebotError(msg) from err2
else:
msg = f'Could not convert "{value}" of {name} into {type_}'
raise DeebotError(msg) from err


class GetCommand(CommandWithMessageHandling, ABC):
Expand Down
6 changes: 5 additions & 1 deletion deebot_client/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
COMMANDS as JSON_COMMANDS,
COMMANDS_WITH_MQTT_P2P_HANDLING as JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
)
from .xml import (
COMMANDS_WITH_MQTT_P2P_HANDLING as XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
)

if TYPE_CHECKING:
from deebot_client.command import Command, CommandMqttP2P

COMMANDS: dict[DataType, dict[str, type[Command]]] = {DataType.JSON: JSON_COMMANDS}

COMMANDS_WITH_MQTT_P2P_HANDLING: dict[DataType, dict[str, type[CommandMqttP2P]]] = {
DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING
DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
DataType.XML: XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
}


Expand Down
25 changes: 23 additions & 2 deletions deebot_client/commands/xml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,55 @@

from deebot_client.command import Command, CommandMqttP2P

from .battery import GetBatteryInfo
from .charge import Charge
from .charge_state import GetChargeState
from .clean import Clean, CleanArea, GetCleanState
from .clean_logs import GetCleanLogs
from .error import GetError
from .fan_speed import GetFanSpeed
from .fan_speed import GetCleanSpeed, SetCleanSpeed
from .life_span import GetLifeSpan
from .play_sound import PlaySound
from .pos import GetPos
from .stats import GetCleanSum
from .water_info import GetWaterBoxInfo, GetWaterPermeability

if TYPE_CHECKING:
from .common import XmlCommand

__all__ = [
"Charge",
"Clean",
"CleanArea",
"GetBatteryInfo",
"GetChargeState",
"GetCleanLogs",
"GetCleanSpeed",
"GetCleanState",
"GetCleanSum",
"GetError",
"GetFanSpeed",
"GetLifeSpan",
"GetPos",
"GetWaterBoxInfo",
"GetWaterPermeability",
"PlaySound",
"SetCleanSpeed",
]

# fmt: off
# ordered by file asc
_COMMANDS: list[type[XmlCommand]] = [
Clean,
CleanArea,
GetError,
GetBatteryInfo,
GetCleanLogs,
GetCleanSpeed,
GetCleanState,
GetLifeSpan,
GetWaterBoxInfo,
GetWaterPermeability,
SetCleanSpeed,
PlaySound,
]
# fmt: on
Expand Down
40 changes: 40 additions & 0 deletions deebot_client/commands/xml/battery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Battery Info command."""

from __future__ import annotations

from typing import TYPE_CHECKING

from deebot_client.events import BatteryEvent
from deebot_client.message import HandlingResult

from .common import XmlCommandWithMessageHandling

if TYPE_CHECKING:
from xml.etree.ElementTree import Element

from deebot_client.event_bus import EventBus


class GetBatteryInfo(XmlCommandWithMessageHandling):
"""GetBatteryInfo command."""

NAME = "GetBatteryInfo"

@classmethod
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
"""Handle xml message and notify the correct event subscribers.

:return: A message response
"""
if (
xml.attrib.get("ret") != "ok"
or (battery := xml.find("battery")) is None
or (power := battery.attrib.get("power")) is None
):
return HandlingResult.analyse()

if power:
event_bus.notify(BatteryEvent(int(power)))
return HandlingResult.success()

return HandlingResult.analyse()
2 changes: 1 addition & 1 deletion deebot_client/commands/xml/charge_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
case "slotcharging" | "slot_charging" | "wirecharging":
status = State.DOCKED
case "idle":
status = State.IDLE
pass
case "going":
status = State.RETURNING
case _:
Expand Down
97 changes: 97 additions & 0 deletions deebot_client/commands/xml/clean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Clean commands."""

from __future__ import annotations

from typing import TYPE_CHECKING

from deebot_client.events import FanSpeedEvent, FanSpeedLevel, StateEvent
from deebot_client.logging_filter import get_logger
from deebot_client.message import HandlingResult
from deebot_client.models import CleanAction, CleanMode, State

from .common import ExecuteCommand, XmlCommandWithMessageHandling

if TYPE_CHECKING:
from xml.etree.ElementTree import Element

from deebot_client.event_bus import EventBus

_LOGGER = get_logger(__name__)


class Clean(ExecuteCommand):
"""Generic start/pause/stop cleaning command."""

NAME = "Clean"
HAS_SUB_ELEMENT = True

def __init__(
self, action: CleanAction, speed: FanSpeedLevel = FanSpeedLevel.NORMAL
) -> None:
# <ctl><clean type='SpotArea' act='s' speed='standard' deep='1' mid='4,5'/></ctl>

super().__init__(
{
"type": CleanMode.AUTO.xml_value,
"act": action.xml_value,
"speed": speed.xml_value,
}
)


class CleanArea(ExecuteCommand):
"""Clean area command."""

NAME = "Clean"
HAS_SUB_ELEMENT = True

def __init__(
self,
mode: CleanMode,
area: str,
cleanings: int = 1,
speed: FanSpeedLevel = FanSpeedLevel.NORMAL,
) -> None:
# <ctl><clean type='SpotArea' act='s' speed='standard' deep='1' mid='4,5'/></ctl>

super().__init__(
{
"type": mode.xml_value,
"act": CleanAction.START.xml_value,
"speed": speed.xml_value,
"deep": str(cleanings),
"mid": area,
}
)


class GetCleanState(XmlCommandWithMessageHandling):
"""GetCleanState command."""

NAME = "GetCleanState"

@classmethod
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
"""Handle xml message and notify the correct event subscribers.

:return: A message response
"""
if xml.attrib.get("ret") != "ok" or (clean := xml.find("clean")) is None:
return HandlingResult.analyse()

speed_attrib = clean.attrib.get("speed")
if speed_attrib is not None:
fan_speed_level = FanSpeedLevel.from_xml(speed_attrib)
event_bus.notify(FanSpeedEvent(fan_speed_level))

clean_attrib = clean.attrib.get("st")
if clean_attrib is not None:
clean_action = CleanAction.from_xml(clean_attrib)
if clean_action == CleanAction.START:
event_bus.notify(StateEvent(State.CLEANING))
elif clean_action == CleanAction.PAUSE:
event_bus.notify(StateEvent(State.PAUSED))
else:
_LOGGER.debug("Ignored CleanState %s", clean_action)

return HandlingResult.success()
64 changes: 64 additions & 0 deletions deebot_client/commands/xml/clean_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Clean Logs commands."""

from __future__ import annotations

from typing import TYPE_CHECKING

from deebot_client.command import CommandResult
from deebot_client.events import (
CleanJobStatus,
CleanLogEntry,
CleanLogEvent,
)
from deebot_client.logging_filter import get_logger
from deebot_client.message import HandlingResult

from .common import XmlCommandWithMessageHandling

if TYPE_CHECKING:
from xml.etree.ElementTree import Element

from deebot_client.event_bus import EventBus

_LOGGER = get_logger(__name__)


class GetCleanLogs(XmlCommandWithMessageHandling):
"""GetCleanLogs command."""

NAME = "GetCleanLogs"

def __init__(self, count: int = 0) -> None:
super().__init__({"count": str(count)})

@classmethod
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
"""Handle xml message and notify the correct event subscribers.

:return: A message response
"""
if (
xml.attrib.get("ret") != "ok"
or (resp_logs := xml.findall("CleanSt")) is None
):
return HandlingResult.analyse()

if len(resp_logs) >= 0:
logs: list[CleanLogEntry] = []
for log in resp_logs:
try:
logs.append(
CleanLogEntry(
timestamp=int(log.attrib["s"]),
image_url="", # Missing
type=log.attrib["t"],
area=int(log.attrib["a"]),
stop_reason=CleanJobStatus.FINISHED, # To be extracted
duration=int(log.attrib["l"]),
)
)
except Exception: # pylint: disable=broad-except
_LOGGER.warning("Skipping log entry: %s", log, exc_info=True)
event_bus.notify(CleanLogEvent(logs))
return CommandResult.success()
return HandlingResult.analyse()
54 changes: 51 additions & 3 deletions deebot_client/commands/xml/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, Any, cast
from xml.etree.ElementTree import Element, SubElement

from defusedxml import ElementTree # type: ignore[import-untyped]

from deebot_client.command import Command, CommandWithMessageHandling, SetCommand
from deebot_client.command import (
Command,
CommandMqttP2P,
CommandWithMessageHandling,
GetCommand,
SetCommand,
)
from deebot_client.const import DataType
from deebot_client.logging_filter import get_logger
from deebot_client.message import HandlingResult, HandlingState, MessageStr
Expand Down Expand Up @@ -81,8 +87,50 @@ def _handle_xml(cls, _: EventBus, xml: Element) -> HandlingResult:
return HandlingResult(HandlingState.FAILED)


class XmlSetCommand(ExecuteCommand, SetCommand, ABC):
class XmlCommandMqttP2P(XmlCommand, CommandMqttP2P, ABC):
"""Json base command for mqtt p2p channel."""

@classmethod
def create_from_mqtt(cls, payload: str | bytes | bytearray) -> CommandMqttP2P:
"""Create a command from the mqtt data."""
xml = ElementTree.fromstring(payload)
return cls._create_from_mqtt(xml.attrib)

def handle_mqtt_p2p(
self, event_bus: EventBus, response_payload: str | bytes | bytearray
) -> None:
"""Handle response received over the mqtt channel "p2p"."""
if isinstance(response_payload, bytearray):
data = bytes(response_payload).decode()
elif isinstance(response_payload, bytes):
data = response_payload.decode()
elif isinstance(response_payload, str):
data = response_payload
else:
msg = "Unsupported message data type {message_type}"
raise TypeError(msg.format(essage_type=type(response_payload)))
self._handle_mqtt_p2p(event_bus, data)

@abstractmethod
def _handle_mqtt_p2p(
self, event_bus: EventBus, response: dict[str, Any] | str
) -> None:
"""Handle response received over the mqtt channel "p2p"."""


class XmlSetCommand(ExecuteCommand, SetCommand, XmlCommandMqttP2P, ABC):
"""Xml base set command.

Command needs to be linked to the "get" command, for handling (updating) the sensors.
"""


class XmlGetCommand(XmlCommandWithMessageHandling, GetCommand, ABC):
"""Xml get command."""

@classmethod
@abstractmethod
def handle_set_args(
cls, event_bus: EventBus, args: dict[str, Any]
) -> HandlingResult:
"""Handle arguments of set command."""
Loading
Loading