Skip to content

Commit 2ce647f

Browse files
authored
Add XML Message BatteryInfo (#900)
1 parent 231973c commit 2ce647f

File tree

8 files changed

+186
-1
lines changed

8 files changed

+186
-1
lines changed

deebot_client/messages/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
from deebot_client.message import Message
1010

1111
from .json import MESSAGES as JSON_MESSAGES
12+
from .xml import MESSAGES as XML_MESSAGES
1213

1314
_LOGGER = get_logger(__name__)
1415

1516
MESSAGES = {
1617
DataType.JSON: JSON_MESSAGES,
18+
DataType.XML: XML_MESSAGES,
1719
}
1820

1921
_LEGACY_USE_GET_COMMAND = [
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""XML messages."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from deebot_client.messages.xml.battery import BatteryInfo
8+
9+
if TYPE_CHECKING:
10+
from collections.abc import Sequence
11+
12+
from deebot_client.message import Message
13+
14+
__all__: Sequence[str] = ["BatteryInfo"]
15+
# fmt: off
16+
# ordered by file asc
17+
_MESSAGES: list[type[Message]] = [
18+
BatteryInfo
19+
]
20+
# fmt: on
21+
22+
MESSAGES: dict[str, type[Message]] = {message.NAME: message for message in _MESSAGES}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Battery messages."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from deebot_client.events import BatteryEvent
8+
from deebot_client.logging_filter import get_logger
9+
from deebot_client.message import HandlingResult
10+
from deebot_client.messages.xml.common import XmlMessage
11+
12+
if TYPE_CHECKING:
13+
from xml.etree.ElementTree import Element
14+
15+
from deebot_client.event_bus import EventBus
16+
17+
_LOGGER = get_logger(__name__)
18+
19+
20+
class BatteryInfo(XmlMessage):
21+
"""BatteryInfo message."""
22+
23+
NAME = "BatteryInfo"
24+
25+
@classmethod
26+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
27+
"""Handle xml message and notify the correct event subscribers.
28+
29+
:return: A message response
30+
"""
31+
if (battery := xml.find("battery")) is None or (
32+
power := battery.attrib.get("power")
33+
) is None:
34+
return HandlingResult.analyse()
35+
36+
if power.isdecimal() and (power_int := int(power)) >= 0:
37+
event_bus.notify(BatteryEvent(power_int))
38+
return HandlingResult.success()
39+
40+
_LOGGER.error("Invalid battery power level received %s", power)
41+
return HandlingResult.analyse()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Common xml based messages."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
from typing import TYPE_CHECKING
7+
8+
from defusedxml import ElementTree # type: ignore[import-untyped]
9+
10+
from deebot_client.message import MessageStr
11+
12+
if TYPE_CHECKING:
13+
from xml.etree.ElementTree import Element
14+
15+
from deebot_client.event_bus import EventBus
16+
from deebot_client.message import HandlingResult
17+
18+
19+
class XmlMessage(MessageStr, ABC):
20+
"""Xml message."""
21+
22+
@classmethod
23+
def _handle_str(cls, event_bus: EventBus, message: str) -> HandlingResult:
24+
"""Handle string message and notify the correct event subscribers.
25+
26+
:return: A message response
27+
"""
28+
xml = ElementTree.fromstring(message)
29+
return cls._handle_xml(event_bus, xml)
30+
31+
@classmethod
32+
@abstractmethod
33+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
34+
"""Handle xml message and notify the correct event subscribers.
35+
36+
:return: A message response
37+
"""

tests/messages/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,24 @@
1111

1212

1313
def assert_message(
14-
message: type[Message], data: dict[str, Any], expected_event: Event
14+
message: type[Message], data: dict[str, Any] | str, expected_event: Event
1515
) -> None:
1616
event_bus = Mock(spec_set=EventBus)
1717

1818
result = message.handle(event_bus, data)
1919

2020
assert result.state == HandlingState.SUCCESS
2121
event_bus.notify.assert_called_once_with(expected_event)
22+
23+
24+
def assert_message_failure(
25+
message: type[Message],
26+
data: dict[str, Any] | str,
27+
expected_result_state: HandlingState,
28+
) -> None:
29+
event_bus = Mock(spec_set=EventBus)
30+
31+
result = message.handle(event_bus, data)
32+
33+
assert result.state == expected_result_state
34+
event_bus.notify.assert_not_called()

tests/messages/xml/__init__.py

Whitespace-only changes.

tests/messages/xml/test_battery.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from deebot_client.events import BatteryEvent
6+
from deebot_client.message import HandlingState
7+
from deebot_client.messages.xml import BatteryInfo
8+
from tests.messages import assert_message, assert_message_failure
9+
10+
11+
@pytest.mark.parametrize("percentage", [0, 49, 100])
12+
def test_BatteryInfo(percentage: int) -> None:
13+
xml_message = f'<ctl ret="ok"><battery power="{percentage}" /></ctl>'
14+
15+
assert_message(BatteryInfo, xml_message, BatteryEvent(percentage))
16+
17+
18+
@pytest.mark.parametrize(
19+
"xml_message",
20+
[
21+
'<ctl ret="ok"><battery /></ctl>',
22+
'<ctl ret="ok"><battery power="-1" /></ctl>',
23+
'<ctl ret="ok"><battery power="fake" /></ctl>',
24+
'<ctl ret="ok"><wrong power="100" /></ctl>',
25+
],
26+
)
27+
def test_BatteryInfo_error(xml_message: str) -> None:
28+
assert_message_failure(BatteryInfo, xml_message, HandlingState.ANALYSE_LOGGED)

tests/messages/xml/test_common.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
5+
6+
from deebot_client.events import Event
7+
from deebot_client.message import HandlingResult, HandlingState
8+
from deebot_client.messages.xml.common import XmlMessage
9+
from tests.messages import assert_message, assert_message_failure
10+
11+
if TYPE_CHECKING:
12+
from xml.etree.ElementTree import Element
13+
14+
from deebot_client.event_bus import EventBus
15+
16+
17+
@dataclass(frozen=True)
18+
class _TestEvent(Event):
19+
payload: str
20+
21+
22+
class _TestXmlMessage(XmlMessage):
23+
NAME = "Test"
24+
25+
@classmethod
26+
def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult:
27+
if payload := xml.attrib.get("payload"):
28+
event_bus.notify(_TestEvent(payload))
29+
return HandlingResult.success()
30+
return HandlingResult.analyse()
31+
32+
33+
def test_XmlMessageDecoding() -> None:
34+
assert_message(
35+
_TestXmlMessage, '<ctl ret="ok" payload="test" />', _TestEvent("test")
36+
)
37+
38+
39+
def test_XmlMessageFailure() -> None:
40+
assert_message_failure(
41+
_TestXmlMessage, '<ctl ret="ok" />', HandlingState.ANALYSE_LOGGED
42+
)

0 commit comments

Comments
 (0)