From 4262a012150d612651df1602758ffb2a615d1574 Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Tue, 22 Apr 2025 16:25:01 +0200 Subject: [PATCH 1/6] Add XML Position messages --- deebot_client/messages/xml/__init__.py | 6 +++-- deebot_client/messages/xml/pos.py | 34 ++++++++++++++++++++++++++ tests/messages/xml/test_pos.py | 30 +++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 deebot_client/messages/xml/pos.py create mode 100644 tests/messages/xml/test_pos.py diff --git a/deebot_client/messages/xml/__init__.py b/deebot_client/messages/xml/__init__.py index 21d5370a9..c0cb694d6 100644 --- a/deebot_client/messages/xml/__init__.py +++ b/deebot_client/messages/xml/__init__.py @@ -5,17 +5,19 @@ from typing import TYPE_CHECKING from deebot_client.messages.xml.battery import BatteryInfo +from deebot_client.messages.xml.pos import Pos if TYPE_CHECKING: from collections.abc import Sequence from deebot_client.message import Message -__all__: Sequence[str] = ["BatteryInfo"] +__all__: Sequence[str] = ["BatteryInfo", "Pos"] # fmt: off # ordered by file asc _MESSAGES: list[type[Message]] = [ - BatteryInfo + BatteryInfo, + Pos ] # fmt: on diff --git a/deebot_client/messages/xml/pos.py b/deebot_client/messages/xml/pos.py new file mode 100644 index 000000000..703495337 --- /dev/null +++ b/deebot_client/messages/xml/pos.py @@ -0,0 +1,34 @@ +"""Pos messages.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from deebot_client.events import Position, PositionsEvent +from deebot_client.message import HandlingResult +from deebot_client.messages.xml.common import XmlMessage +from deebot_client.rs.map import PositionType + +if TYPE_CHECKING: + from xml.etree.ElementTree import Element + + from deebot_client.event_bus import EventBus + + +class Pos(XmlMessage): + """Pos message.""" + + NAME = "Pos" + + @classmethod + def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: + if p := xml.attrib.get("p"): + p_x, p_y = p.split(",", 2) + p_a = xml.attrib.get("a", 0) + position = Position( + type=PositionType.DEEBOT, x=int(p_x), y=int(p_y), a=int(p_a) + ) + event_bus.notify(PositionsEvent(positions=[position])) + return HandlingResult.success() + + return HandlingResult.analyse() diff --git a/tests/messages/xml/test_pos.py b/tests/messages/xml/test_pos.py new file mode 100644 index 000000000..bad125946 --- /dev/null +++ b/tests/messages/xml/test_pos.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import pytest + +from deebot_client.events import Position, PositionsEvent +from deebot_client.message import HandlingState +from deebot_client.messages.xml import Pos +from deebot_client.rs.map import PositionType +from tests.messages import assert_message, assert_message_failure + + +@pytest.mark.parametrize("position", [(-9, 15, 89)]) +def test_Pos(position: tuple[int, int, int]) -> None: + x, y, a = position + xml_message = f'' + assert_message( + Pos, + xml_message, + PositionsEvent([Position(type=PositionType.DEEBOT, x=x, y=y, a=a)]), + ) + + +@pytest.mark.parametrize( + "xml_message", + [ + '', + ], +) +def test_Pos_error(xml_message: str) -> None: + assert_message_failure(Pos, xml_message, HandlingState.ANALYSE_LOGGED) From 073edeeb839ef73e69b100218ead70b26bc04e7a Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Thu, 24 Apr 2025 10:59:50 +0200 Subject: [PATCH 2/6] Introduce GetChargerPos command and Pos parser class --- deebot_client/commands/xml/__init__.py | 5 ++- deebot_client/commands/xml/pos.py | 46 ++++++++++++++++++++------ deebot_client/messages/xml/pos.py | 17 +++------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/deebot_client/commands/xml/__init__.py b/deebot_client/commands/xml/__init__.py index ef7e51fab..9561f6581 100644 --- a/deebot_client/commands/xml/__init__.py +++ b/deebot_client/commands/xml/__init__.py @@ -14,7 +14,7 @@ from .fan_speed import GetFanSpeed from .life_span import GetLifeSpan from .play_sound import PlaySound -from .pos import GetPos +from .pos import GetChargerPos, GetPos from .stats import GetCleanSum if TYPE_CHECKING: @@ -24,6 +24,7 @@ "Charge", "GetBatteryInfo", "GetChargeState", + "GetChargerPos", "GetCleanLogs", "GetCleanSum", "GetError", @@ -37,9 +38,11 @@ # ordered by file asc _COMMANDS: list[type[XmlCommand]] = [ GetBatteryInfo, + GetChargerPos, GetCleanLogs, GetError, GetLifeSpan, + GetPos, PlaySound, ] # fmt: on diff --git a/deebot_client/commands/xml/pos.py b/deebot_client/commands/xml/pos.py index c9fed0920..16c15c469 100644 --- a/deebot_client/commands/xml/pos.py +++ b/deebot_client/commands/xml/pos.py @@ -16,7 +16,25 @@ from deebot_client.event_bus import EventBus -class GetPos(XmlCommandWithMessageHandling): +class PosParser: + """Support class for producing Pos events.""" + + @classmethod + def __parse_xml( + cls, position_type: PositionType, event_bus: EventBus, xml: Element + ) -> HandlingResult: + """Handle xml message and notify the correct event subscribers.""" + if (p := xml.attrib.get("p")) and (xml.attrib.get("valid", "1")) != "1": + p_x, p_y = p.split(",", 2) + p_a = xml.attrib.get("a", 0) + position = Position(type=position_type, x=int(p_x), y=int(p_y), a=int(p_a)) + event_bus.notify(PositionsEvent(positions=[position])) + return HandlingResult.success() + + return HandlingResult.analyse() + + +class GetPos(XmlCommandWithMessageHandling, PosParser): """GetPos command.""" NAME = "GetPos" @@ -30,13 +48,21 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: if xml.attrib.get("ret") != "ok" or xml.attrib.get("t") != "p": return HandlingResult.analyse() - if p := xml.attrib.get("p"): - p_x, p_y = p.split(",", 2) - p_a = xml.attrib.get("a", 0) - position = Position( - type=PositionType.DEEBOT, x=int(p_x), y=int(p_y), a=int(p_a) - ) - event_bus.notify(PositionsEvent(positions=[position])) - return HandlingResult.success() + return cls.__parse_xml(PositionType.DEEBOT, event_bus, xml) - return HandlingResult.analyse() + +class GetChargerPos(XmlCommandWithMessageHandling, PosParser): + """GetChargerPos command.""" + + NAME = "GetChargerPos" + + @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": + return HandlingResult.analyse() + + return cls.__parse_xml(PositionType.CHARGER, event_bus, xml) diff --git a/deebot_client/messages/xml/pos.py b/deebot_client/messages/xml/pos.py index 703495337..4ea6f3fef 100644 --- a/deebot_client/messages/xml/pos.py +++ b/deebot_client/messages/xml/pos.py @@ -4,8 +4,7 @@ from typing import TYPE_CHECKING -from deebot_client.events import Position, PositionsEvent -from deebot_client.message import HandlingResult +from deebot_client.commands.xml.pos import PosParser from deebot_client.messages.xml.common import XmlMessage from deebot_client.rs.map import PositionType @@ -13,22 +12,14 @@ from xml.etree.ElementTree import Element from deebot_client.event_bus import EventBus + from deebot_client.message import HandlingResult -class Pos(XmlMessage): +class Pos(XmlMessage, PosParser): """Pos message.""" NAME = "Pos" @classmethod def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: - if p := xml.attrib.get("p"): - p_x, p_y = p.split(",", 2) - p_a = xml.attrib.get("a", 0) - position = Position( - type=PositionType.DEEBOT, x=int(p_x), y=int(p_y), a=int(p_a) - ) - event_bus.notify(PositionsEvent(positions=[position])) - return HandlingResult.success() - - return HandlingResult.analyse() + return cls.__parse_xml(PositionType.DEEBOT, event_bus, xml) From e21500ec58048f4cbd9885379a3e04854a861bc1 Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Thu, 24 Apr 2025 11:04:11 +0200 Subject: [PATCH 3/6] Add more tests --- tests/commands/xml/test_pos.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/commands/xml/test_pos.py b/tests/commands/xml/test_pos.py index 6157f1d93..b9238456b 100644 --- a/tests/commands/xml/test_pos.py +++ b/tests/commands/xml/test_pos.py @@ -4,6 +4,7 @@ from deebot_client.command import CommandResult from deebot_client.commands.xml import GetPos +from deebot_client.commands.xml.pos import GetChargerPos from deebot_client.events import Position, PositionsEvent from deebot_client.message import HandlingState from deebot_client.rs.map import PositionType @@ -22,8 +23,13 @@ async def test_get_pos() -> None: @pytest.mark.parametrize( "xml", - ["", ""], - ids=["error", "no_state"], + [ + "", + "", + "", + "", + ], + ids=["error", "no_state", "wrong_type", "not_valid"], ) async def test_get_pos_error(xml: str) -> None: json = get_request_xml(xml) @@ -33,3 +39,26 @@ async def test_get_pos_error(xml: str) -> None: None, command_result=CommandResult(HandlingState.ANALYSE_LOGGED), ) + + +async def test_get_charger_pos() -> None: + json = get_request_xml("") + expected_event = PositionsEvent( + positions=[Position(type=PositionType.CHARGER, x=77, y=-5, a=-3)] + ) + await assert_command(GetPos(), json, expected_event) + + +@pytest.mark.parametrize( + "xml", + ["", ""], + ids=["error", "no_state"], +) +async def test_get_charger_pos_error(xml: str) -> None: + json = get_request_xml(xml) + await assert_command( + GetChargerPos(), + json, + None, + command_result=CommandResult(HandlingState.ANALYSE_LOGGED), + ) From e363ad0b445bc6f588ee6404bb983d8a4f2118e6 Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Thu, 24 Apr 2025 11:20:35 +0200 Subject: [PATCH 4/6] Add more checks to Pos message parsing --- deebot_client/commands/xml/pos.py | 8 ++++---- deebot_client/messages/xml/pos.py | 7 +++++-- tests/messages/xml/test_pos.py | 13 +++++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/deebot_client/commands/xml/pos.py b/deebot_client/commands/xml/pos.py index 16c15c469..58e3be281 100644 --- a/deebot_client/commands/xml/pos.py +++ b/deebot_client/commands/xml/pos.py @@ -20,11 +20,11 @@ class PosParser: """Support class for producing Pos events.""" @classmethod - def __parse_xml( + def _parse_xml( cls, position_type: PositionType, event_bus: EventBus, xml: Element ) -> HandlingResult: """Handle xml message and notify the correct event subscribers.""" - if (p := xml.attrib.get("p")) and (xml.attrib.get("valid", "1")) != "1": + if (p := xml.attrib.get("p")) and (xml.attrib.get("valid", "1")) == "1": p_x, p_y = p.split(",", 2) p_a = xml.attrib.get("a", 0) position = Position(type=position_type, x=int(p_x), y=int(p_y), a=int(p_a)) @@ -48,7 +48,7 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: if xml.attrib.get("ret") != "ok" or xml.attrib.get("t") != "p": return HandlingResult.analyse() - return cls.__parse_xml(PositionType.DEEBOT, event_bus, xml) + return cls._parse_xml(PositionType.DEEBOT, event_bus, xml) class GetChargerPos(XmlCommandWithMessageHandling, PosParser): @@ -65,4 +65,4 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: if xml.attrib.get("ret") != "ok": return HandlingResult.analyse() - return cls.__parse_xml(PositionType.CHARGER, event_bus, xml) + return cls._parse_xml(PositionType.CHARGER, event_bus, xml) diff --git a/deebot_client/messages/xml/pos.py b/deebot_client/messages/xml/pos.py index 4ea6f3fef..df3dbce4f 100644 --- a/deebot_client/messages/xml/pos.py +++ b/deebot_client/messages/xml/pos.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING from deebot_client.commands.xml.pos import PosParser +from deebot_client.message import HandlingResult from deebot_client.messages.xml.common import XmlMessage from deebot_client.rs.map import PositionType @@ -12,7 +13,6 @@ from xml.etree.ElementTree import Element from deebot_client.event_bus import EventBus - from deebot_client.message import HandlingResult class Pos(XmlMessage, PosParser): @@ -22,4 +22,7 @@ class Pos(XmlMessage, PosParser): @classmethod def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: - return cls.__parse_xml(PositionType.DEEBOT, event_bus, xml) + if xml.attrib.get("t") != "p": + return HandlingResult.analyse() + + return cls._parse_xml(PositionType.DEEBOT, event_bus, xml) diff --git a/tests/messages/xml/test_pos.py b/tests/messages/xml/test_pos.py index bad125946..03b484966 100644 --- a/tests/messages/xml/test_pos.py +++ b/tests/messages/xml/test_pos.py @@ -21,10 +21,15 @@ def test_Pos(position: tuple[int, int, int]) -> None: @pytest.mark.parametrize( - "xml_message", + ("xml_message", "expected_result_state"), [ - '', + ('', HandlingState.ANALYSE_LOGGED), + ('', HandlingState.ERROR), + ( + '', + HandlingState.ANALYSE_LOGGED, + ), ], ) -def test_Pos_error(xml_message: str) -> None: - assert_message_failure(Pos, xml_message, HandlingState.ANALYSE_LOGGED) +def test_Pos_error(xml_message: str, expected_result_state: HandlingState) -> None: + assert_message_failure(Pos, xml_message, expected_result_state) From 20b064d2bc598b637d5a382a24800dd6f4ad42d5 Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Thu, 24 Apr 2025 12:38:26 +0200 Subject: [PATCH 5/6] Fix tests --- tests/commands/xml/test_pos.py | 2 +- tests/messages/xml/test_pos.py | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/commands/xml/test_pos.py b/tests/commands/xml/test_pos.py index b9238456b..86d5da587 100644 --- a/tests/commands/xml/test_pos.py +++ b/tests/commands/xml/test_pos.py @@ -46,7 +46,7 @@ async def test_get_charger_pos() -> None: expected_event = PositionsEvent( positions=[Position(type=PositionType.CHARGER, x=77, y=-5, a=-3)] ) - await assert_command(GetPos(), json, expected_event) + await assert_command(GetChargerPos(), json, expected_event) @pytest.mark.parametrize( diff --git a/tests/messages/xml/test_pos.py b/tests/messages/xml/test_pos.py index 03b484966..4505e7af7 100644 --- a/tests/messages/xml/test_pos.py +++ b/tests/messages/xml/test_pos.py @@ -21,15 +21,12 @@ def test_Pos(position: tuple[int, int, int]) -> None: @pytest.mark.parametrize( - ("xml_message", "expected_result_state"), - [ - ('', HandlingState.ANALYSE_LOGGED), - ('', HandlingState.ERROR), - ( - '', - HandlingState.ANALYSE_LOGGED, - ), - ], + "xml_message", + { + '', + '', + '', + }, ) -def test_Pos_error(xml_message: str, expected_result_state: HandlingState) -> None: - assert_message_failure(Pos, xml_message, expected_result_state) +def test_Pos_error(xml_message: str) -> None: + assert_message_failure(Pos, xml_message, HandlingState.ANALYSE_LOGGED) From a8efe159c3dfbcf0ac5569cc4f0582325ce77a05 Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Thu, 24 Apr 2025 12:58:47 +0200 Subject: [PATCH 6/6] Integrate PR feedback --- deebot_client/commands/xml/pos.py | 24 +++--------------------- deebot_client/messages/xml/pos.py | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/deebot_client/commands/xml/pos.py b/deebot_client/commands/xml/pos.py index 58e3be281..b7c8f16fe 100644 --- a/deebot_client/commands/xml/pos.py +++ b/deebot_client/commands/xml/pos.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING -from deebot_client.events import Position, PositionsEvent from deebot_client.message import HandlingResult +from deebot_client.messages.xml import Pos from deebot_client.rs.map import PositionType from .common import XmlCommandWithMessageHandling @@ -16,25 +16,7 @@ from deebot_client.event_bus import EventBus -class PosParser: - """Support class for producing Pos events.""" - - @classmethod - def _parse_xml( - cls, position_type: PositionType, event_bus: EventBus, xml: Element - ) -> HandlingResult: - """Handle xml message and notify the correct event subscribers.""" - if (p := xml.attrib.get("p")) and (xml.attrib.get("valid", "1")) == "1": - p_x, p_y = p.split(",", 2) - p_a = xml.attrib.get("a", 0) - position = Position(type=position_type, x=int(p_x), y=int(p_y), a=int(p_a)) - event_bus.notify(PositionsEvent(positions=[position])) - return HandlingResult.success() - - return HandlingResult.analyse() - - -class GetPos(XmlCommandWithMessageHandling, PosParser): +class GetPos(XmlCommandWithMessageHandling, Pos): """GetPos command.""" NAME = "GetPos" @@ -51,7 +33,7 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: return cls._parse_xml(PositionType.DEEBOT, event_bus, xml) -class GetChargerPos(XmlCommandWithMessageHandling, PosParser): +class GetChargerPos(XmlCommandWithMessageHandling, Pos): """GetChargerPos command.""" NAME = "GetChargerPos" diff --git a/deebot_client/messages/xml/pos.py b/deebot_client/messages/xml/pos.py index df3dbce4f..df6e6c21a 100644 --- a/deebot_client/messages/xml/pos.py +++ b/deebot_client/messages/xml/pos.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING -from deebot_client.commands.xml.pos import PosParser +from deebot_client.events import Position, PositionsEvent from deebot_client.message import HandlingResult from deebot_client.messages.xml.common import XmlMessage from deebot_client.rs.map import PositionType @@ -15,7 +15,7 @@ from deebot_client.event_bus import EventBus -class Pos(XmlMessage, PosParser): +class Pos(XmlMessage): """Pos message.""" NAME = "Pos" @@ -26,3 +26,17 @@ def _handle_xml(cls, event_bus: EventBus, xml: Element) -> HandlingResult: return HandlingResult.analyse() return cls._parse_xml(PositionType.DEEBOT, event_bus, xml) + + @classmethod + def _parse_xml( + cls, position_type: PositionType, event_bus: EventBus, xml: Element + ) -> HandlingResult: + """Handle xml message and notify the correct event subscribers.""" + if (p := xml.attrib.get("p")) and (xml.attrib.get("valid", "1")) == "1": + p_x, p_y = p.split(",", 2) + p_a = xml.attrib.get("a", 0) + position = Position(type=position_type, x=int(p_x), y=int(p_y), a=int(p_a)) + event_bus.notify(PositionsEvent(positions=[position])) + return HandlingResult.success() + + return HandlingResult.analyse()