Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new command: "AutoEmpty" #349

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions deebot_client/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
WorkMode,
WorkModeEvent,
)
from deebot_client.events.auto_empty import AutoEmptyMode, AutoEmptyModeEvent
from deebot_client.events.efficiency_mode import EfficiencyMode, EfficiencyModeEvent
from deebot_client.models import CleanAction, CleanMode

Expand Down Expand Up @@ -107,6 +108,15 @@ class CapabilitySetTypes(CapabilitySet[_EVENT, _T | str], CapabilityTypes[_T]):
"""Capability for set command and types."""


@dataclass(frozen=True, kw_only=True)
class CapabilityCleanAutoEmpty(
CapabilityEvent[AutoEmptyModeEvent], CapabilityTypes[AutoEmptyMode]
):
"""Capabilities for clean auto empty."""

set: Callable[[bool, AutoEmptyMode | str | None], SetCommand]


@dataclass(frozen=True, kw_only=True)
class CapabilityCleanAction:
"""Capabilities for clean action."""
Expand All @@ -119,6 +129,7 @@ class CapabilityCleanAction:
class CapabilityClean:
"""Capabilities for clean."""

auto_empty: CapabilityCleanAutoEmpty | None = None
action: CapabilityCleanAction
continuous: CapabilitySetEnable[ContinuousCleaningEvent]
count: CapabilitySet[CleanCountEvent, int] | None = None
Expand Down
6 changes: 6 additions & 0 deletions deebot_client/commands/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from deebot_client.command import Command, CommandMqttP2P

from .advanced_mode import GetAdvancedMode, SetAdvancedMode
from .auto_empty import GetAutoEmpty, SetAutoEmpty
from .battery import GetBattery
from .carpet import GetCarpetAutoFanBoost, SetCarpetAutoFanBoost
from .charge import Charge
Expand Down Expand Up @@ -39,6 +40,8 @@
__all__ = [
"GetAdvancedMode",
"SetAdvancedMode",
"GetAutoEmpty",
"SetAutoEmpty",
"GetBattery",
"GetCarpetAutoFanBoost",
"SetCarpetAutoFanBoost",
Expand Down Expand Up @@ -93,6 +96,9 @@
GetAdvancedMode,
SetAdvancedMode,

GetAutoEmpty,
SetAutoEmpty,

GetBattery,

GetCarpetAutoFanBoost,
Expand Down
55 changes: 55 additions & 0 deletions deebot_client/commands/json/auto_empty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Auto empty command module."""
from types import MappingProxyType
from typing import Any

from deebot_client.command import InitParam
from deebot_client.event_bus import EventBus
from deebot_client.events import AutoEmptyMode, AutoEmptyModeEvent
from deebot_client.message import HandlingResult, MessageBodyDataDict

from .common import JsonCommandWithMessageHandling, JsonSetCommand


class GetAutoEmpty(JsonCommandWithMessageHandling, MessageBodyDataDict):
"""Get auto empty command."""

name = "getAutoEmpty"

@classmethod
def _handle_body_data_dict(
cls, event_bus: EventBus, data: dict[str, Any]
) -> HandlingResult:
"""Handle message->body->data and notify the correct event subscribers.

:return: A message response
"""
event_bus.notify(
AutoEmptyModeEvent(
enable=bool(data["enable"]),
mode=AutoEmptyMode(str(data["frequency"])),
)
)
return HandlingResult.success()


class SetAutoEmpty(JsonSetCommand):
"""Set auto empty command."""

name = "setAutoEmpty"
get_command = GetAutoEmpty
_mqtt_params = MappingProxyType(
{"enable": InitParam(bool), "frequency": InitParam(AutoEmptyMode)}
)

def __init__(
self,
enable: bool = True, # noqa: FBT001, FBT002
frequency: AutoEmptyMode | str | None = None,
) -> None:
args: dict[str, int | str] = {"enable": int(enable)}
if frequency is not None:
if isinstance(frequency, str):
frequency = AutoEmptyMode.get(frequency)
args["frequency"] = frequency.value

super().__init__(args)
3 changes: 3 additions & 0 deletions deebot_client/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from deebot_client.models import Room, State
from deebot_client.util import DisplayNameIntEnum

from .auto_empty import AutoEmptyMode, AutoEmptyModeEvent
from .efficiency_mode import EfficiencyMode, EfficiencyModeEvent
from .fan_speed import FanSpeedEvent, FanSpeedLevel
from .map import (
Expand All @@ -28,6 +29,8 @@
from .work_mode import WorkMode, WorkModeEvent

__all__ = [
"AutoEmptyMode",
"AutoEmptyModeEvent",
"BatteryEvent",
"CachedMapInfoEvent",
"CleanJobStatus",
Expand Down
24 changes: 24 additions & 0 deletions deebot_client/events/auto_empty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Auto empty event module."""
from dataclasses import dataclass

from deebot_client.util import DisplayNameStrEnum

from .base import Event


class AutoEmptyMode(DisplayNameStrEnum):
"""Enum class for all possible auto emptys."""

MODE_10 = "10"
MODE_15 = "15"
MODE_25 = "25"
MODE_AUTO = "auto"
MODE_SMART = "smart"


@dataclass(frozen=True)
class AutoEmptyModeEvent(Event):
"""Auto empty event representation."""

enable: bool
mode: AutoEmptyMode
14 changes: 14 additions & 0 deletions deebot_client/hardware/deebot/p95mgv.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Capabilities,
CapabilityClean,
CapabilityCleanAction,
CapabilityCleanAutoEmpty,
CapabilityCustomCommand,
CapabilityEvent,
CapabilityExecute,
Expand All @@ -15,6 +16,7 @@
CapabilityStats,
)
from deebot_client.commands.json.advanced_mode import GetAdvancedMode, SetAdvancedMode
from deebot_client.commands.json.auto_empty import GetAutoEmpty, SetAutoEmpty
from deebot_client.commands.json.battery import GetBattery
from deebot_client.commands.json.carpet import (
GetCarpetAutoFanBoost,
Expand Down Expand Up @@ -94,6 +96,7 @@
WaterAmount,
WaterInfoEvent,
)
from deebot_client.events.auto_empty import AutoEmptyMode, AutoEmptyModeEvent
from deebot_client.events.efficiency_mode import EfficiencyMode
from deebot_client.models import StaticDeviceInfo
from deebot_client.util import short_name
Expand All @@ -111,6 +114,17 @@
battery=CapabilityEvent(BatteryEvent, [GetBattery()]),
charge=CapabilityExecute(Charge),
clean=CapabilityClean(
auto_empty=CapabilityCleanAutoEmpty(
event=AutoEmptyModeEvent,
get=[GetAutoEmpty()],
set=SetAutoEmpty,
types=(
AutoEmptyMode.MODE_10,
AutoEmptyMode.MODE_15,
AutoEmptyMode.MODE_25,
AutoEmptyMode.MODE_AUTO,
),
),
action=CapabilityCleanAction(command=Clean, area=CleanArea),
continuous=CapabilitySetEnable(
ContinuousCleaningEvent,
Expand Down
51 changes: 50 additions & 1 deletion deebot_client/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import asyncio
from contextlib import suppress
from enum import IntEnum, unique
from enum import Enum, IntEnum, unique
import hashlib
from typing import TYPE_CHECKING, Any, Self, TypeVar

Expand Down Expand Up @@ -88,6 +88,55 @@
return hash(self._value_)


@unique
class DisplayNameStrEnum(Enum):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you adding this enum back?

Copy link
Contributor Author

@MVladislav MVladislav Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because inside the event auto_empty.py in class AutoEmptyMod i used instead of integer enums - string enums. For that I copy the existing int enum definition to have it with same functions for string enum.

"""Int enum with a property "display_name"."""

def __new__(cls, *args: str, **_: Mapping[Any, Any]) -> Self:
"""Create new DisplayNameIntEnum."""
obj = object.__new__(cls)
obj._value_ = args[0]
return obj

def __init__(self, value: str, display_name: str | None = None) -> None:
super().__init__()
self._value_ = value
self._display_name = display_name

@property
def display_name(self) -> str:
"""Return the custom display name or the lowered name property."""
if self._display_name:
return self._display_name

Check warning on line 110 in deebot_client/util.py

View check run for this annotation

Codecov / codecov/patch

deebot_client/util.py#L110

Added line #L110 was not covered by tests

return self.name.lower()

@classmethod
def get(cls, value: str) -> Self:
"""Get enum member from name or display_name."""
value = value.upper()
if value in cls.__members__:
return cls[value]

for member in cls:
if value == member.display_name.upper():
return member

Check warning on line 123 in deebot_client/util.py

View check run for this annotation

Codecov / codecov/patch

deebot_client/util.py#L123

Added line #L123 was not covered by tests

msg = f"'{value}' is not a valid {cls.__name__} member"
raise ValueError(msg)

Check warning on line 126 in deebot_client/util.py

View check run for this annotation

Codecov / codecov/patch

deebot_client/util.py#L125-L126

Added lines #L125 - L126 were not covered by tests

def __eq__(self, x: object) -> bool:
if not isinstance(x, type(self)):
return False
return bool(self._value_ == x._value_)

Check warning on line 131 in deebot_client/util.py

View check run for this annotation

Codecov / codecov/patch

deebot_client/util.py#L130-L131

Added lines #L130 - L131 were not covered by tests

def __ne__(self, x: object) -> bool:
return not self.__eq__(x)

Check warning on line 134 in deebot_client/util.py

View check run for this annotation

Codecov / codecov/patch

deebot_client/util.py#L134

Added line #L134 was not covered by tests

def __hash__(self) -> int:
return hash(self._value_)

Check warning on line 137 in deebot_client/util.py

View check run for this annotation

Codecov / codecov/patch

deebot_client/util.py#L137

Added line #L137 was not covered by tests


class OnChangedList(list[_T]):
"""List, which will call passed on_change if a change happens."""

Expand Down
135 changes: 135 additions & 0 deletions tests/commands/json/test_auto_empty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from typing import Any

import pytest

from deebot_client.commands.json import GetAutoEmpty, SetAutoEmpty
from deebot_client.events import AutoEmptyMode, AutoEmptyModeEvent
from tests.helpers import (
get_request_json,
get_success_body,
verify_DisplayNameStrEnum_unique,
)

from . import assert_command, assert_set_command


def test_WorkMode_unique() -> None:
verify_DisplayNameStrEnum_unique(AutoEmptyMode)


@pytest.mark.parametrize(
("json", "expected"),
[
(
{"enable": 1, "frequency": "10"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_10),
),
(
{"enable": 1, "frequency": "15"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_15),
),
(
{"enable": 1, "frequency": "25"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_25),
),
(
{"enable": 0, "frequency": "25"},
AutoEmptyModeEvent(enable=False, mode=AutoEmptyMode.MODE_25),
),
(
{"enable": 1, "frequency": "auto"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_AUTO),
),
(
{"enable": 1, "frequency": "smart"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_SMART),
),
],
)
async def test_GetAutoEmpty(json: dict[str, Any], expected: AutoEmptyModeEvent) -> None:
json = get_request_json(get_success_body(json))
await assert_command(GetAutoEmpty(), json, expected)


@pytest.mark.parametrize(
("value", "args", "expected"),
[
(
(True, AutoEmptyMode.MODE_10),
{"enable": 1, "frequency": "10"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_10),
),
(
(True, "mode_smart"),
{"enable": 1, "frequency": "smart"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_SMART),
),
# NOTE: this test is also working, as 'enable' will set auto to 'true' if not provided
# as 'enable' is required when set a 'frequency'
(
(None, AutoEmptyMode.MODE_25),
{"enable": 1, "frequency": "25"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_25),
),
# NOTE: it should be possible to only send 'True' for turn on without 'frequency',
# but not sure how to implement the test correct
(
(True, AutoEmptyMode.MODE_AUTO),
{"enable": 1, "frequency": "auto"},
AutoEmptyModeEvent(enable=True, mode=AutoEmptyMode.MODE_AUTO),
),
# NOTE: it should be possible to only send 'False' for turn off without 'frequency',
# but not sure how to implement the test correct
(
(False, AutoEmptyMode.MODE_AUTO),
{"enable": 0, "frequency": "auto"},
AutoEmptyModeEvent(enable=False, mode=AutoEmptyMode.MODE_AUTO),
),
],
)
async def test_SetAutoEmpty(
value: tuple[bool | None, AutoEmptyMode | str | None],
args: dict[str, Any],
expected: AutoEmptyModeEvent,
) -> None:
command = SetAutoEmpty()
if value[0] is None and value[1] is not None:
command = SetAutoEmpty(frequency=value[1])
elif value[1] is None and value[0] is not None:
command = SetAutoEmpty(enable=value[0])
elif value[0] is not None and value[1] is not None:
command = SetAutoEmpty(value[0], value[1])

await assert_set_command(command, args, expected)


@pytest.mark.parametrize(
("value", "args", "expected"),
[
(
(None, AutoEmptyMode.MODE_AUTO),
{"enable": 0, "frequency": "auto"},
AutoEmptyModeEvent(enable=False, mode=AutoEmptyMode.MODE_AUTO),
),
(
(None, None),
{"enable": 0, "frequency": "auto"},
AutoEmptyModeEvent(enable=False, mode=AutoEmptyMode.MODE_AUTO),
),
],
)
async def test_SetAutoEmptyFail(
value: tuple[bool | None, AutoEmptyMode | str | None],
args: dict[str, Any],
expected: AutoEmptyModeEvent,
) -> None:
command = SetAutoEmpty()
if value[0] is None and value[1] is not None:
command = SetAutoEmpty(frequency=value[1])
elif value[1] is None and value[0] is not None:
command = SetAutoEmpty(enable=value[0])
elif value[0] is not None and value[1] is not None:
command = SetAutoEmpty(value[0], value[1])

with pytest.raises(AssertionError):
await assert_set_command(command, args, expected)
Loading