diff --git a/deebot_client/commands/json/network.py b/deebot_client/commands/json/network.py index 838aa82ad..bb85deebb 100644 --- a/deebot_client/commands/json/network.py +++ b/deebot_client/commands/json/network.py @@ -9,7 +9,7 @@ from deebot_client.message import ( HandlingResult, MessageBodyDataDict, - MessageDict, + MessageDictOrJson, ) from .common import JsonCommand, JsonCommandWithMessageHandling @@ -42,7 +42,7 @@ def _handle_body_data_dict( return HandlingResult.success() -class GetNetInfoLegacy(JsonCommand, CommandWithMessageHandling, MessageDict): +class GetNetInfoLegacy(JsonCommand, CommandWithMessageHandling, MessageDictOrJson): """Get network info command.""" NAME = "GetNetInfo" diff --git a/deebot_client/device.py b/deebot_client/device.py index e07d38dd5..2b417a5f3 100644 --- a/deebot_client/device.py +++ b/deebot_client/device.py @@ -6,7 +6,6 @@ from collections.abc import Callable, Coroutine from contextlib import suppress from datetime import datetime -import json from typing import TYPE_CHECKING, Any, Final from deebot_client.events.network import NetworkInfoEvent @@ -19,6 +18,7 @@ AvailabilityEvent, CleanLogEvent, CustomCommandEvent, + FirmwareEvent, LifeSpanEvent, PositionsEvent, StateEvent, @@ -34,6 +34,7 @@ if TYPE_CHECKING: from .authentication import Authenticator from .command import DeviceCommandResult + from .message import MessagePayloadType _LOGGER = get_logger(__name__) _AVAILABLE_CHECK_INTERVAL = 60 @@ -113,6 +114,11 @@ async def on_network(event: NetworkInfoEvent) -> None: self.events.subscribe(NetworkInfoEvent, on_network) + async def on_firmware(event: FirmwareEvent) -> None: + self.fw_version = event.version + + self.events.subscribe(FirmwareEvent, on_firmware) + async def execute_command(self, command: Command) -> dict[str, Any]: """Execute given command. @@ -191,7 +197,7 @@ def _set_available(self, *, available: bool) -> None: self.events.notify(AvailabilityEvent(available=available)) def _handle_message( - self, message_name: str, message_data: str | bytes | bytearray | dict[str, Any] + self, message_name: str, message_data: MessagePayloadType ) -> None: """Handle the given message. @@ -205,15 +211,6 @@ def _handle_message( _LOGGER.debug("Try to handle message %s: %s", message_name, message_data) if message := get_message(message_name, self._device_info.static.data_type): - if isinstance(message_data, dict): - data = message_data - else: - data = json.loads(message_data) - - fw_version = data.get("header", {}).get("fwVer", None) - if fw_version: - self.fw_version = fw_version - - message.handle(self.events, data) + message.handle(self.events, message_data) except Exception: # pylint: disable=broad-except _LOGGER.exception("An exception occurred during handling message") diff --git a/deebot_client/events/__init__.py b/deebot_client/events/__init__.py index c6fea59ea..a1b44e363 100644 --- a/deebot_client/events/__init__.py +++ b/deebot_client/events/__init__.py @@ -43,6 +43,7 @@ "Event", "FanSpeedEvent", "FanSpeedLevel", + "FirmwareEvent", "MajorMapEvent", "MapChangedEvent", "MapSetEvent", @@ -300,3 +301,10 @@ class CutDirectionEvent(Event): """Cut direction event representation.""" angle: int + + +@dataclass(frozen=True) +class FirmwareEvent(Event): + """Firmware event.""" + + version: str diff --git a/deebot_client/message.py b/deebot_client/message.py index c25d9e9e0..f843df21c 100644 --- a/deebot_client/message.py +++ b/deebot_client/message.py @@ -6,8 +6,10 @@ from dataclasses import dataclass from enum import IntEnum, auto import functools +import json from typing import TYPE_CHECKING, Any, TypeVar, final +from deebot_client.events import FirmwareEvent from deebot_client.util import verify_required_class_variables_exists from .logging_filter import get_logger @@ -19,6 +21,8 @@ _LOGGER = get_logger(__name__) +MessagePayloadType = str | bytes | bytearray | dict[str, Any] + class HandlingState(IntEnum): """Handling state enum.""" @@ -63,6 +67,15 @@ def wrapper( ) -> HandlingResult: try: response = func(cls, event_bus, data) + # This happens if for some reason someone calls super() of an ABC where handle is not implemented + if not response: + _LOGGER.error( + "Handler for message %s: %s returned no response. " + "This is a bug should not happen. Please report it.", + cls.NAME, + data, + ) + return HandlingResult(HandlingState.ERROR) if response.state == HandlingState.ANALYSE: _LOGGER.debug("Could not handle %s message: %s", cls.NAME, data) return HandlingResult(HandlingState.ANALYSE_LOGGED, response.args) @@ -88,7 +101,7 @@ def __init_subclass__(cls) -> None: @classmethod @abstractmethod def _handle( - cls, event_bus: EventBus, message: dict[str, Any] | str + cls, event_bus: EventBus, message: MessagePayloadType ) -> HandlingResult: """Handle message and notify the correct event subscribers. @@ -98,9 +111,7 @@ def _handle( @classmethod @_handle_error_or_analyse @final - def handle( - cls, event_bus: EventBus, message: dict[str, Any] | str - ) -> HandlingResult: + def handle(cls, event_bus: EventBus, message: MessagePayloadType) -> HandlingResult: """Handle message and notify the correct event subscribers. :return: A message response @@ -120,28 +131,33 @@ def _handle_str(cls, event_bus: EventBus, message: str) -> HandlingResult: """ @classmethod - # @_handle_error_or_analyse @edenhaus will make the decorator to work again + @_handle_error_or_analyse @final def __handle_str(cls, event_bus: EventBus, message: str) -> HandlingResult: return cls._handle_str(event_bus, message) @classmethod def _handle( - cls, event_bus: EventBus, message: dict[str, Any] | str + cls, event_bus: EventBus, message: MessagePayloadType ) -> HandlingResult: """Handle message and notify the correct event subscribers. :return: A message response """ - # This basically means an XML message - if isinstance(message, str): - return cls.__handle_str(event_bus, message) + if isinstance(message, bytearray): + data = bytes(message).decode() + elif isinstance(message, bytes): + data = message.decode() + elif isinstance(message, str): + data = message + else: + return super()._handle(event_bus, message) - return super()._handle(event_bus, message) + return cls.__handle_str(event_bus, data) -class MessageDict(Message, ABC): - """Dict message.""" +class MessageDictOrJson(Message, ABC): + """Dict or json message.""" @classmethod @abstractmethod @@ -163,19 +179,34 @@ def __handle_dict( @classmethod def _handle( - cls, event_bus: EventBus, message: dict[str, Any] | str + cls, event_bus: EventBus, message: MessagePayloadType ) -> HandlingResult: """Handle message and notify the correct event subscribers. :return: A message response """ - if isinstance(message, dict): - return cls.__handle_dict(event_bus, message) + data = message + if not isinstance(message, dict): + try: + data = json.loads(message) + except Exception: # pylint: disable=broad-except + _LOGGER.debug( + "Could not decode message %s payload %s as JSON", + cls.NAME, + message, + ) + + if isinstance(data, dict): + fw_version = data.get("header", {}).get("fwVer", None) + if fw_version: + event_bus.notify(FirmwareEvent(fw_version)) + + return cls.__handle_dict(event_bus, data) return super()._handle(event_bus, message) -class MessageBody(MessageDict, ABC): +class MessageBody(MessageDictOrJson, ABC): """Dict message with body attribute.""" @classmethod diff --git a/tests/commands/json/__init__.py b/tests/commands/json/__init__.py index ce22db632..034b467b7 100644 --- a/tests/commands/json/__init__.py +++ b/tests/commands/json/__init__.py @@ -36,15 +36,18 @@ async def assert_execute_command( assert command._args == args # success - json = get_request_json(get_success_body()) - await assert_command(command, json, None) + json, firmware_event = get_request_json(get_success_body()) + await assert_command(command, json, firmware_event) # failed with LogCapture() as log: body = {"code": 500, "msg": "fail"} - json = get_request_json(body) + json, firmware_event = get_request_json(body) await assert_command( - command, json, None, command_result=CommandResult(HandlingState.FAILED) + command, + json, + firmware_event, + command_result=CommandResult(HandlingState.FAILED), ) log.check_present( @@ -66,24 +69,26 @@ async def assert_set_command( event_bus = Mock(spec_set=EventBus) # Failed to set - json_data = get_message_json( + json_data, firmware_event = get_message_json( { "code": 500, "msg": "fail", } ) command.handle_mqtt_p2p(event_bus, json.dumps(json_data)) - event_bus.notify.assert_not_called() + event_bus.notify.assert_called_once_with(firmware_event) + event_bus.reset_mock() # Success - command.handle_mqtt_p2p(event_bus, json.dumps(get_message_json(get_success_body()))) - if isinstance(expected_get_command_events, Sequence): - event_bus.notify.assert_has_calls( - [call(x) for x in expected_get_command_events] - ) - assert event_bus.notify.call_count == len(expected_get_command_events) + data, firmware_event = get_message_json(get_success_body()) + command.handle_mqtt_p2p(event_bus, json.dumps(data)) + if not isinstance(expected_get_command_events, Sequence): + expected_events = [firmware_event, expected_get_command_events] else: - event_bus.notify.assert_called_once_with(expected_get_command_events) + expected_events = [firmware_event, *expected_get_command_events] + + event_bus.notify.assert_has_calls([call(x) for x in expected_events]) + assert event_bus.notify.call_count == len(expected_events) payload = json.dumps({"body": {"data": args}}) mqtt_command = command.create_from_mqtt(payload) diff --git a/tests/commands/json/test_advanced_mode.py b/tests/commands/json/test_advanced_mode.py index d5094518f..04f16051b 100644 --- a/tests/commands/json/test_advanced_mode.py +++ b/tests/commands/json/test_advanced_mode.py @@ -11,8 +11,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetAdvancedMode(*, value: bool) -> None: - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetAdvancedMode(), json, AdvancedModeEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetAdvancedMode(), json, (firmware_event, AdvancedModeEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_auto_empty.py b/tests/commands/json/test_auto_empty.py index cae4f0c71..bf75fe346 100644 --- a/tests/commands/json/test_auto_empty.py +++ b/tests/commands/json/test_auto_empty.py @@ -47,8 +47,8 @@ ) async def test_GetAutoEmpty(json: dict[str, Any], expected: AutoEmptyEvent) -> None: """Test GetAutoEmpty.""" - json = get_request_json(get_success_body(json)) - await assert_command(GetAutoEmpty(), json, expected) + json, firmware_event = get_request_json(get_success_body(json)) + await assert_command(GetAutoEmpty(), json, (firmware_event, expected)) @pytest.mark.parametrize( diff --git a/tests/commands/json/test_battery.py b/tests/commands/json/test_battery.py index 0d60684ef..a412986a2 100644 --- a/tests/commands/json/test_battery.py +++ b/tests/commands/json/test_battery.py @@ -11,7 +11,7 @@ @pytest.mark.parametrize("percentage", [0, 49, 100]) async def test_GetBattery(percentage: int) -> None: - json = get_request_json( + json, firmware_event = get_request_json( get_success_body({"value": percentage, "isLow": 1 if percentage < 20 else 0}) ) - await assert_command(GetBattery(), json, BatteryEvent(percentage)) + await assert_command(GetBattery(), json, (firmware_event, BatteryEvent(percentage))) diff --git a/tests/commands/json/test_border_switch.py b/tests/commands/json/test_border_switch.py index 7e41d0934..6667d8d08 100644 --- a/tests/commands/json/test_border_switch.py +++ b/tests/commands/json/test_border_switch.py @@ -14,8 +14,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetBorderSwitch(*, value: bool) -> None: """Testing get border switch.""" - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetBorderSwitch(), json, BorderSwitchEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetBorderSwitch(), json, (firmware_event, BorderSwitchEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_carpet.py b/tests/commands/json/test_carpet.py index c451d1578..6a63e30a0 100644 --- a/tests/commands/json/test_carpet.py +++ b/tests/commands/json/test_carpet.py @@ -11,8 +11,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetCarpetAutoFanBoost(*, value: bool) -> None: - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetCarpetAutoFanBoost(), json, CarpetAutoFanBoostEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetCarpetAutoFanBoost(), json, (firmware_event, CarpetAutoFanBoostEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_charge.py b/tests/commands/json/test_charge.py index b4d2df6a5..03e63d59b 100644 --- a/tests/commands/json/test_charge.py +++ b/tests/commands/json/test_charge.py @@ -7,7 +7,7 @@ from deebot_client.command import CommandResult from deebot_client.commands.json import Charge -from deebot_client.events import StateEvent +from deebot_client.events import FirmwareEvent, StateEvent from deebot_client.message import HandlingState from deebot_client.models import State from tests.helpers import get_request_json, get_success_body @@ -15,32 +15,38 @@ from . import assert_command -def _prepare_json(code: int, msg: str = "ok") -> dict[str, Any]: - json = get_request_json(get_success_body()) +def _prepare_json(code: int, msg: str = "ok") -> tuple[dict[str, Any], FirmwareEvent]: + json, firmware_event = get_request_json(get_success_body()) json["resp"]["body"].update( { "code": code, "msg": msg, } ) - return json + return json, firmware_event @pytest.mark.parametrize( - ("json", "expected"), + ("data", "expected"), [ (get_request_json(get_success_body()), StateEvent(State.RETURNING)), (_prepare_json(30007), StateEvent(State.DOCKED)), ], ) -async def test_Charge(json: dict[str, Any], expected: StateEvent) -> None: - await assert_command(Charge(), json, expected) +async def test_Charge( + data: tuple[dict[str, Any], FirmwareEvent], expected: StateEvent +) -> None: + json, firmware_event = data + await assert_command(Charge(), json, (firmware_event, expected)) async def test_Charge_failed(caplog: pytest.LogCaptureFixture) -> None: - json = _prepare_json(500, "fail") + json, firmware_event = _prepare_json(500, "fail") await assert_command( - Charge(), json, None, command_result=CommandResult(HandlingState.FAILED) + Charge(), + json, + firmware_event, + command_result=CommandResult(HandlingState.FAILED), ) assert ( diff --git a/tests/commands/json/test_charge_state.py b/tests/commands/json/test_charge_state.py index 3cc61fe54..412c092c9 100644 --- a/tests/commands/json/test_charge_state.py +++ b/tests/commands/json/test_charge_state.py @@ -10,16 +10,21 @@ from . import assert_command if TYPE_CHECKING: - from deebot_client.events import StateEvent + from deebot_client.events import FirmwareEvent, StateEvent + from deebot_client.events.base import Event @pytest.mark.parametrize( - ("json", "expected"), + ("data", "expected"), [ (get_request_json(get_success_body({"isCharging": 0, "mode": "slot"})), None), ], ) async def test_GetChargeState( - json: dict[str, Any], expected: StateEvent | None + data: tuple[dict[str, Any], FirmwareEvent], expected: StateEvent | None ) -> None: - await assert_command(GetChargeState(), json, expected) + json, firmware_event = data + events: list[Event] = [firmware_event] + if expected: + events.append(expected) + await assert_command(GetChargeState(), json, events) diff --git a/tests/commands/json/test_child_lock.py b/tests/commands/json/test_child_lock.py index 5eb28ab2d..80056dabe 100644 --- a/tests/commands/json/test_child_lock.py +++ b/tests/commands/json/test_child_lock.py @@ -14,8 +14,8 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetChildLock(*, value: bool) -> None: """Testing get child lock.""" - json = get_request_json(get_success_body({"on": 1 if value else 0})) - await assert_command(GetChildLock(), json, ChildLockEvent(value)) + json, firmware_event = get_request_json(get_success_body({"on": 1 if value else 0})) + await assert_command(GetChildLock(), json, (firmware_event, ChildLockEvent(value))) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_clean.py b/tests/commands/json/test_clean.py index 571cefe03..7955a049b 100644 --- a/tests/commands/json/test_clean.py +++ b/tests/commands/json/test_clean.py @@ -14,7 +14,7 @@ GetCleanInfoV2, ) from deebot_client.event_bus import EventBus -from deebot_client.events import StateEvent +from deebot_client.events import FirmwareEvent, StateEvent from deebot_client.models import ApiDeviceInfo, CleanAction, CleanMode, State from tests.helpers import get_request_json, get_success_body @@ -26,7 +26,7 @@ @pytest.mark.parametrize("command", [GetCleanInfo(), GetCleanInfoV2()]) @pytest.mark.parametrize( - ("json", "expected"), + ("data", "expected"), [ ( get_request_json(get_success_body({"trigger": "none", "state": "idle"})), @@ -35,9 +35,12 @@ ], ) async def test_GetCleanInfo( - command: GetCleanInfo, json: dict[str, Any], expected: StateEvent + command: GetCleanInfo, + data: tuple[dict[str, Any], FirmwareEvent], + expected: StateEvent, ) -> None: - await assert_command(command, json, expected) + json, firmware_event = data + await assert_command(command, json, (firmware_event, expected)) @pytest.mark.parametrize("command_type", [Clean, CleanV2]) diff --git a/tests/commands/json/test_clean_count.py b/tests/commands/json/test_clean_count.py index dc7e81689..12fac2666 100644 --- a/tests/commands/json/test_clean_count.py +++ b/tests/commands/json/test_clean_count.py @@ -10,8 +10,8 @@ async def test_GetCleanCount() -> None: - json = get_request_json(get_success_body({"count": 2})) - await assert_command(GetCleanCount(), json, CleanCountEvent(2)) + json, firmware_event = get_request_json(get_success_body({"count": 2})) + await assert_command(GetCleanCount(), json, (firmware_event, CleanCountEvent(2))) @pytest.mark.parametrize("count", [1, 2, 3]) diff --git a/tests/commands/json/test_clean_preference.py b/tests/commands/json/test_clean_preference.py index ae18c43e8..6cace333e 100644 --- a/tests/commands/json/test_clean_preference.py +++ b/tests/commands/json/test_clean_preference.py @@ -11,8 +11,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetCleanPreference(*, value: bool) -> None: - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetCleanPreference(), json, CleanPreferenceEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetCleanPreference(), json, (firmware_event, CleanPreferenceEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_continuous_cleaning.py b/tests/commands/json/test_continuous_cleaning.py index 19ab1092b..33e23f3f0 100644 --- a/tests/commands/json/test_continuous_cleaning.py +++ b/tests/commands/json/test_continuous_cleaning.py @@ -11,8 +11,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetContinuousCleaning(*, value: bool) -> None: - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetContinuousCleaning(), json, ContinuousCleaningEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetContinuousCleaning(), json, (firmware_event, ContinuousCleaningEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_cross_map_border_warning.py b/tests/commands/json/test_cross_map_border_warning.py index 95d24fb94..df71015d0 100644 --- a/tests/commands/json/test_cross_map_border_warning.py +++ b/tests/commands/json/test_cross_map_border_warning.py @@ -17,9 +17,13 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetCrossMapBorderWarning(*, value: bool) -> None: """Testing get cross map border warning.""" - json = get_request_json(get_success_body({"enable": 1 if value else 0})) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) await assert_command( - GetCrossMapBorderWarning(), json, CrossMapBorderWarningEvent(value) + GetCrossMapBorderWarning(), + json, + (firmware_event, CrossMapBorderWarningEvent(value)), ) diff --git a/tests/commands/json/test_custom.py b/tests/commands/json/test_custom.py index 0bcc6ab64..bc098a771 100644 --- a/tests/commands/json/test_custom.py +++ b/tests/commands/json/test_custom.py @@ -18,9 +18,9 @@ [ ( CustomCommand("getSleep"), - get_request_json(get_success_body({"enable": 1})), + get_request_json(get_success_body({"enable": 1}))[0], CustomCommandEvent( - "getSleep", get_message_json(get_success_body({"enable": 1})) + "getSleep", get_message_json(get_success_body({"enable": 1}))[0] ), CommandResult.success(), ), diff --git a/tests/commands/json/test_cut_direction.py b/tests/commands/json/test_cut_direction.py index fe9a9aff1..c1411da27 100644 --- a/tests/commands/json/test_cut_direction.py +++ b/tests/commands/json/test_cut_direction.py @@ -10,8 +10,10 @@ async def test_GetCutDirection() -> None: - json = get_request_json(get_success_body({"angle": 90})) - await assert_command(GetCutDirection(), json, CutDirectionEvent(90)) + json, firmware_event = get_request_json(get_success_body({"angle": 90})) + await assert_command( + GetCutDirection(), json, (firmware_event, CutDirectionEvent(90)) + ) @pytest.mark.parametrize("angle", [1, 45, 90]) diff --git a/tests/commands/json/test_efficiency.py b/tests/commands/json/test_efficiency.py index 71dcb68ce..d876c94e9 100644 --- a/tests/commands/json/test_efficiency.py +++ b/tests/commands/json/test_efficiency.py @@ -24,8 +24,8 @@ async def test_GetEfficiencyMode( json: dict[str, Any], expected: EfficiencyModeEvent ) -> None: - json = get_request_json(get_success_body(json)) - await assert_command(GetEfficiencyMode(), json, expected) + json, firmware_event = get_request_json(get_success_body(json)) + await assert_command(GetEfficiencyMode(), json, (firmware_event, expected)) @pytest.mark.parametrize(("value"), [EfficiencyMode.STANDARD_MODE, "standard_mode"]) diff --git a/tests/commands/json/test_error.py b/tests/commands/json/test_error.py index 2d0423015..3703ff01b 100644 --- a/tests/commands/json/test_error.py +++ b/tests/commands/json/test_error.py @@ -20,16 +20,16 @@ @pytest.mark.parametrize( ("body", "expected_events"), [ - ({"code": [0]}, ErrorEvent(0, "NoError: Robot is operational")), - ({"code": []}, ErrorEvent(0, "NoError: Robot is operational")), + ({"code": [0]}, (ErrorEvent(0, "NoError: Robot is operational"),)), + ({"code": []}, (ErrorEvent(0, "NoError: Robot is operational"),)), ( {"code": [105]}, - [StateEvent(State.ERROR), ErrorEvent(105, "Stuck: Robot is stuck")], + (StateEvent(State.ERROR), ErrorEvent(105, "Stuck: Robot is stuck")), ), ], ) async def test_getErrors( - body: dict[str, Any], expected_events: Event | Sequence[Event] + body: dict[str, Any], expected_events: Sequence[Event] ) -> None: - json = get_request_json(get_success_body(body)) - await assert_command(GetError(), json, expected_events) + json, firmware_event = get_request_json(get_success_body(body)) + await assert_command(GetError(), json, (firmware_event, *expected_events)) diff --git a/tests/commands/json/test_fan_speed.py b/tests/commands/json/test_fan_speed.py index d3cffd7a2..6ab8287a9 100644 --- a/tests/commands/json/test_fan_speed.py +++ b/tests/commands/json/test_fan_speed.py @@ -14,8 +14,10 @@ async def test_GetFanSpeed() -> None: - json = get_request_json(get_success_body({"speed": 2})) - await assert_command(GetFanSpeed(), json, FanSpeedEvent(FanSpeedLevel.MAX_PLUS)) + json, firmware_event = get_request_json(get_success_body({"speed": 2})) + await assert_command( + GetFanSpeed(), json, (firmware_event, FanSpeedEvent(FanSpeedLevel.MAX_PLUS)) + ) @pytest.mark.parametrize(("value"), [FanSpeedLevel.MAX, "max"]) diff --git a/tests/commands/json/test_life_span.py b/tests/commands/json/test_life_span.py index 840409f72..dbc07d1f1 100644 --- a/tests/commands/json/test_life_span.py +++ b/tests/commands/json/test_life_span.py @@ -6,14 +6,14 @@ from deebot_client.commands.json import GetLifeSpan from deebot_client.commands.json.life_span import ResetLifeSpan -from deebot_client.events import LifeSpan, LifeSpanEvent +from deebot_client.events import FirmwareEvent, LifeSpan, LifeSpanEvent from tests.helpers import get_request_json, get_success_body from . import assert_command, assert_execute_command @pytest.mark.parametrize( - ("command", "json", "expected"), + ("command", "data", "expected"), [ ( GetLifeSpan( @@ -56,7 +56,7 @@ ] ) ), - [ + ( LifeSpanEvent(LifeSpan.BRUSH, 99.88, 17979), LifeSpanEvent(LifeSpan.FILTER, 99.71, 7179), LifeSpanEvent(LifeSpan.SIDE_BRUSH, 99.74, 8977), @@ -70,56 +70,56 @@ LifeSpanEvent(LifeSpan.DUST_BAG, 67.7, 2031), LifeSpanEvent(LifeSpan.HAND_FILTER, 100.0, 30000), LifeSpanEvent(LifeSpan.STRAINER, 100.0, 1800), - ], + ), ), ( GetLifeSpan({LifeSpan.BRUSH}), get_request_json( get_success_body([{"type": "brush", "left": 17979, "total": 18000}]) ), - [LifeSpanEvent(LifeSpan.BRUSH, 99.88, 17979)], + (LifeSpanEvent(LifeSpan.BRUSH, 99.88, 17979),), ), ( GetLifeSpan([LifeSpan.FILTER]), get_request_json( get_success_body([{"type": "heap", "left": 7179, "total": 7200}]) ), - [LifeSpanEvent(LifeSpan.FILTER, 99.71, 7179)], + (LifeSpanEvent(LifeSpan.FILTER, 99.71, 7179),), ), ( GetLifeSpan([LifeSpan.SIDE_BRUSH]), get_request_json( get_success_body([{"type": "sideBrush", "left": 8977, "total": 9000}]) ), - [LifeSpanEvent(LifeSpan.SIDE_BRUSH, 99.74, 8977)], + (LifeSpanEvent(LifeSpan.SIDE_BRUSH, 99.74, 8977),), ), ( GetLifeSpan({LifeSpan.UNIT_CARE}), get_request_json( get_success_body([{"type": "unitCare", "left": 265, "total": 1800}]) ), - [LifeSpanEvent(LifeSpan.UNIT_CARE, 14.72, 265)], + (LifeSpanEvent(LifeSpan.UNIT_CARE, 14.72, 265),), ), ( GetLifeSpan({LifeSpan.ROUND_MOP}), get_request_json( get_success_body([{"type": "roundMop", "left": 6820, "total": 9000}]) ), - [LifeSpanEvent(LifeSpan.ROUND_MOP, 75.78, 6820)], + (LifeSpanEvent(LifeSpan.ROUND_MOP, 75.78, 6820),), ), ( GetLifeSpan({LifeSpan.AIR_FRESHENER}), get_request_json( get_success_body([{"type": "dModule", "left": 17537, "total": 18000}]) ), - [LifeSpanEvent(LifeSpan.AIR_FRESHENER, 97.43, 17537)], + (LifeSpanEvent(LifeSpan.AIR_FRESHENER, 97.43, 17537),), ), ( GetLifeSpan({LifeSpan.UV_SANITIZER}), get_request_json( get_success_body([{"type": "uv", "left": 898586, "total": 900000}]) ), - [LifeSpanEvent(LifeSpan.UV_SANITIZER, 99.84, 898586)], + (LifeSpanEvent(LifeSpan.UV_SANITIZER, 99.84, 898586),), ), ( GetLifeSpan({LifeSpan.HUMIDIFY}), @@ -128,14 +128,14 @@ [{"type": "humidify", "left": 191547, "total": 194400}] ) ), - [LifeSpanEvent(LifeSpan.HUMIDIFY, 98.53, 191547)], + (LifeSpanEvent(LifeSpan.HUMIDIFY, 98.53, 191547),), ), ( GetLifeSpan({LifeSpan.HUMIDIFY_MAINTENANCE}), get_request_json( get_success_body([{"type": "wbCare", "left": 22260, "total": 43200}]) ), - [LifeSpanEvent(LifeSpan.HUMIDIFY_MAINTENANCE, 51.53, 22260)], + (LifeSpanEvent(LifeSpan.HUMIDIFY_MAINTENANCE, 51.53, 22260),), ), ( GetLifeSpan({LifeSpan.CLEANING_FLUID}), @@ -144,14 +144,14 @@ [{"type": "autoWater_cleaningFluid", "left": 86400, "total": 86400}] ) ), - [LifeSpanEvent(LifeSpan.CLEANING_FLUID, 100.0, 86400)], + (LifeSpanEvent(LifeSpan.CLEANING_FLUID, 100.0, 86400),), ), ( GetLifeSpan({LifeSpan.DUST_BAG}), get_request_json( get_success_body([{"type": "dustBag", "left": 2031, "total": 3000}]) ), - [LifeSpanEvent(LifeSpan.DUST_BAG, 67.7, 2031)], + (LifeSpanEvent(LifeSpan.DUST_BAG, 67.7, 2031),), ), ( GetLifeSpan({LifeSpan.HAND_FILTER}), @@ -160,21 +160,24 @@ [{"type": "handFilter", "left": 30000, "total": 30000}] ) ), - [LifeSpanEvent(LifeSpan.HAND_FILTER, 100.0, 30000)], + (LifeSpanEvent(LifeSpan.HAND_FILTER, 100.0, 30000),), ), ( GetLifeSpan({LifeSpan.STRAINER}), get_request_json( get_success_body([{"type": "strainer", "left": 1800, "total": 1800}]) ), - [LifeSpanEvent(LifeSpan.STRAINER, 100.0, 1800)], + (LifeSpanEvent(LifeSpan.STRAINER, 100.0, 1800),), ), ], ) async def test_GetLifeSpan( - command: GetLifeSpan, json: dict[str, Any], expected: list[LifeSpanEvent] + command: GetLifeSpan, + data: tuple[dict[str, Any], FirmwareEvent], + expected: tuple[LifeSpanEvent, ...], ) -> None: - await assert_command(command, json, expected) + json, firmware_event = data + await assert_command(command, json, (firmware_event, *expected)) @pytest.mark.parametrize( diff --git a/tests/commands/json/test_map.py b/tests/commands/json/test_map.py index 540c53a1c..9bd3e6001 100644 --- a/tests/commands/json/test_map.py +++ b/tests/commands/json/test_map.py @@ -15,6 +15,7 @@ ) from deebot_client.commands.json.map import GetMapSetV2 from deebot_client.events import ( + FirmwareEvent, MajorMapEvent, MapSetEvent, MapSetType, @@ -72,7 +73,7 @@ async def test_getMapSubSet_customName( _type = MapSetType.ROOMS mid = "98100521" mssid = "8" - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "type": _type.value, @@ -96,7 +97,7 @@ async def test_getMapSubSet_customName( await assert_command( GetMapSubSet(mid=mid, mssid=mssid, msid="1"), json, - MapSubsetEvent(8, _type, expected_coordinates, expected_name), + [firmware_event, MapSubsetEvent(8, _type, expected_coordinates, expected_name)], ) @@ -122,12 +123,12 @@ async def test_getMapSubSet_invalid( "mid": mid, **additional_data, } - json = get_request_json(get_success_body(data)) + json, firmware_event = get_request_json(get_success_body(data)) with LogCapture() as log: await assert_command( GetMapSubSet(mid=mid, mssid=mssid, msid="1"), json, - None, + firmware_event, command_result=CommandResult(HandlingState.ANALYSE_LOGGED), ) @@ -147,7 +148,9 @@ async def test_getMapSubSet_invalid( ) -def _getMapSubSet_room_valid_response(value: str, id: int) -> dict[str, Any]: +def _getMapSubSet_room_valid_response( + value: str, id: int +) -> tuple[dict[str, Any], FirmwareEvent]: return get_request_json( get_success_body( { @@ -164,11 +167,11 @@ def _getMapSubSet_room_valid_response(value: str, id: int) -> dict[str, Any]: async def test_getMapSubSet_living_room() -> None: value = "-1400,-1600;-1400,-1350;-950,-1100;-900,-150;-550,100;200,950;500,950;650,800;800,950;1850,950;1950,800;1950,-200;2050,-300;2300,-300;2550,-650;2700,-650;2700,-1600;2400,-1750;2700,-1900;2700,-2950;2450,-2950;2300,-3100;2400,-3200;2650,-3200;2700,-3500;2300,-3500;2200,-3250;2050,-3550;1200,-3550;1200,-3300;1050,-3200;950,-3300;950,-3550;600,-3550;550,-2850;850,-2800;950,-2700;850,-2600;950,-2400;900,-2350;800,-2300;550,-2500;550,-2350;400,-2250;200,-2650;-800,-2650;-950,-2550;-950,-2150;-650,-2000;-450,-2000;-400,-1950;-450,-1850;-750,-1800;-950,-1900;-1350,-1900;-1400,-1600" - json = _getMapSubSet_room_valid_response(value, 7) + json, firmware_event = _getMapSubSet_room_valid_response(value, 7) await assert_command( GetMapSubSet(mid="199390082", mssid="7", msid="1"), json, - MapSubsetEvent(7, MapSetType.ROOMS, value, "Living Room"), + [firmware_event, MapSubsetEvent(7, MapSetType.ROOMS, value, "Living Room")], ) @@ -185,7 +188,7 @@ async def test_getCachedMapInfo( ) -> None: expected_mid = "199390082" expected_name = "Erdgeschoss" - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "enable": 1, @@ -213,7 +216,11 @@ async def test_getCachedMapInfo( await assert_command( command, json, - CachedMapInfoEvent(expected_name, active=True), + [ + firmware_event, + CachedMapInfoEvent(expected_name, active=True), + *[firmware_event for _ in MapSetType], + ], command_result=CommandResult( HandlingState.SUCCESS, {"map_id": expected_mid}, @@ -225,7 +232,7 @@ async def test_getCachedMapInfo( async def test_getMajorMap() -> None: expected_mid = "199390082" value = "1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,3378526980,2963288214,2739565817,729228561,2452519304,1295764014,1295764014,1295764014,2753376360,329080101,952462272,3648890579,412193448,1540631558,1295764014,1295764014,1561391782,1081327924,1096350476,2860639280,37066625,86907282,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014,1295764014" - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "mid": expected_mid, @@ -307,7 +314,7 @@ async def test_getMajorMap() -> None: await assert_command( GetMajorMap(), json, - MajorMapEvent(expected_mid, expected_vaue, requested=True), + [firmware_event, MajorMapEvent(expected_mid, expected_vaue, requested=True)], ) @@ -316,40 +323,46 @@ async def test_getMapSet() -> None: msid = "8" room_value = "-442,2910;-442,982;1214,982;1214,2910" subsets = [7, 12, 17, 14, 10, 11, 13] + data, firmware_event = get_request_json( + get_success_body( + { + "type": "ar", + "count": 7, + "mid": mid, + "msid": msid, + "subsets": [ + {"mssid": "7"}, + {"mssid": "12"}, + {"mssid": "17"}, + {"mssid": "14"}, + {"mssid": "10"}, + {"mssid": "11"}, + {"mssid": "13"}, + ], + } + ) + ) json = ( # getMapSet response - get_request_json( - get_success_body( - { - "type": "ar", - "count": 7, - "mid": mid, - "msid": msid, - "subsets": [ - {"mssid": "7"}, - {"mssid": "12"}, - {"mssid": "17"}, - {"mssid": "14"}, - {"mssid": "10"}, - {"mssid": "11"}, - {"mssid": "13"}, - ], - } - ) - ), + data, # getMapSubSet response - *(_getMapSubSet_room_valid_response(room_value, subset) for subset in subsets), + *( + _getMapSubSet_room_valid_response(room_value, subset)[0] + for subset in subsets + ), ) + events = [firmware_event, MapSetEvent(MapSetType.ROOMS, subsets)] + for subset in subsets: + events.extend( + [ + firmware_event, + MapSubsetEvent(subset, MapSetType.ROOMS, room_value, "Living Room"), + ] + ) await assert_command( GetMapSet(mid), json, - ( - MapSetEvent(MapSetType.ROOMS, subsets), - *( - MapSubsetEvent(subset, MapSetType.ROOMS, room_value, "Living Room") - for subset in subsets - ), - ), + events, command_result=CommandResult( HandlingState.SUCCESS, {"id": mid, "set_id": msid, "type": MapSetType.ROOMS, "subsets": subsets}, @@ -364,7 +377,7 @@ async def test_getMapSet() -> None: async def test_getMapSetV2_no_mop_zones() -> None: mid = "199390082" type = MapSetType.NO_MOP_ZONES - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "type": type, @@ -381,6 +394,7 @@ async def test_getMapSetV2_no_mop_zones() -> None: GetMapSetV2(mid, type), json, ( + firmware_event, MapSubsetEvent( 4, type, @@ -400,36 +414,39 @@ async def test_getMapSetV2_rooms() -> None: ) subsets = [0, 1, 6, 2, 7, 3, 5] room_value = "-442,2910;-442,982;1214,982;1214,2910" + data, firmware_event = get_request_json( + get_success_body( + { + "type": type, + "mid": mid, + "msid": msid, + "batid": "gheijg", + "serial": 1, + "index": 1, + "subsets": subsets_comp, + "infoSize": 199, + } + ) + ) json = ( # GetMapSetV2 response - get_request_json( - get_success_body( - { - "type": type, - "mid": mid, - "msid": msid, - "batid": "gheijg", - "serial": 1, - "index": 1, - "subsets": subsets_comp, - "infoSize": 199, - } - ) - ), + data, # getMapSubSet response - *(_getMapSubSet_room_valid_response(room_value, subset) for subset in subsets), + *( + _getMapSubSet_room_valid_response(room_value, subset)[0] + for subset in subsets + ), ) + events = [firmware_event, MapSetEvent(MapSetType(type), subsets)] + for subset in subsets: + events.extend( + [firmware_event, MapSubsetEvent(subset, type, room_value, "Living Room")] + ) await assert_command( GetMapSetV2(mid, type), json, - ( - MapSetEvent(MapSetType(type), subsets), - *( - MapSubsetEvent(subset, MapSetType.ROOMS, room_value, "Living Room") - for subset in subsets - ), - ), + events, command_result=CommandResult( HandlingState.SUCCESS, {"id": mid, "set_id": msid, "type": MapSetType(type), "subsets": subsets}, @@ -441,7 +458,7 @@ async def test_getMapSetV2_rooms() -> None: async def test_getMapSetV2_virtual_walls() -> None: mid = "199390082" type = MapSetType.VIRTUAL_WALLS - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "type": type, @@ -475,10 +492,13 @@ async def test_getMapSetV2_virtual_walls() -> None: await assert_command( GetMapSetV2(mid, type), json, - [ - MapSubsetEvent(int(subs["mssid"]), type, str(subs["coordinates"])) - for subs in expected_walls - ], + ( + firmware_event, + *[ + MapSubsetEvent(int(subs["mssid"]), type, str(subs["coordinates"])) + for subs in expected_walls + ], + ), ) @@ -486,7 +506,10 @@ async def test_getMapTrace() -> None: start = 0 total = 160 trace_value = "REMOVED" - json = get_request_json( + ( + json, + firmware_event, + ) = get_request_json( get_success_body( { "tid": "173207", @@ -500,7 +523,7 @@ async def test_getMapTrace() -> None: await assert_command( GetMapTrace(start), json, - MapTraceEvent(start=start, total=total, data=trace_value), + (firmware_event, MapTraceEvent(start=start, total=total, data=trace_value)), command_result=CommandResult( HandlingState.SUCCESS, {"start": start, "total": total}, [] ), diff --git a/tests/commands/json/test_moveup_warning.py b/tests/commands/json/test_moveup_warning.py index 4f17d2629..a3e380745 100644 --- a/tests/commands/json/test_moveup_warning.py +++ b/tests/commands/json/test_moveup_warning.py @@ -14,8 +14,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetMoveUpWarning(*, value: bool) -> None: """Testing get moveup warning.""" - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetMoveUpWarning(), json, MoveUpWarningEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetMoveUpWarning(), json, (firmware_event, MoveUpWarningEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_mulitmap_state.py b/tests/commands/json/test_mulitmap_state.py index f0fc7f954..70d143882 100644 --- a/tests/commands/json/test_mulitmap_state.py +++ b/tests/commands/json/test_mulitmap_state.py @@ -11,8 +11,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetMultimapState(*, value: bool) -> None: - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetMultimapState(), json, MultimapStateEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetMultimapState(), json, (firmware_event, MultimapStateEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_network.py b/tests/commands/json/test_network.py index 5617789b3..dd22f85f1 100644 --- a/tests/commands/json/test_network.py +++ b/tests/commands/json/test_network.py @@ -10,7 +10,7 @@ async def test_GetNetInfo() -> None: - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "ip": "192.168.1.100", @@ -24,8 +24,11 @@ async def test_GetNetInfo() -> None: await assert_command( GetNetInfo(), json, - NetworkInfoEvent( - ip="192.168.1.100", ssid="WLAN", rssi=-61, mac="AA:BB:CC:DD:EE:FF" + ( + firmware_event, + NetworkInfoEvent( + ip="192.168.1.100", ssid="WLAN", rssi=-61, mac="AA:BB:CC:DD:EE:FF" + ), ), ) diff --git a/tests/commands/json/test_ota.py b/tests/commands/json/test_ota.py index 5afeab757..6b3c45068 100644 --- a/tests/commands/json/test_ota.py +++ b/tests/commands/json/test_ota.py @@ -18,7 +18,7 @@ ], ) async def test_GetOta(*, auto_enabled: bool, support_auto: bool) -> None: - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "autoSwitch": 1 if auto_enabled else 0, @@ -32,12 +32,15 @@ async def test_GetOta(*, auto_enabled: bool, support_auto: bool) -> None: await assert_command( GetOta(), json, - OtaEvent( - auto_enabled=auto_enabled, - support_auto=support_auto, - version="1.7.2", - status="idle", - progress=0, + ( + firmware_event, + OtaEvent( + auto_enabled=auto_enabled, + support_auto=support_auto, + version="1.7.2", + status="idle", + progress=0, + ), ), ) diff --git a/tests/commands/json/test_pos.py b/tests/commands/json/test_pos.py index 39090a8e2..47a9f592d 100644 --- a/tests/commands/json/test_pos.py +++ b/tests/commands/json/test_pos.py @@ -9,15 +9,18 @@ async def test_GetPos() -> None: - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( {"chargePos": {"x": 5, "y": 9}, "deebotPos": {"x": 1, "y": 5, "a": 85}} ) ) - expected_event = PositionsEvent( - positions=[ - Position(type=PositionType.DEEBOT, x=1, y=5, a=85), - Position(type=PositionType.CHARGER, x=5, y=9, a=0), - ] + expected_events = ( + firmware_event, + PositionsEvent( + positions=[ + Position(type=PositionType.DEEBOT, x=1, y=5, a=85), + Position(type=PositionType.CHARGER, x=5, y=9, a=0), + ] + ), ) - await assert_command(GetPos(), json, expected_event) + await assert_command(GetPos(), json, expected_events) diff --git a/tests/commands/json/test_safe_protect.py b/tests/commands/json/test_safe_protect.py index 44ff5ca5b..db6e72d5a 100644 --- a/tests/commands/json/test_safe_protect.py +++ b/tests/commands/json/test_safe_protect.py @@ -14,8 +14,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetSafeProtect(*, value: bool) -> None: """Testing get safe protect.""" - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetSafeProtect(), json, SafeProtectEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetSafeProtect(), json, (firmware_event, SafeProtectEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_station_state.py b/tests/commands/json/test_station_state.py index 512a697e0..59b08be12 100644 --- a/tests/commands/json/test_station_state.py +++ b/tests/commands/json/test_station_state.py @@ -23,7 +23,7 @@ async def test_GetStationState( additional_content: dict[str, Any], expected: State, ) -> None: - json = get_request_json( + json, firmware_event = get_request_json( get_success_body( { "content": {"error": [], **additional_content}, @@ -31,4 +31,6 @@ async def test_GetStationState( } ) ) - await assert_command(GetStationState(), json, StationEvent(expected)) + await assert_command( + GetStationState(), json, (firmware_event, StationEvent(expected)) + ) diff --git a/tests/commands/json/test_sweep_mode.py b/tests/commands/json/test_sweep_mode.py index 834182af5..3392443f4 100644 --- a/tests/commands/json/test_sweep_mode.py +++ b/tests/commands/json/test_sweep_mode.py @@ -11,8 +11,10 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetSweepMode(*, value: bool) -> None: - json = get_request_json(get_success_body({"type": 1 if value else 0})) - await assert_command(GetSweepMode(), json, SweepModeEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"type": 1 if value else 0}) + ) + await assert_command(GetSweepMode(), json, (firmware_event, SweepModeEvent(value))) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_true_detect.py b/tests/commands/json/test_true_detect.py index ba5c75651..afbcf5255 100644 --- a/tests/commands/json/test_true_detect.py +++ b/tests/commands/json/test_true_detect.py @@ -11,8 +11,12 @@ @pytest.mark.parametrize("value", [False, True]) async def test_GetTrueDetect(*, value: bool) -> None: - json = get_request_json(get_success_body({"enable": 1 if value else 0})) - await assert_command(GetTrueDetect(), json, TrueDetectEvent(value)) + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) + await assert_command( + GetTrueDetect(), json, (firmware_event, TrueDetectEvent(value)) + ) @pytest.mark.parametrize("value", [False, True]) diff --git a/tests/commands/json/test_voice_assistant_state.py b/tests/commands/json/test_voice_assistant_state.py index 4236806b4..f2a9172fb 100644 --- a/tests/commands/json/test_voice_assistant_state.py +++ b/tests/commands/json/test_voice_assistant_state.py @@ -10,15 +10,19 @@ @pytest.mark.parametrize("value", [False, True]) -async def test_GetTrueDetect(*, value: bool) -> None: - json = get_request_json(get_success_body({"enable": 1 if value else 0})) +async def test_GetVoiceAssistantState(*, value: bool) -> None: + json, firmware_event = get_request_json( + get_success_body({"enable": 1 if value else 0}) + ) await assert_command( - GetVoiceAssistantState(), json, VoiceAssistantStateEvent(value) + GetVoiceAssistantState(), + json, + (firmware_event, VoiceAssistantStateEvent(value)), ) @pytest.mark.parametrize("value", [False, True]) -async def test_SetTrueDetect(*, value: bool) -> None: +async def test_SetVoiceAssistantState(*, value: bool) -> None: await assert_set_enable_command( SetVoiceAssistantState(value), VoiceAssistantStateEvent, enabled=value ) diff --git a/tests/commands/json/test_volume.py b/tests/commands/json/test_volume.py index dc0e767dc..480c4e433 100644 --- a/tests/commands/json/test_volume.py +++ b/tests/commands/json/test_volume.py @@ -10,8 +10,10 @@ async def test_GetVolume() -> None: - json = get_request_json(get_success_body({"volume": 2, "total": 10})) - await assert_command(GetVolume(), json, VolumeEvent(2, 10)) + json, firmware_event = get_request_json( + get_success_body({"volume": 2, "total": 10}) + ) + await assert_command(GetVolume(), json, (firmware_event, VolumeEvent(2, 10))) @pytest.mark.parametrize("level", [0, 2, 10]) diff --git a/tests/commands/json/test_water_info.py b/tests/commands/json/test_water_info.py index 2ac44e4a3..3b0826d7d 100644 --- a/tests/commands/json/test_water_info.py +++ b/tests/commands/json/test_water_info.py @@ -27,44 +27,42 @@ @pytest.mark.parametrize( ("json", "expected"), [ - ({"amount": 2}, WaterAmountEvent(WaterAmount.MEDIUM)), + ({"amount": 2}, (WaterAmountEvent(WaterAmount.MEDIUM),)), ( {"amount": 1, "enable": 1}, - [ + ( WaterAmountEvent(WaterAmount.LOW), MopAttachedEvent(True), - ], + ), ), ( {"amount": 4, "enable": 0}, - [ + ( WaterAmountEvent(WaterAmount.ULTRAHIGH), MopAttachedEvent(False), - ], + ), ), ( {"amount": 4, "sweepType": 1, "enable": 0}, - [ + ( WaterAmountEvent(WaterAmount.ULTRAHIGH), MopAttachedEvent(False), WaterSweepTypeEvent(SweepType.STANDARD), - ], + ), ), ( {"amount": 4, "sweepType": 2, "enable": 0}, - [ + ( WaterAmountEvent(WaterAmount.ULTRAHIGH), MopAttachedEvent(False), WaterSweepTypeEvent(SweepType.DEEP), - ], + ), ), ], ) -async def test_GetWaterInfo( - json: dict[str, Any], expected: Event | list[Event] -) -> None: - json = get_request_json(get_success_body(json)) - await assert_command(GetWaterInfo(), json, expected) +async def test_GetWaterInfo(json: dict[str, Any], expected: tuple[Event, ...]) -> None: + json, firmware_event = get_request_json(get_success_body(json)) + await assert_command(GetWaterInfo(), json, (firmware_event, *expected)) @pytest.mark.parametrize(("water_value"), [WaterAmount.MEDIUM, "medium"]) diff --git a/tests/commands/json/test_work_mode.py b/tests/commands/json/test_work_mode.py index b60d7c4bf..9740457f8 100644 --- a/tests/commands/json/test_work_mode.py +++ b/tests/commands/json/test_work_mode.py @@ -24,8 +24,8 @@ ], ) async def test_GetWorkMode(json: dict[str, Any], expected: WorkModeEvent) -> None: - json = get_request_json(get_success_body(json)) - await assert_command(GetWorkMode(), json, expected) + json, firmware_event = get_request_json(get_success_body(json)) + await assert_command(GetWorkMode(), json, (firmware_event, expected)) @pytest.mark.parametrize(("value"), [WorkMode.MOP_AFTER_VACUUM, "mop_after_vacuum"]) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 1c0c44a29..fb4e5e7ea 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -5,6 +5,7 @@ from deebot_client.capabilities import Capabilities from deebot_client.const import DataType +from deebot_client.events import FirmwareEvent from deebot_client.models import StaticDeviceInfo if TYPE_CHECKING: @@ -14,8 +15,9 @@ from deebot_client.events.base import Event -def get_request_json(body: dict[str, Any]) -> dict[str, Any]: - return {"id": "ALZf", "ret": "ok", "resp": get_message_json(body)} +def get_request_json(body: dict[str, Any]) -> tuple[dict[str, Any], FirmwareEvent]: + message, firmware = get_message_json(body) + return {"id": "ALZf", "ret": "ok", "resp": message}, firmware def get_success_body(data: dict[str, Any] | None | list[Any] = None) -> dict[str, Any]: @@ -29,22 +31,26 @@ def get_success_body(data: dict[str, Any] | None | list[Any] = None) -> dict[str return body -def get_message_json(body: dict[str, Any]) -> dict[str, Any]: - return { - "header": { - "pri": 1, - "tzm": 480, - "ts": "1304623069888", - "ver": "0.0.1", - "fwVer": "1.8.2", - "hwVer": "0.1.1", +def get_message_json(body: dict[str, Any]) -> tuple[dict[str, Any], FirmwareEvent]: + return ( + { + "header": { + "pri": 1, + "tzm": 480, + "ts": "1304623069888", + "ver": "0.0.1", + "fwVer": "1.8.2", + "hwVer": "0.1.1", + }, + "body": body, }, - "body": body, - } + FirmwareEvent("1.8.2"), + ) def mock_static_device_info( events: Mapping[type[Event], list[Command]] | None = None, + data_type: DataType = DataType.JSON, ) -> StaticDeviceInfo: """Mock static device info.""" if events is None: @@ -57,4 +63,4 @@ def get_refresh_commands(event: type[Event]) -> list[Command]: mock.get_refresh_commands.side_effect = get_refresh_commands - return StaticDeviceInfo(DataType.JSON, mock) + return StaticDeviceInfo(data_type, mock) diff --git a/tests/messages/__init__.py b/tests/messages/__init__.py index 2dff959e9..501dd85bc 100644 --- a/tests/messages/__init__.py +++ b/tests/messages/__init__.py @@ -1,29 +1,36 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any -from unittest.mock import Mock +from collections.abc import Sequence +from typing import TYPE_CHECKING +from unittest.mock import Mock, call from deebot_client.event_bus import EventBus -from deebot_client.message import HandlingState, Message +from deebot_client.message import HandlingState, Message, MessagePayloadType if TYPE_CHECKING: from deebot_client.events import Event def assert_message( - message: type[Message], data: dict[str, Any] | str, expected_event: Event + message: type[Message], + data: MessagePayloadType, + expected_events: Event | Sequence[Event], ) -> None: event_bus = Mock(spec_set=EventBus) result = message.handle(event_bus, data) assert result.state == HandlingState.SUCCESS - event_bus.notify.assert_called_once_with(expected_event) + if isinstance(expected_events, Sequence): + event_bus.notify.assert_has_calls([call(x) for x in expected_events]) + assert event_bus.notify.call_count == len(expected_events) + else: + event_bus.notify.assert_called_once_with(expected_events) def assert_message_failure( message: type[Message], - data: dict[str, Any] | str, + data: MessagePayloadType, expected_result_state: HandlingState, ) -> None: event_bus = Mock(spec_set=EventBus) diff --git a/tests/messages/json/test_auto_empty.py b/tests/messages/json/test_auto_empty.py index f114740fe..1a85c40a3 100644 --- a/tests/messages/json/test_auto_empty.py +++ b/tests/messages/json/test_auto_empty.py @@ -4,7 +4,7 @@ import pytest -from deebot_client.events import auto_empty +from deebot_client.events import FirmwareEvent, auto_empty from deebot_client.messages.json.auto_empty import OnAutoEmpty from tests.messages import assert_message @@ -37,4 +37,8 @@ def test_onAutoEmpty( if frequency is not None: data["body"]["data"]["frequency"] = frequency - assert_message(OnAutoEmpty, data, auto_empty.AutoEmptyEvent(enable, expected_freq)) + assert_message( + OnAutoEmpty, + data, + (FirmwareEvent("1.30.0"), auto_empty.AutoEmptyEvent(enable, expected_freq)), + ) diff --git a/tests/messages/json/test_battery.py b/tests/messages/json/test_battery.py index a12e19f44..110f2b3cc 100644 --- a/tests/messages/json/test_battery.py +++ b/tests/messages/json/test_battery.py @@ -2,7 +2,7 @@ import pytest -from deebot_client.events import BatteryEvent +from deebot_client.events import BatteryEvent, FirmwareEvent from deebot_client.messages.json import OnBattery from tests.messages import assert_message @@ -21,4 +21,4 @@ def test_onBattery(percentage: int) -> None: "body": {"data": {"value": percentage, "isLow": 1 if percentage < 20 else 0}}, } - assert_message(OnBattery, data, BatteryEvent(percentage)) + assert_message(OnBattery, data, (FirmwareEvent("1.8.2"), BatteryEvent(percentage))) diff --git a/tests/messages/json/test_station_state.py b/tests/messages/json/test_station_state.py index e1bf4084c..f8fd3c31b 100644 --- a/tests/messages/json/test_station_state.py +++ b/tests/messages/json/test_station_state.py @@ -4,6 +4,7 @@ import pytest +from deebot_client.events import FirmwareEvent from deebot_client.events.station import State, StationEvent from deebot_client.messages.json.station_state import OnStationState from tests.messages import assert_message @@ -38,4 +39,6 @@ def test_onStationState( }, } - assert_message(OnStationState, data, StationEvent(expected)) + assert_message( + OnStationState, data, (FirmwareEvent("1.30.0"), StationEvent(expected)) + ) diff --git a/tests/messages/json/test_stats.py b/tests/messages/json/test_stats.py index e5b49b92b..13c234b53 100644 --- a/tests/messages/json/test_stats.py +++ b/tests/messages/json/test_stats.py @@ -4,7 +4,7 @@ import pytest -from deebot_client.events import CleanJobStatus, ReportStatsEvent +from deebot_client.events import CleanJobStatus, FirmwareEvent, ReportStatsEvent from deebot_client.messages.json import ReportStats from tests.messages import assert_message @@ -60,4 +60,4 @@ def test_ReportStats(data: dict[str, Any], expected: ReportStatsEvent) -> None: "body": {"data": data}, } - assert_message(ReportStats, data, expected) + assert_message(ReportStats, data, (FirmwareEvent("1.8.2"), expected)) diff --git a/tests/test_device.py b/tests/test_device.py index f34be3de3..968df33ac 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -8,12 +8,16 @@ import pytest -from deebot_client.command import DeviceCommandResult +from deebot_client.command import Command, DeviceCommandResult from deebot_client.commands.json.battery import GetBattery +from deebot_client.commands.xml import GetBatteryInfo +from deebot_client.const import DataType from deebot_client.device import Device from deebot_client.events import AvailabilityEvent from deebot_client.events.network import NetworkInfoEvent from deebot_client.hardware import get_static_device_info +from deebot_client.messages.json import OnBattery +from deebot_client.messages.xml import BatteryInfo from deebot_client.models import DeviceInfo, StaticDeviceInfo from deebot_client.mqtt_client import MqttClient, SubscriberInfo from tests.helpers import mock_static_device_info @@ -21,11 +25,45 @@ if TYPE_CHECKING: from deebot_client.authentication import Authenticator + from deebot_client.message import Message from deebot_client.models import ApiDeviceInfo +def json_battery_message_payload(expected_version: str | None = "1.8.2") -> str: + header = { + "pri": 1, + "tzm": 480, + "ts": "1304637391896", + "ver": "0.0.1", + "hwVer": "0.1.1", + } + if expected_version: + header.update({"fwVer": expected_version}) + data = { + "header": header, + "body": {"data": {"value": 100, "isLow": 0}}, + } + return json.dumps(data) + + +def xml_battery_message_payload() -> str: + return '' + + +@pytest.mark.parametrize( + ("data_type", "get_battery_command", "battery_message", "battery_message_payload"), + [ + (DataType.JSON, GetBattery, OnBattery, json_battery_message_payload()), + (DataType.XML, GetBatteryInfo, BatteryInfo, xml_battery_message_payload()), + ], + ids=["json_bot", "xml_bot"], +) @patch("deebot_client.device._AVAILABLE_CHECK_INTERVAL", 2) # reduce interval async def test_available_check_and_teardown( + data_type: DataType, + get_battery_command: Command, + battery_message: Message, + battery_message_payload: str, authenticator: Authenticator, api_device_info: ApiDeviceInfo, ) -> None: @@ -40,10 +78,11 @@ async def assert_received_status(*, expected: bool) -> None: assert received_statuses.get_nowait().available is expected # prepare mocks - battery_mock = Mock(spec_set=GetBattery) + battery_mock = Mock(spec_set=get_battery_command) device_info = DeviceInfo( - api_device_info, mock_static_device_info({AvailabilityEvent: [battery_mock]}) + api_device_info, + mock_static_device_info({AvailabilityEvent: [battery_mock]}, data_type), ) execute_mock = battery_mock.execute @@ -88,19 +127,8 @@ async def assert_received_status(*, expected: bool) -> None: # Simulate message over mqtt and therefore available is not needed await asyncio.sleep(0.8) - data = { - "header": { - "pri": 1, - "tzm": 480, - "ts": "1304637391896", - "ver": "0.0.1", - "fwVer": "1.8.2", - "hwVer": "0.1.1", - }, - "body": {"data": {"value": 100, "isLow": 0}}, - } - sub_info.callback("onBattery", json.dumps(data)) + sub_info.callback(battery_message.NAME, battery_message_payload) await asyncio.sleep(1) # As the last message is not more than (interval-1) old, we skip the available check @@ -160,3 +188,92 @@ async def test_behaviour_with_no_map_capability( assert device.map is None await device.teardown() + + +@pytest.mark.parametrize( + ( + "data_type", + "get_battery_command", + "battery_message", + "battery_message_payload", + "expected_version", + ), + [ + ( + DataType.JSON, + GetBattery, + OnBattery, + json_battery_message_payload("1.8.2"), + "1.8.2", + ), + ( + DataType.JSON, + GetBattery, + OnBattery, + json_battery_message_payload(None), + None, + ), + (DataType.JSON, GetBattery, OnBattery, "{corrupted}", None), + (DataType.JSON, GetBattery, OnBattery, '["not an object"]', None), + ( + DataType.XML, + GetBatteryInfo, + BatteryInfo, + xml_battery_message_payload(), + None, + ), + ], + ids=[ + "json_bot", + "json_bot_no_version", + "json_bot_corrupted_json", + "json_bot_not_a_dict_json", + "xml_bot", + ], +) +@patch("deebot_client.device._AVAILABLE_CHECK_INTERVAL", 2) # reduce interval +async def test_device_handle_message_behaviour( + data_type: DataType, + get_battery_command: Command, + battery_message: Message, + battery_message_payload: str, + expected_version: str | None, + authenticator: Authenticator, + api_device_info: ApiDeviceInfo, +) -> None: + """Test the available check including if the status Event is fired correctly.""" + received_statuses: asyncio.Queue[AvailabilityEvent] = asyncio.Queue() + + async def on_status(event: AvailabilityEvent) -> None: + received_statuses.put_nowait(event) + + # prepare mocks + battery_mock = Mock(spec_set=get_battery_command) + + device_info = DeviceInfo( + api_device_info, + mock_static_device_info({AvailabilityEvent: [battery_mock]}, data_type), + ) + + # prepare bot and mock mqtt + bot = Device(device_info, authenticator) + mqtt_client = Mock(spec=MqttClient) + unsubscribe_mock = Mock(spec=Callable[[], None]) + mqtt_client.subscribe.return_value = unsubscribe_mock + await bot.initialize(mqtt_client) + + # deactivate refresh event subscribe refresh calls + bot.events._get_refresh_commands = lambda _: [] + + bot.events.subscribe(AvailabilityEvent, on_status) + + # verify mqtt was subscribed and available task was started + mqtt_client.subscribe.assert_called_once() + sub_info: SubscriberInfo = mqtt_client.subscribe.call_args.args[0] + sub_info.callback(battery_message.NAME, battery_message_payload) + await asyncio.sleep(1) + + assert bot.fw_version == expected_version + + # teardown bot + await bot.teardown() diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 000000000..4cdf47708 --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from unittest.mock import Mock + +import pytest + +from deebot_client.event_bus import EventBus +from deebot_client.message import ( + HandlingResult, + HandlingState, + Message, + MessagePayloadType, + MessageStr, +) + + +class WronglyImplementedMessage(Message): + """Mock class of a wrongly implemented message.""" + + NAME = "WronglyImplementedMessage" + + +class TestMessageStr(MessageStr): + """Mock class for MessageStr.""" + + NAME = "TestMessageStr" + + @classmethod + def _handle_str(cls, _event_bus: EventBus, message: str) -> HandlingResult: + assert isinstance(message, str) + return HandlingResult(HandlingState.SUCCESS, {"payload": message}) + + +@pytest.mark.parametrize( + ("input", "expected"), + [ + ("a string", "a string"), + (b"a byte string", "a byte string"), + (bytearray(b"a byte array string"), "a byte array string"), + ], + ids=["string", "byte string", "byte array"], +) +def test_MessageStr_should_convert_across_types( + input: MessagePayloadType, expected: str +) -> None: + event_bus = Mock(spec_set=EventBus) + result = TestMessageStr.handle(event_bus, input) + + assert result.state == HandlingState.SUCCESS + + assert result.args is not None + + converted = result.args.get("payload") + assert converted is not None + assert converted == expected + + +def test_MessageStr_should_error_on_unknown_types() -> None: + event_bus = Mock(spec_set=EventBus) + result = TestMessageStr.handle(event_bus, {"key": "value"}) + + assert result.state == HandlingState.ERROR + + +def test_WronglyImplementedMessage() -> None: + event_bus = Mock(spec_set=EventBus) + result = WronglyImplementedMessage.handle(event_bus, {}) + + assert result.state == HandlingState.ERROR