Skip to content

Commit 66bc325

Browse files
nanomadedenhaus
andauthored
Fix XML Bots and add FirmwareEvent (#935)
Co-authored-by: Robert Resch <robert@resch.dev>
1 parent 4cc81d1 commit 66bc325

46 files changed

Lines changed: 622 additions & 274 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deebot_client/commands/json/network.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from deebot_client.message import (
1010
HandlingResult,
1111
MessageBodyDataDict,
12-
MessageDict,
12+
MessageDictOrJson,
1313
)
1414

1515
from .common import JsonCommand, JsonCommandWithMessageHandling
@@ -42,7 +42,7 @@ def _handle_body_data_dict(
4242
return HandlingResult.success()
4343

4444

45-
class GetNetInfoLegacy(JsonCommand, CommandWithMessageHandling, MessageDict):
45+
class GetNetInfoLegacy(JsonCommand, CommandWithMessageHandling, MessageDictOrJson):
4646
"""Get network info command."""
4747

4848
NAME = "GetNetInfo"

deebot_client/device.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from collections.abc import Callable, Coroutine
77
from contextlib import suppress
88
from datetime import datetime
9-
import json
109
from typing import TYPE_CHECKING, Any, Final
1110

1211
from deebot_client.events.network import NetworkInfoEvent
@@ -19,6 +18,7 @@
1918
AvailabilityEvent,
2019
CleanLogEvent,
2120
CustomCommandEvent,
21+
FirmwareEvent,
2222
LifeSpanEvent,
2323
PositionsEvent,
2424
StateEvent,
@@ -34,6 +34,7 @@
3434
if TYPE_CHECKING:
3535
from .authentication import Authenticator
3636
from .command import DeviceCommandResult
37+
from .message import MessagePayloadType
3738

3839
_LOGGER = get_logger(__name__)
3940
_AVAILABLE_CHECK_INTERVAL = 60
@@ -113,6 +114,11 @@ async def on_network(event: NetworkInfoEvent) -> None:
113114

114115
self.events.subscribe(NetworkInfoEvent, on_network)
115116

117+
async def on_firmware(event: FirmwareEvent) -> None:
118+
self.fw_version = event.version
119+
120+
self.events.subscribe(FirmwareEvent, on_firmware)
121+
116122
async def execute_command(self, command: Command) -> dict[str, Any]:
117123
"""Execute given command.
118124
@@ -191,7 +197,7 @@ def _set_available(self, *, available: bool) -> None:
191197
self.events.notify(AvailabilityEvent(available=available))
192198

193199
def _handle_message(
194-
self, message_name: str, message_data: str | bytes | bytearray | dict[str, Any]
200+
self, message_name: str, message_data: MessagePayloadType
195201
) -> None:
196202
"""Handle the given message.
197203
@@ -205,15 +211,6 @@ def _handle_message(
205211
_LOGGER.debug("Try to handle message %s: %s", message_name, message_data)
206212

207213
if message := get_message(message_name, self._device_info.static.data_type):
208-
if isinstance(message_data, dict):
209-
data = message_data
210-
else:
211-
data = json.loads(message_data)
212-
213-
fw_version = data.get("header", {}).get("fwVer", None)
214-
if fw_version:
215-
self.fw_version = fw_version
216-
217-
message.handle(self.events, data)
214+
message.handle(self.events, message_data)
218215
except Exception: # pylint: disable=broad-except
219216
_LOGGER.exception("An exception occurred during handling message")

deebot_client/events/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"Event",
4444
"FanSpeedEvent",
4545
"FanSpeedLevel",
46+
"FirmwareEvent",
4647
"MajorMapEvent",
4748
"MapChangedEvent",
4849
"MapSetEvent",
@@ -300,3 +301,10 @@ class CutDirectionEvent(Event):
300301
"""Cut direction event representation."""
301302

302303
angle: int
304+
305+
306+
@dataclass(frozen=True)
307+
class FirmwareEvent(Event):
308+
"""Firmware event."""
309+
310+
version: str

deebot_client/message.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
from dataclasses import dataclass
77
from enum import IntEnum, auto
88
import functools
9+
import json
910
from typing import TYPE_CHECKING, Any, TypeVar, final
1011

12+
from deebot_client.events import FirmwareEvent
1113
from deebot_client.util import verify_required_class_variables_exists
1214

1315
from .logging_filter import get_logger
@@ -19,6 +21,8 @@
1921

2022
_LOGGER = get_logger(__name__)
2123

24+
MessagePayloadType = str | bytes | bytearray | dict[str, Any]
25+
2226

2327
class HandlingState(IntEnum):
2428
"""Handling state enum."""
@@ -63,6 +67,15 @@ def wrapper(
6367
) -> HandlingResult:
6468
try:
6569
response = func(cls, event_bus, data)
70+
# This happens if for some reason someone calls super() of an ABC where handle is not implemented
71+
if not response:
72+
_LOGGER.error(
73+
"Handler for message %s: %s returned no response. "
74+
"This is a bug should not happen. Please report it.",
75+
cls.NAME,
76+
data,
77+
)
78+
return HandlingResult(HandlingState.ERROR)
6679
if response.state == HandlingState.ANALYSE:
6780
_LOGGER.debug("Could not handle %s message: %s", cls.NAME, data)
6881
return HandlingResult(HandlingState.ANALYSE_LOGGED, response.args)
@@ -88,7 +101,7 @@ def __init_subclass__(cls) -> None:
88101
@classmethod
89102
@abstractmethod
90103
def _handle(
91-
cls, event_bus: EventBus, message: dict[str, Any] | str
104+
cls, event_bus: EventBus, message: MessagePayloadType
92105
) -> HandlingResult:
93106
"""Handle message and notify the correct event subscribers.
94107
@@ -98,9 +111,7 @@ def _handle(
98111
@classmethod
99112
@_handle_error_or_analyse
100113
@final
101-
def handle(
102-
cls, event_bus: EventBus, message: dict[str, Any] | str
103-
) -> HandlingResult:
114+
def handle(cls, event_bus: EventBus, message: MessagePayloadType) -> HandlingResult:
104115
"""Handle message and notify the correct event subscribers.
105116
106117
:return: A message response
@@ -120,28 +131,33 @@ def _handle_str(cls, event_bus: EventBus, message: str) -> HandlingResult:
120131
"""
121132

122133
@classmethod
123-
# @_handle_error_or_analyse @edenhaus will make the decorator to work again
134+
@_handle_error_or_analyse
124135
@final
125136
def __handle_str(cls, event_bus: EventBus, message: str) -> HandlingResult:
126137
return cls._handle_str(event_bus, message)
127138

128139
@classmethod
129140
def _handle(
130-
cls, event_bus: EventBus, message: dict[str, Any] | str
141+
cls, event_bus: EventBus, message: MessagePayloadType
131142
) -> HandlingResult:
132143
"""Handle message and notify the correct event subscribers.
133144
134145
:return: A message response
135146
"""
136-
# This basically means an XML message
137-
if isinstance(message, str):
138-
return cls.__handle_str(event_bus, message)
147+
if isinstance(message, bytearray):
148+
data = bytes(message).decode()
149+
elif isinstance(message, bytes):
150+
data = message.decode()
151+
elif isinstance(message, str):
152+
data = message
153+
else:
154+
return super()._handle(event_bus, message)
139155

140-
return super()._handle(event_bus, message)
156+
return cls.__handle_str(event_bus, data)
141157

142158

143-
class MessageDict(Message, ABC):
144-
"""Dict message."""
159+
class MessageDictOrJson(Message, ABC):
160+
"""Dict or json message."""
145161

146162
@classmethod
147163
@abstractmethod
@@ -163,19 +179,34 @@ def __handle_dict(
163179

164180
@classmethod
165181
def _handle(
166-
cls, event_bus: EventBus, message: dict[str, Any] | str
182+
cls, event_bus: EventBus, message: MessagePayloadType
167183
) -> HandlingResult:
168184
"""Handle message and notify the correct event subscribers.
169185
170186
:return: A message response
171187
"""
172-
if isinstance(message, dict):
173-
return cls.__handle_dict(event_bus, message)
188+
data = message
189+
if not isinstance(message, dict):
190+
try:
191+
data = json.loads(message)
192+
except Exception: # pylint: disable=broad-except
193+
_LOGGER.debug(
194+
"Could not decode message %s payload %s as JSON",
195+
cls.NAME,
196+
message,
197+
)
198+
199+
if isinstance(data, dict):
200+
fw_version = data.get("header", {}).get("fwVer", None)
201+
if fw_version:
202+
event_bus.notify(FirmwareEvent(fw_version))
203+
204+
return cls.__handle_dict(event_bus, data)
174205

175206
return super()._handle(event_bus, message)
176207

177208

178-
class MessageBody(MessageDict, ABC):
209+
class MessageBody(MessageDictOrJson, ABC):
179210
"""Dict message with body attribute."""
180211

181212
@classmethod

tests/commands/json/__init__.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,18 @@ async def assert_execute_command(
3636
assert command._args == args
3737

3838
# success
39-
json = get_request_json(get_success_body())
40-
await assert_command(command, json, None)
39+
json, firmware_event = get_request_json(get_success_body())
40+
await assert_command(command, json, firmware_event)
4141

4242
# failed
4343
with LogCapture() as log:
4444
body = {"code": 500, "msg": "fail"}
45-
json = get_request_json(body)
45+
json, firmware_event = get_request_json(body)
4646
await assert_command(
47-
command, json, None, command_result=CommandResult(HandlingState.FAILED)
47+
command,
48+
json,
49+
firmware_event,
50+
command_result=CommandResult(HandlingState.FAILED),
4851
)
4952

5053
log.check_present(
@@ -66,24 +69,26 @@ async def assert_set_command(
6669
event_bus = Mock(spec_set=EventBus)
6770

6871
# Failed to set
69-
json_data = get_message_json(
72+
json_data, firmware_event = get_message_json(
7073
{
7174
"code": 500,
7275
"msg": "fail",
7376
}
7477
)
7578
command.handle_mqtt_p2p(event_bus, json.dumps(json_data))
76-
event_bus.notify.assert_not_called()
79+
event_bus.notify.assert_called_once_with(firmware_event)
7780

81+
event_bus.reset_mock()
7882
# Success
79-
command.handle_mqtt_p2p(event_bus, json.dumps(get_message_json(get_success_body())))
80-
if isinstance(expected_get_command_events, Sequence):
81-
event_bus.notify.assert_has_calls(
82-
[call(x) for x in expected_get_command_events]
83-
)
84-
assert event_bus.notify.call_count == len(expected_get_command_events)
83+
data, firmware_event = get_message_json(get_success_body())
84+
command.handle_mqtt_p2p(event_bus, json.dumps(data))
85+
if not isinstance(expected_get_command_events, Sequence):
86+
expected_events = [firmware_event, expected_get_command_events]
8587
else:
86-
event_bus.notify.assert_called_once_with(expected_get_command_events)
88+
expected_events = [firmware_event, *expected_get_command_events]
89+
90+
event_bus.notify.assert_has_calls([call(x) for x in expected_events])
91+
assert event_bus.notify.call_count == len(expected_events)
8792

8893
payload = json.dumps({"body": {"data": args}})
8994
mqtt_command = command.create_from_mqtt(payload)

tests/commands/json/test_advanced_mode.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
@pytest.mark.parametrize("value", [False, True])
1313
async def test_GetAdvancedMode(*, value: bool) -> None:
14-
json = get_request_json(get_success_body({"enable": 1 if value else 0}))
15-
await assert_command(GetAdvancedMode(), json, AdvancedModeEvent(value))
14+
json, firmware_event = get_request_json(
15+
get_success_body({"enable": 1 if value else 0})
16+
)
17+
await assert_command(
18+
GetAdvancedMode(), json, (firmware_event, AdvancedModeEvent(value))
19+
)
1620

1721

1822
@pytest.mark.parametrize("value", [False, True])

tests/commands/json/test_auto_empty.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
)
4848
async def test_GetAutoEmpty(json: dict[str, Any], expected: AutoEmptyEvent) -> None:
4949
"""Test GetAutoEmpty."""
50-
json = get_request_json(get_success_body(json))
51-
await assert_command(GetAutoEmpty(), json, expected)
50+
json, firmware_event = get_request_json(get_success_body(json))
51+
await assert_command(GetAutoEmpty(), json, (firmware_event, expected))
5252

5353

5454
@pytest.mark.parametrize(

tests/commands/json/test_battery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
@pytest.mark.parametrize("percentage", [0, 49, 100])
1313
async def test_GetBattery(percentage: int) -> None:
14-
json = get_request_json(
14+
json, firmware_event = get_request_json(
1515
get_success_body({"value": percentage, "isLow": 1 if percentage < 20 else 0})
1616
)
17-
await assert_command(GetBattery(), json, BatteryEvent(percentage))
17+
await assert_command(GetBattery(), json, (firmware_event, BatteryEvent(percentage)))

tests/commands/json/test_border_switch.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
@pytest.mark.parametrize("value", [False, True])
1515
async def test_GetBorderSwitch(*, value: bool) -> None:
1616
"""Testing get border switch."""
17-
json = get_request_json(get_success_body({"enable": 1 if value else 0}))
18-
await assert_command(GetBorderSwitch(), json, BorderSwitchEvent(value))
17+
json, firmware_event = get_request_json(
18+
get_success_body({"enable": 1 if value else 0})
19+
)
20+
await assert_command(
21+
GetBorderSwitch(), json, (firmware_event, BorderSwitchEvent(value))
22+
)
1923

2024

2125
@pytest.mark.parametrize("value", [False, True])

tests/commands/json/test_carpet.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
@pytest.mark.parametrize("value", [False, True])
1313
async def test_GetCarpetAutoFanBoost(*, value: bool) -> None:
14-
json = get_request_json(get_success_body({"enable": 1 if value else 0}))
15-
await assert_command(GetCarpetAutoFanBoost(), json, CarpetAutoFanBoostEvent(value))
14+
json, firmware_event = get_request_json(
15+
get_success_body({"enable": 1 if value else 0})
16+
)
17+
await assert_command(
18+
GetCarpetAutoFanBoost(), json, (firmware_event, CarpetAutoFanBoostEvent(value))
19+
)
1620

1721

1822
@pytest.mark.parametrize("value", [False, True])

0 commit comments

Comments
 (0)