From 97d641ddec00980cd305525a9d38314d9fc2c79e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 15 Oct 2023 10:40:14 +0200 Subject: [PATCH] Add create_from_mqtt and make strict types for command init (#319) --- deebot_client/command.py | 44 +++++++++++++ deebot_client/commands/json/clean_count.py | 7 +- deebot_client/commands/json/common.py | 21 ++---- deebot_client/commands/json/fan_speed.py | 14 ++-- deebot_client/commands/json/life_span.py | 23 ++----- deebot_client/commands/json/volume.py | 12 ++-- deebot_client/commands/json/water_info.py | 20 ++---- deebot_client/mqtt_client.py | 4 +- tests/commands/json/__init__.py | 22 ++++++- tests/commands/json/test_advanced_mode.py | 5 +- tests/commands/json/test_carpet.py | 7 +- tests/commands/json/test_clean_preference.py | 7 +- .../commands/json/test_continuous_cleaning.py | 7 +- tests/commands/json/test_fan_speed.py | 16 ++--- tests/commands/json/test_life_span.py | 21 +++--- tests/commands/json/test_mulitmap_state.py | 5 +- tests/commands/json/test_true_detect.py | 5 +- tests/commands/json/test_volume.py | 18 ++++++ tests/commands/json/test_water_info.py | 25 ++------ tests/test_command.py | 64 +++++++++++++++++++ tests/test_mqtt_client.py | 12 ++-- 21 files changed, 225 insertions(+), 134 deletions(-) create mode 100644 tests/commands/json/test_volume.py create mode 100644 tests/test_command.py diff --git a/deebot_client/command.py b/deebot_client/command.py index d3a075c5..3458b865 100644 --- a/deebot_client/command.py +++ b/deebot_client/command.py @@ -4,6 +4,8 @@ from dataclasses import dataclass, field from typing import Any, final +from deebot_client.exceptions import DeebotError + from .authentication import Authenticator from .const import PATH_API_IOT_DEVMANAGER, REQUEST_HEADERS, DataType from .events.event_bus import EventBus @@ -186,9 +188,51 @@ def __hash__(self) -> int: return hash(self.name) + hash(self._args) +@dataclass +class InitParam: + """Init param.""" + + type_: type + name: str | None = None + + class CommandMqttP2P(Command, ABC): """Command which can handle mqtt p2p messages.""" + _mqtt_params: dict[str, InitParam | None] + @abstractmethod def handle_mqtt_p2p(self, event_bus: EventBus, response: dict[str, Any]) -> None: """Handle response received over the mqtt channel "p2p".""" + + @classmethod + def create_from_mqtt(cls, data: dict[str, Any]) -> "CommandMqttP2P": + """Create a command from the mqtt data.""" + values: dict[str, Any] = {} + if not hasattr(cls, "_mqtt_params"): + raise DeebotError("_mqtt_params not set") + + for name, param in cls._mqtt_params.items(): + if param is None: + # Remove field + data.pop(name, None) + else: + values[param.name or name] = _pop_or_raise(name, param.type_, data) + + if data: + _LOGGER.debug("Following data will be ignored: %s", data) + + return cls(**values) + + +def _pop_or_raise(name: str, type_: type, data: dict[str, Any]) -> Any: + try: + value = data.pop(name) + except KeyError as err: + raise DeebotError(f'"{name}" is missing in {data}') from err + try: + return type_(value) + except ValueError as err: + raise DeebotError( + f'Could not convert "{value}" of {name} into {type_}' + ) from err diff --git a/deebot_client/commands/json/clean_count.py b/deebot_client/commands/json/clean_count.py index d57ce90f..f44700bd 100644 --- a/deebot_client/commands/json/clean_count.py +++ b/deebot_client/commands/json/clean_count.py @@ -1,8 +1,8 @@ """Clean count command module.""" -from collections.abc import Mapping from typing import Any +from deebot_client.command import InitParam from deebot_client.events import CleanCountEvent from deebot_client.message import HandlingResult, MessageBodyDataDict @@ -32,6 +32,7 @@ class SetCleanCount(SetCommand): name = "setCleanCount" get_command = GetCleanCount + _mqtt_params = {"count": InitParam(int)} - def __init__(self, count: int, **kwargs: Mapping[str, Any]) -> None: - super().__init__({"count": count}, **kwargs) + def __init__(self, count: int) -> None: + super().__init__({"count": count}) diff --git a/deebot_client/commands/json/common.py b/deebot_client/commands/json/common.py index 1dd5acab..fbf6e6f0 100644 --- a/deebot_client/commands/json/common.py +++ b/deebot_client/commands/json/common.py @@ -1,10 +1,9 @@ """Base commands.""" from abc import ABC, abstractmethod -from collections.abc import Mapping from datetime import datetime from typing import Any -from deebot_client.command import Command, CommandMqttP2P, CommandResult +from deebot_client.command import Command, CommandMqttP2P, CommandResult, InitParam from deebot_client.const import DataType from deebot_client.events import AvailabilityEvent, EnableEvent from deebot_client.events.event_bus import EventBus @@ -108,16 +107,6 @@ class SetCommand(ExecuteCommand, CommandMqttP2P, ABC): Command needs to be linked to the "get" command, for handling (updating) the sensors. """ - def __init__( - self, - args: dict | list | None, - **kwargs: Mapping[str, Any], - ) -> None: - if kwargs: - _LOGGER.debug("Following passed parameters will be ignored: %s", kwargs) - - super().__init__(args) - @property @abstractmethod def get_command(self) -> type[CommandWithMessageHandling]: @@ -156,7 +145,7 @@ def _handle_body_data_dict( class SetEnableCommand(SetCommand, ABC): """Abstract set enable command.""" - def __init__(self, enable: int | bool, **kwargs: Mapping[str, Any]) -> None: - if isinstance(enable, bool): - enable = 1 if enable else 0 - super().__init__({"enable": enable}, **kwargs) + _mqtt_params = {"enable": InitParam(bool)} + + def __init__(self, enable: bool) -> None: + super().__init__({"enable": 1 if enable else 0}) diff --git a/deebot_client/commands/json/fan_speed.py b/deebot_client/commands/json/fan_speed.py index 1bfab1f9..6f874033 100644 --- a/deebot_client/commands/json/fan_speed.py +++ b/deebot_client/commands/json/fan_speed.py @@ -1,7 +1,7 @@ """(fan) speed commands.""" -from collections.abc import Mapping from typing import Any +from deebot_client.command import InitParam from deebot_client.events import FanSpeedEvent, FanSpeedLevel from deebot_client.message import HandlingResult, MessageBodyDataDict @@ -30,13 +30,7 @@ class SetFanSpeed(SetCommand): name = "setSpeed" get_command = GetFanSpeed + _mqtt_params = {"speed": InitParam(FanSpeedLevel)} - def __init__( - self, speed: str | int | FanSpeedLevel, **kwargs: Mapping[str, Any] - ) -> None: - if isinstance(speed, str): - speed = FanSpeedLevel.get(speed) - if isinstance(speed, FanSpeedLevel): - speed = speed.value - - super().__init__({"speed": speed}, **kwargs) + def __init__(self, speed: FanSpeedLevel) -> None: + super().__init__({"speed": speed.value}) diff --git a/deebot_client/commands/json/life_span.py b/deebot_client/commands/json/life_span.py index 98702e47..86dc0c13 100644 --- a/deebot_client/commands/json/life_span.py +++ b/deebot_client/commands/json/life_span.py @@ -1,33 +1,21 @@ """Life span commands.""" from typing import Any -from deebot_client.command import CommandMqttP2P +from deebot_client.command import CommandMqttP2P, InitParam from deebot_client.events import LifeSpan, LifeSpanEvent from deebot_client.message import HandlingResult, HandlingState, MessageBodyDataList from deebot_client.util import LST from .common import CommandWithMessageHandling, EventBus, ExecuteCommand -LifeSpanType = LifeSpan | str - - -def _get_str(_type: LifeSpanType) -> str: - if isinstance(_type, LifeSpan): - return _type.value - - return _type - class GetLifeSpan(CommandWithMessageHandling, MessageBodyDataList): """Get life span command.""" name = "getLifeSpan" - def __init__(self, _types: LifeSpanType | LST[LifeSpanType]) -> None: - if isinstance(_types, LifeSpanType): # type: ignore[misc, arg-type] - _types = set(_types) - - args = [_get_str(life_span) for life_span in _types] + def __init__(self, life_spans: LST[LifeSpan]) -> None: + args = [life_span.value for life_span in life_spans] super().__init__(args) @classmethod @@ -54,9 +42,10 @@ class ResetLifeSpan(ExecuteCommand, CommandMqttP2P): """Reset life span command.""" name = "resetLifeSpan" + _mqtt_params = {"type": InitParam(LifeSpan, "life_span")} - def __init__(self, type: LifeSpanType) -> None: # pylint: disable=redefined-builtin - super().__init__({"type": _get_str(type)}) + def __init__(self, life_span: LifeSpan) -> None: + super().__init__({"type": life_span.value}) def handle_mqtt_p2p(self, event_bus: EventBus, response: dict[str, Any]) -> None: """Handle response received over the mqtt channel "p2p".""" diff --git a/deebot_client/commands/json/volume.py b/deebot_client/commands/json/volume.py index 6a7eb81f..843348cb 100644 --- a/deebot_client/commands/json/volume.py +++ b/deebot_client/commands/json/volume.py @@ -1,8 +1,8 @@ """Volume command module.""" -from collections.abc import Mapping from typing import Any +from deebot_client.command import InitParam from deebot_client.events import VolumeEvent from deebot_client.message import HandlingResult, MessageBodyDataDict @@ -34,8 +34,10 @@ class SetVolume(SetCommand): name = "setVolume" get_command = GetVolume + _mqtt_params = { + "volume": InitParam(int), + "total": None, # Remove it as we don't can set it (App includes it) + } - def __init__(self, volume: int, **kwargs: Mapping[str, Any]) -> None: - # removing "total" as we don't can set it (App includes it) - kwargs.pop("total", None) - super().__init__({"volume": volume}, **kwargs) + def __init__(self, volume: int) -> None: + super().__init__({"volume": volume}) diff --git a/deebot_client/commands/json/water_info.py b/deebot_client/commands/json/water_info.py index 4ceed610..af3422e4 100644 --- a/deebot_client/commands/json/water_info.py +++ b/deebot_client/commands/json/water_info.py @@ -1,7 +1,7 @@ """Water info commands.""" -from collections.abc import Mapping from typing import Any +from deebot_client.command import InitParam from deebot_client.events import WaterAmount, WaterInfoEvent from deebot_client.message import HandlingResult, MessageBodyDataDict @@ -34,16 +34,10 @@ class SetWaterInfo(SetCommand): name = "setWaterInfo" get_command = GetWaterInfo + _mqtt_params = { + "amount": InitParam(WaterAmount), + "enable": None, # Remove it as we don't can set it (App includes it) + } - def __init__( - self, amount: str | int | WaterAmount, **kwargs: Mapping[str, Any] - ) -> None: - # removing "enable" as we don't can set it - kwargs.pop("enable", None) - - if isinstance(amount, str): - amount = WaterAmount.get(amount) - if isinstance(amount, WaterAmount): - amount = amount.value - - super().__init__({"amount": amount}, **kwargs) + def __init__(self, amount: WaterAmount) -> None: + super().__init__({"amount": amount.value}) diff --git a/deebot_client/mqtt_client.py b/deebot_client/mqtt_client.py index bfd44485..8089fe48 100644 --- a/deebot_client/mqtt_client.py +++ b/deebot_client/mqtt_client.py @@ -289,7 +289,9 @@ def _handle_p2p( ) return - self._received_p2p_commands[request_id] = command_type(**data) + self._received_p2p_commands[request_id] = command_type.create_from_mqtt( + data + ) else: if command := self._received_p2p_commands.pop(request_id, None): if sub_info := self._subscribtions.get(topic_split[3]): diff --git a/tests/commands/json/__init__.py b/tests/commands/json/__init__.py index 80cded3d..fa5232b9 100644 --- a/tests/commands/json/__init__.py +++ b/tests/commands/json/__init__.py @@ -6,8 +6,12 @@ from deebot_client.authentication import Authenticator from deebot_client.command import Command -from deebot_client.commands.json.common import ExecuteCommand, SetCommand -from deebot_client.events import Event +from deebot_client.commands.json.common import ( + ExecuteCommand, + SetCommand, + SetEnableCommand, +) +from deebot_client.events import EnableEvent, Event from deebot_client.events.event_bus import EventBus from deebot_client.models import Credentials, DeviceInfo from tests.helpers import get_message_json, get_request_json, get_success_body @@ -78,7 +82,7 @@ async def assert_execute_command( async def assert_set_command( command: SetCommand, - args: dict | list | None, + args: dict, expected_get_command_event: Event, ) -> None: await assert_execute_command(command, args) @@ -98,3 +102,15 @@ async def assert_set_command( # Success command.handle_mqtt_p2p(event_bus, get_message_json(get_success_body())) event_bus.notify.assert_called_once_with(expected_get_command_event) + + mqtt_command = command.create_from_mqtt(args) + assert mqtt_command == command + + +async def assert_set_enable_command( + command: SetEnableCommand, + enabled: bool, + expected_get_command_event: type[EnableEvent], +) -> None: + args = {"enable": 1 if enabled else 0} + await assert_set_command(command, args, expected_get_command_event(enabled)) diff --git a/tests/commands/json/test_advanced_mode.py b/tests/commands/json/test_advanced_mode.py index 529a24bd..454ee96e 100644 --- a/tests/commands/json/test_advanced_mode.py +++ b/tests/commands/json/test_advanced_mode.py @@ -4,7 +4,7 @@ from deebot_client.events import AdvancedModeEvent from tests.helpers import get_request_json, get_success_body -from . import assert_command, assert_set_command +from . import assert_command, assert_set_enable_command @pytest.mark.parametrize("value", [False, True]) @@ -15,5 +15,4 @@ async def test_GetAdvancedMode(value: bool) -> None: @pytest.mark.parametrize("value", [False, True]) async def test_SetAdvancedMode(value: bool) -> None: - args = {"enable": 1 if value else 0} - await assert_set_command(SetAdvancedMode(value), args, AdvancedModeEvent(value)) + await assert_set_enable_command(SetAdvancedMode(value), value, AdvancedModeEvent) diff --git a/tests/commands/json/test_carpet.py b/tests/commands/json/test_carpet.py index 4229a6fe..0dea13e0 100644 --- a/tests/commands/json/test_carpet.py +++ b/tests/commands/json/test_carpet.py @@ -4,7 +4,7 @@ from deebot_client.events import CarpetAutoFanBoostEvent from tests.helpers import get_request_json, get_success_body -from . import assert_command, assert_set_command +from . import assert_command, assert_set_enable_command @pytest.mark.parametrize("value", [False, True]) @@ -15,7 +15,6 @@ async def test_GetCarpetAutoFanBoost(value: bool) -> None: @pytest.mark.parametrize("value", [False, True]) async def test_SetCarpetAutoFanBoost(value: bool) -> None: - args = {"enable": 1 if value else 0} - await assert_set_command( - SetCarpetAutoFanBoost(value), args, CarpetAutoFanBoostEvent(value) + await assert_set_enable_command( + SetCarpetAutoFanBoost(value), value, CarpetAutoFanBoostEvent ) diff --git a/tests/commands/json/test_clean_preference.py b/tests/commands/json/test_clean_preference.py index 51946049..9ba26b8e 100644 --- a/tests/commands/json/test_clean_preference.py +++ b/tests/commands/json/test_clean_preference.py @@ -4,7 +4,7 @@ from deebot_client.events import CleanPreferenceEvent from tests.helpers import get_request_json, get_success_body -from . import assert_command, assert_set_command +from . import assert_command, assert_set_enable_command @pytest.mark.parametrize("value", [False, True]) @@ -15,7 +15,6 @@ async def test_GetCleanPreference(value: bool) -> None: @pytest.mark.parametrize("value", [False, True]) async def test_SetCleanPreference(value: bool) -> None: - args = {"enable": 1 if value else 0} - await assert_set_command( - SetCleanPreference(value), args, CleanPreferenceEvent(value) + await assert_set_enable_command( + SetCleanPreference(value), value, CleanPreferenceEvent ) diff --git a/tests/commands/json/test_continuous_cleaning.py b/tests/commands/json/test_continuous_cleaning.py index 1c596401..9f8e205a 100644 --- a/tests/commands/json/test_continuous_cleaning.py +++ b/tests/commands/json/test_continuous_cleaning.py @@ -4,7 +4,7 @@ from deebot_client.events import ContinuousCleaningEvent from tests.helpers import get_request_json, get_success_body -from . import assert_command, assert_set_command +from . import assert_command, assert_set_enable_command @pytest.mark.parametrize("value", [False, True]) @@ -15,7 +15,6 @@ async def test_GetContinuousCleaning(value: bool) -> None: @pytest.mark.parametrize("value", [False, True]) async def test_SetContinuousCleaning(value: bool) -> None: - args = {"enable": 1 if value else 0} - await assert_set_command( - SetContinuousCleaning(value), args, ContinuousCleaningEvent(value) + await assert_set_enable_command( + SetContinuousCleaning(value), value, ContinuousCleaningEvent ) diff --git a/tests/commands/json/test_fan_speed.py b/tests/commands/json/test_fan_speed.py index d741dd1f..e211d96c 100644 --- a/tests/commands/json/test_fan_speed.py +++ b/tests/commands/json/test_fan_speed.py @@ -1,5 +1,3 @@ -import pytest - from deebot_client.commands.json import GetFanSpeed, SetFanSpeed from deebot_client.events import FanSpeedEvent from deebot_client.events.fan_speed import FanSpeedLevel @@ -9,7 +7,7 @@ verify_DisplayNameEnum_unique, ) -from . import assert_command +from . import assert_command, assert_set_command def test_FanSpeedLevel_unique() -> None: @@ -21,11 +19,7 @@ async def test_GetFanSpeed() -> None: await assert_command(GetFanSpeed(), json, FanSpeedEvent(FanSpeedLevel.MAX_PLUS)) -@pytest.mark.parametrize( - "value, expected", - [("quiet", 1000), ("max_plus", 2), (0, 0), (FanSpeedLevel.MAX, 1)], -) -def test_SetFanSpeed(value: str | int | FanSpeedLevel, expected: int) -> None: - command = SetFanSpeed(value) - assert command.name == "setSpeed" - assert command._args == {"speed": expected} +async def test_SetFanSpeed() -> None: + command = SetFanSpeed(FanSpeedLevel.MAX) + args = {"speed": 1} + await assert_set_command(command, args, FanSpeedEvent(FanSpeedLevel.MAX)) diff --git a/tests/commands/json/test_life_span.py b/tests/commands/json/test_life_span.py index de596812..d06ce391 100644 --- a/tests/commands/json/test_life_span.py +++ b/tests/commands/json/test_life_span.py @@ -3,7 +3,7 @@ import pytest from deebot_client.commands.json import GetLifeSpan -from deebot_client.commands.json.life_span import LifeSpanType, ResetLifeSpan +from deebot_client.commands.json.life_span import ResetLifeSpan from deebot_client.events import LifeSpan, LifeSpanEvent from tests.helpers import get_request_json, get_success_body @@ -14,7 +14,7 @@ "command, json, expected", [ ( - GetLifeSpan({"brush", LifeSpan.FILTER, LifeSpan.SIDE_BRUSH}), + GetLifeSpan({LifeSpan.BRUSH, LifeSpan.FILTER, LifeSpan.SIDE_BRUSH}), get_request_json( get_success_body( [ @@ -31,14 +31,14 @@ ], ), ( - GetLifeSpan(LifeSpan.FILTER), + GetLifeSpan([LifeSpan.FILTER]), get_request_json( get_success_body([{"type": "heap", "left": 7179, "total": 7200}]) ), [LifeSpanEvent(LifeSpan.FILTER, 99.71, 7179)], ), ( - GetLifeSpan("brush"), + GetLifeSpan({LifeSpan.BRUSH}), get_request_json( get_success_body([{"type": "brush", "left": 17979, "total": 18000}]) ), @@ -53,11 +53,14 @@ async def test_GetLifeSpan( @pytest.mark.parametrize( - "_type, args", + "command, args", [ - (LifeSpan.FILTER, {"type": LifeSpan.FILTER.value}), - ("brush", {"type": LifeSpan.BRUSH.value}), + (ResetLifeSpan(LifeSpan.FILTER), {"type": LifeSpan.FILTER.value}), + ( + ResetLifeSpan.create_from_mqtt({"type": "brush"}), + {"type": LifeSpan.BRUSH.value}, + ), ], ) -async def test_ResetLifeSpan(_type: LifeSpanType, args: dict[str, str]) -> None: - await assert_execute_command(ResetLifeSpan(_type), args) +async def test_ResetLifeSpan(command: ResetLifeSpan, args: dict[str, str]) -> None: + await assert_execute_command(command, args) diff --git a/tests/commands/json/test_mulitmap_state.py b/tests/commands/json/test_mulitmap_state.py index 10b19cd2..d6705ec3 100644 --- a/tests/commands/json/test_mulitmap_state.py +++ b/tests/commands/json/test_mulitmap_state.py @@ -4,7 +4,7 @@ from deebot_client.events import MultimapStateEvent from tests.helpers import get_request_json, get_success_body -from . import assert_command, assert_set_command +from . import assert_command, assert_set_enable_command @pytest.mark.parametrize("value", [False, True]) @@ -15,5 +15,4 @@ async def test_GetMultimapState(value: bool) -> None: @pytest.mark.parametrize("value", [False, True]) async def test_SetMultimapState(value: bool) -> None: - args = {"enable": 1 if value else 0} - await assert_set_command(SetMultimapState(value), args, MultimapStateEvent(value)) + await assert_set_enable_command(SetMultimapState(value), value, MultimapStateEvent) diff --git a/tests/commands/json/test_true_detect.py b/tests/commands/json/test_true_detect.py index f0ec7ab7..0a33fb21 100644 --- a/tests/commands/json/test_true_detect.py +++ b/tests/commands/json/test_true_detect.py @@ -4,7 +4,7 @@ from deebot_client.events import TrueDetectEvent from tests.helpers import get_request_json, get_success_body -from . import assert_command, assert_set_command +from . import assert_command, assert_set_enable_command @pytest.mark.parametrize("value", [False, True]) @@ -15,5 +15,4 @@ async def test_GetTrueDetect(value: bool) -> None: @pytest.mark.parametrize("value", [False, True]) async def test_SetTrueDetect(value: bool) -> None: - args = {"enable": 1 if value else 0} - await assert_set_command(SetTrueDetect(value), args, TrueDetectEvent(value)) + await assert_set_enable_command(SetTrueDetect(value), value, TrueDetectEvent) diff --git a/tests/commands/json/test_volume.py b/tests/commands/json/test_volume.py new file mode 100644 index 00000000..419e3a16 --- /dev/null +++ b/tests/commands/json/test_volume.py @@ -0,0 +1,18 @@ +import pytest + +from deebot_client.commands.json import GetVolume, SetVolume +from deebot_client.events import VolumeEvent +from tests.helpers import get_request_json, get_success_body + +from . import assert_command, assert_set_command + + +async def test_GetVolume() -> None: + json = get_request_json(get_success_body({"volume": 2, "total": 10})) + await assert_command(GetVolume(), json, VolumeEvent(2, 10)) + + +@pytest.mark.parametrize("level", [0, 2, 10]) +async def test_SetCleanCount(level: int) -> None: + args = {"volume": level} + await assert_set_command(SetVolume(level), args, VolumeEvent(level, None)) diff --git a/tests/commands/json/test_water_info.py b/tests/commands/json/test_water_info.py index 17d75800..a228a913 100644 --- a/tests/commands/json/test_water_info.py +++ b/tests/commands/json/test_water_info.py @@ -30,24 +30,7 @@ async def test_GetWaterInfo(json: dict[str, Any], expected: WaterInfoEvent) -> N await assert_command(GetWaterInfo(), json, expected) -@pytest.mark.parametrize( - "value, exptected_args_amount, expected", - [ - ("low", 1, WaterInfoEvent(None, WaterAmount.LOW)), - (WaterAmount.MEDIUM, 2, WaterInfoEvent(None, WaterAmount.MEDIUM)), - ({"amount": 3, "enable": 1}, 3, WaterInfoEvent(None, WaterAmount.HIGH)), - (4, 4, WaterInfoEvent(None, WaterAmount.ULTRAHIGH)), - ], -) -async def test_SetWaterInfo( - value: str | int | WaterAmount | dict, - exptected_args_amount: int, - expected: WaterInfoEvent, -) -> None: - if isinstance(value, dict): - command = SetWaterInfo(**value) - else: - command = SetWaterInfo(value) - - args = {"amount": exptected_args_amount} - await assert_set_command(command, args, expected) +async def test_SetWaterInfo() -> None: + command = SetWaterInfo(WaterAmount.MEDIUM) + args = {"amount": 2} + await assert_set_command(command, args, WaterInfoEvent(None, WaterAmount.MEDIUM)) diff --git a/tests/test_command.py b/tests/test_command.py new file mode 100644 index 00000000..3bd914de --- /dev/null +++ b/tests/test_command.py @@ -0,0 +1,64 @@ +from typing import Any + +import pytest +from testfixtures import LogCapture + +from deebot_client.command import CommandMqttP2P, CommandResult, InitParam +from deebot_client.const import DataType +from deebot_client.events.event_bus import EventBus +from deebot_client.exceptions import DeebotError + + +class _TestCommand(CommandMqttP2P): + name = "TestCommand" + data_type = DataType.JSON + _mqtt_params = {"field": InitParam(int), "remove": None} + + def __init__(self, field: int) -> None: + pass + + def handle_mqtt_p2p(self, event_bus: EventBus, response: dict[str, Any]) -> None: + pass + + def _get_payload(self) -> dict[str, Any] | list | str: + return {} + + def _handle_response( + self, event_bus: EventBus, response: dict[str, Any] + ) -> CommandResult: + return CommandResult.analyse() + + +def test_CommandMqttP2P_no_mqtt_params() -> None: + class TestCommandNoParams(CommandMqttP2P): + pass + + with pytest.raises(DeebotError, match=r"_mqtt_params not set"): + TestCommandNoParams.create_from_mqtt({}) + + +@pytest.mark.parametrize( + "data, expected", + [ + ({"field": "a"}, r"""Could not convert "a" of field into """), + ({"something": "a"}, r'"field" is missing in {\'something\': \'a\'}'), + ], +) +def test_CommandMqttP2P_create_from_mqtt_error( + data: dict[str, Any], expected: str +) -> None: + with pytest.raises(DeebotError, match=expected): + _TestCommand.create_from_mqtt(data) + + +def test_CommandMqttP2P_create_from_mqtt_additional_fields() -> None: + with LogCapture() as log: + _TestCommand.create_from_mqtt({"field": 0, "remove": "bla", "additional": 1}) + + log.check_present( + ( + "deebot_client.command", + "DEBUG", + "Following data will be ignored: {'additional': 1}", + ) + ) diff --git a/tests/test_mqtt_client.py b/tests/test_mqtt_client.py index 1c5c40b5..3bb1fdf0 100644 --- a/tests/test_mqtt_client.py +++ b/tests/test_mqtt_client.py @@ -239,7 +239,9 @@ async def test_p2p_success( command_object = Mock(spec=SetVolume) command_name = SetVolume.name - command_type = Mock(spec=SetVolume, return_value=command_object) + command_type = Mock(spec=SetVolume) + create_from_mqtt = command_type.create_from_mqtt + create_from_mqtt.return_value = command_object with patch.dict( "deebot_client.mqtt_client.COMMANDS_WITH_MQTT_P2P_HANDLING", {DataType.JSON: {command_name: command_type}}, @@ -250,7 +252,7 @@ async def test_p2p_success( command_name, device_info, data, True, request_id, test_mqtt_client ) - command_type.assert_called_with(**(data["body"]["data"])) + create_from_mqtt.assert_called_with(data["body"]["data"]) assert len(mqtt_client._received_p2p_commands) == 1 assert mqtt_client._received_p2p_commands[request_id] == command_object @@ -329,7 +331,9 @@ async def test_p2p_to_late( command_object = Mock(spec=SetVolume) command_name = SetVolume.name - command_type = Mock(spec=SetVolume, return_value=command_object) + command_type = Mock(spec=SetVolume) + create_from_mqtt = command_type.create_from_mqtt + create_from_mqtt.return_value = command_object with patch.dict( "deebot_client.mqtt_client.COMMANDS_WITH_MQTT_P2P_HANDLING", {DataType.JSON: {command_name: command_type}}, @@ -340,7 +344,7 @@ async def test_p2p_to_late( command_name, device_info, data, True, request_id, test_mqtt_client ) - command_type.assert_called_with(**(data["body"]["data"])) + create_from_mqtt.assert_called_with(data["body"]["data"]) assert len(mqtt_client._received_p2p_commands) == 1 assert mqtt_client._received_p2p_commands[request_id] == command_object