Skip to content

Commit 323a38b

Browse files
committed
Implement ResetLifeSpan
1 parent cab2f1b commit 323a38b

6 files changed

Lines changed: 168 additions & 16 deletions

File tree

deebot_client/commands/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,22 @@
1111
COMMANDS as JSON_COMMANDS,
1212
COMMANDS_WITH_MQTT_P2P_HANDLING as JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
1313
)
14+
from .xml import (
15+
COMMANDS as XML_COMMANDS,
16+
COMMANDS_WITH_MQTT_P2P_HANDLING as XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
17+
)
1418

1519
if TYPE_CHECKING:
1620
from deebot_client.command import Command, CommandMqttP2P
1721

18-
COMMANDS: dict[DataType, dict[str, type[Command]]] = {DataType.JSON: JSON_COMMANDS}
22+
COMMANDS: dict[DataType, dict[str, type[Command]]] = {
23+
DataType.JSON: JSON_COMMANDS,
24+
DataType.XML: XML_COMMANDS,
25+
}
1926

2027
COMMANDS_WITH_MQTT_P2P_HANDLING: dict[DataType, dict[str, type[CommandMqttP2P]]] = {
21-
DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING
28+
DataType.JSON: JSON_COMMANDS_WITH_MQTT_P2P_HANDLING,
29+
DataType.XML: XML_COMMANDS_WITH_MQTT_P2P_HANDLING,
2230
}
2331

2432

deebot_client/commands/xml/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .charge_state import GetChargeState
1111
from .error import GetError
1212
from .fan_speed import GetFanSpeed
13-
from .life_span import GetLifeSpan
13+
from .life_span import GetLifeSpan, ResetLifeSpan
1414
from .play_sound import PlaySound
1515
from .pos import GetPos
1616
from .stats import GetCleanSum
@@ -27,14 +27,21 @@
2727
"GetLifeSpan",
2828
"GetPos",
2929
"PlaySound",
30+
"ResetLifeSpan",
3031
]
3132

3233
# fmt: off
3334
# ordered by file asc
3435
_COMMANDS: list[type[XmlCommand]] = [
36+
Charge,
37+
GetChargeState,
38+
GetCleanSum,
3539
GetError,
40+
GetFanSpeed,
3641
GetLifeSpan,
42+
GetPos,
3743
PlaySound,
44+
ResetLifeSpan,
3845
]
3946
# fmt: on
4047

deebot_client/commands/xml/common.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
from __future__ import annotations
44

55
from abc import ABC, abstractmethod
6-
from typing import TYPE_CHECKING, cast
6+
from typing import TYPE_CHECKING, cast, override
77
from xml.etree.ElementTree import Element, SubElement
88

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

11-
from deebot_client.command import Command, CommandWithMessageHandling, SetCommand
11+
from deebot_client.command import (
12+
Command,
13+
CommandMqttP2P,
14+
CommandWithMessageHandling,
15+
SetCommand,
16+
)
1217
from deebot_client.const import DataType
1318
from deebot_client.logging_filter import get_logger
1419
from deebot_client.message import HandlingResult, HandlingState, MessageStr
1520

1621
if TYPE_CHECKING:
22+
from typing import Any
23+
1724
from deebot_client.event_bus import EventBus
1825

1926
_LOGGER = get_logger(__name__)
@@ -76,11 +83,46 @@ def _handle_xml(cls, _: EventBus, xml: Element) -> HandlingResult:
7683
return HandlingResult.success()
7784

7885
_LOGGER.warning(
79-
'Command "%s" was not successful. XML response: %s', cls.NAME, xml
86+
'Command "%s" was not successful. XML response: %s',
87+
cls.NAME,
88+
ElementTree.tostring(xml, "unicode"),
8089
)
8190
return HandlingResult(HandlingState.FAILED)
8291

8392

93+
class XmlCommandMqttP2P(XmlCommand, CommandMqttP2P, ABC):
94+
"""Json base command for mqtt p2p channel."""
95+
96+
@classmethod
97+
def create_from_mqtt(cls, payload: str | bytes | bytearray) -> CommandMqttP2P:
98+
"""Create a command from the mqtt data."""
99+
xml = ElementTree.fromstring(payload)
100+
return cls._create_from_mqtt(xml.attrib)
101+
102+
@override
103+
def handle_mqtt_p2p(
104+
self, event_bus: EventBus, response_payload: str | bytes | bytearray
105+
) -> None:
106+
"""Handle response received over the mqtt channel "p2p"."""
107+
if isinstance(response_payload, bytearray):
108+
data = bytes(response_payload).decode()
109+
elif isinstance(response_payload, bytes):
110+
data = response_payload.decode()
111+
elif isinstance(response_payload, str):
112+
data = response_payload
113+
else:
114+
msg = "Unsupported message data type {message_type}" # type: ignore[unreachable]
115+
raise TypeError(msg.format(essage_type=type(response_payload)))
116+
117+
self._handle_mqtt_p2p(event_bus, data)
118+
119+
@abstractmethod
120+
def _handle_mqtt_p2p(
121+
self, event_bus: EventBus, response: dict[str, Any] | str
122+
) -> None:
123+
"""Handle response received over the mqtt channel "p2p"."""
124+
125+
84126
class XmlSetCommand(ExecuteCommand, SetCommand, ABC):
85127
"""Xml base set command.
86128

deebot_client/commands/xml/life_span.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING
5+
from types import MappingProxyType
6+
from typing import TYPE_CHECKING, Any
67

8+
from deebot_client.command import InitParam
79
from deebot_client.events import LifeSpan, LifeSpanEvent
8-
from deebot_client.message import HandlingResult
10+
from deebot_client.message import HandlingResult, HandlingState
911

10-
from .common import XmlCommandWithMessageHandling
12+
from .common import ExecuteCommand, XmlCommandMqttP2P, XmlCommandWithMessageHandling
1113

1214
if TYPE_CHECKING:
1315
from xml.etree.ElementTree import Element
@@ -20,8 +22,11 @@ class GetLifeSpan(XmlCommandWithMessageHandling):
2022

2123
NAME = "GetLifeSpan"
2224

23-
def __init__(self, life_span: LifeSpan) -> None:
24-
super().__init__({"type": life_span.xml_value})
25+
def __init__(self, life_span: LifeSpan | str) -> None:
26+
xml_value = (
27+
life_span.xml_value if isinstance(life_span, LifeSpan) else life_span
28+
)
29+
super().__init__({"type": xml_value})
2530

2631
@classmethod
2732
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
@@ -47,3 +52,24 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
4752
LifeSpanEvent(LifeSpan.from_xml(component_type), percent, left)
4853
)
4954
return HandlingResult.success()
55+
56+
57+
class ResetLifeSpan(ExecuteCommand, XmlCommandMqttP2P):
58+
"""ResetLifeSpan command."""
59+
60+
NAME = "ResetLifeSpan"
61+
_mqtt_params = MappingProxyType({"type": InitParam(str, "life_span")})
62+
63+
def __init__(self, life_span: LifeSpan | str) -> None:
64+
xml_value = (
65+
life_span.xml_value if isinstance(life_span, LifeSpan) else life_span
66+
)
67+
super().__init__({"type": xml_value})
68+
69+
def _handle_mqtt_p2p(
70+
self, event_bus: EventBus, response: dict[str, Any] | str
71+
) -> None:
72+
"""Handle response received over the mqtt channel "p2p"."""
73+
result = self.handle(event_bus, response)
74+
if result.state == HandlingState.SUCCESS:
75+
event_bus.request_refresh(LifeSpanEvent)

tests/commands/xml/__init__.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,61 @@
11
from __future__ import annotations
22

3-
from typing import Any
3+
from typing import TYPE_CHECKING, Any, cast
4+
from xml.etree.ElementTree import Element, SubElement
5+
6+
from defusedxml import ElementTree # type: ignore[import-untyped]
7+
from testfixtures import LogCapture
8+
9+
from deebot_client.command import CommandResult
10+
from deebot_client.message import HandlingState
11+
from tests.commands import assert_command
12+
13+
if TYPE_CHECKING:
14+
from deebot_client.commands.xml.common import ExecuteCommand
15+
16+
17+
def get_success_body(
18+
extra_attrs: dict[str, Any] | None = None, sub_element_name: str | None = None
19+
) -> str:
20+
element = ctl_element = Element("ctl")
21+
element.set("ret", "ok")
22+
23+
if extra_attrs is not None and len(extra_attrs) > 0:
24+
if sub_element_name is not None:
25+
element = SubElement(element, sub_element_name.lower())
26+
27+
if isinstance(extra_attrs, dict):
28+
for key, value in extra_attrs.items():
29+
element.set(key, value)
30+
31+
return cast("str", ElementTree.tostring(ctl_element, "unicode"))
32+
33+
34+
async def assert_execute_command(
35+
command: ExecuteCommand, args: dict[str, Any] | list[Any] | None
36+
) -> None:
37+
assert command.NAME != "invalid"
38+
assert command._args == args
39+
40+
# success
41+
xml = get_request_xml(get_success_body())
42+
await assert_command(command, xml, None)
43+
44+
# failed
45+
with LogCapture() as log:
46+
body = '<ctl ret="error" />'
47+
json = get_request_xml(body)
48+
await assert_command(
49+
command, json, None, command_result=CommandResult(HandlingState.FAILED)
50+
)
51+
52+
log.check_present(
53+
(
54+
"deebot_client.commands.xml.common",
55+
"WARNING",
56+
f'Command "{command.NAME}" was not successful. XML response: {body}',
57+
)
58+
)
459

560

661
def get_request_xml(data: str | None) -> dict[str, Any]:

tests/commands/xml/test_life_span.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import pytest
44

55
from deebot_client.command import CommandResult
6-
from deebot_client.commands.xml import GetLifeSpan
6+
from deebot_client.commands.xml import GetLifeSpan, ResetLifeSpan
77
from deebot_client.events import LifeSpan, LifeSpanEvent
88
from deebot_client.message import HandlingState
99
from tests.commands import assert_command
1010

11-
from . import get_request_xml
11+
from . import assert_execute_command, get_request_xml
1212

1313

1414
@pytest.mark.parametrize(
@@ -38,10 +38,10 @@ async def test_get_life_span(
3838
total: int,
3939
expected_event: LifeSpanEvent,
4040
) -> None:
41-
json = get_request_xml(
41+
xml = get_request_xml(
4242
f"<ctl ret='ok' type='{component_type}' left='{left}' total='{total}'/>"
4343
)
44-
await assert_command(GetLifeSpan(lifespan_type), json, expected_event)
44+
await assert_command(GetLifeSpan(lifespan_type), xml, expected_event)
4545

4646

4747
@pytest.mark.parametrize(
@@ -57,3 +57,17 @@ async def test_get_life_span_error(xml: str) -> None:
5757
None,
5858
command_result=CommandResult(HandlingState.ANALYSE_LOGGED),
5959
)
60+
61+
62+
@pytest.mark.parametrize(
63+
("command", "args"),
64+
[
65+
(ResetLifeSpan(LifeSpan.FILTER), {"type": LifeSpan.FILTER.xml_value}),
66+
(
67+
ResetLifeSpan.create_from_mqtt(b'<ctl type="Brush" />'),
68+
{"type": LifeSpan.BRUSH.xml_value},
69+
),
70+
],
71+
)
72+
async def test_ResetLifeSpan(command: ResetLifeSpan, args: dict[str, str]) -> None:
73+
await assert_execute_command(command, args)

0 commit comments

Comments
 (0)