Skip to content
This repository has been archived by the owner on Mar 8, 2022. It is now read-only.

Commit

Permalink
Refactor commands (#71)
Browse files Browse the repository at this point in the history
* refactor GetLifeSpan

* refactor GetStats

* refactor GetBattery

* refactor GetChargeState

* refactor Charge

* refactor clean commands

* refactor GetCleanLogs

* refactor GetError

* refactor PlaySound

* refactor getCleanInfo

* refactor setRelocationState

* move map commands

* improve enums

* add CustomCommand

* log on timeout

* fix message handling

* refactor handle method. Only pass command or command_name
  • Loading branch information
edenhaus authored Oct 5, 2021
1 parent 4822f19 commit 754649b
Show file tree
Hide file tree
Showing 30 changed files with 973 additions and 793 deletions.
4 changes: 3 additions & 1 deletion .github/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@ version-resolver:
default: minor

template: |
[![Downloads for this release](https://img.shields.io/github/downloads/And3rsL/Deebotozmo/$RESOLVED_VERSION/total.svg)](https://github.com/And3rsL/Deebotozmo/releases/$RESOLVED_VERSION)
$CHANGES
**Like my work and want to support me?**
<a href="https://www.buymeacoffee.com/edenhaus" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-black.png" width="150px" height="35px" alt="Buy Me A Coffee" style="height: 35px !important;width: 150px !important;" ></a>
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<a href="https://www.buymeacoffee.com/4nd3rs" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-black.png" width="150px" height="35px" alt="Buy Me A Coffee" style="height: 35px !important;width: 150px !important;" ></a>
<a href="https://www.buymeacoffee.com/edenhaus" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-black.png" width="150px" height="35px" alt="Buy Me A Coffee" style="height: 35px !important;width: 150px !important;" ></a>

=====

Expand Down Expand Up @@ -28,7 +28,8 @@ import logging
import random
import string

from deebotozmo.commands_old import *
from deebotozmo.commands import *
from deebotozmo.commands.clean import CleanAction
from deebotozmo.ecovacs_api import EcovacsAPI
from deebotozmo.ecovacs_mqtt import EcovacsMqtt
from deebotozmo.events import BatteryEvent
Expand All @@ -54,7 +55,8 @@ async def main():
auth = await api.get_request_auth()
bot = VacuumBot(session, auth, devices_[0], continent=continent, country=country, verify_ssl=False)

mqtt = EcovacsMqtt(auth, continent=continent)
mqtt = EcovacsMqtt(continent=continent, country=country)
await mqtt.initialize(auth)
await mqtt.subscribe(bot)

async def on_battery(event: BatteryEvent):
Expand All @@ -64,10 +66,10 @@ async def main():
pass

# Subscribe for events (more events available)
bot.batteryEvents.subscribe(on_battery)
bot.events.battery.subscribe(on_battery)

# Execute commands
await bot.execute_command(CleanStart())
await bot.execute_command(Clean(CleanAction.START))
await asyncio.sleep(900) # Wait for...
await bot.execute_command(Charge())

Expand Down
29 changes: 11 additions & 18 deletions deebotozmo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,14 @@
import aiohttp
import click

from deebotozmo.commands import SetFanSpeed, SetWaterInfo
from deebotozmo.commands_old import (
Charge,
CleanCustomArea,
CleanPause,
CleanResume,
CleanSpotArea,
CleanStart,
PlaySound,
)
from deebotozmo.commands import Charge, Clean, PlaySound, SetFanSpeed, SetWaterInfo
from deebotozmo.commands.clean import CleanAction, CleanArea, CleanMode
from deebotozmo.ecovacs_api import EcovacsAPI
from deebotozmo.events import (
BatteryEvent,
CleanLogEvent,
FanSpeedEvent,
LifeSpanEvent,
MapEvent,
RoomsEvent,
StatsEvent,
Expand Down Expand Up @@ -193,7 +186,7 @@ async def play_sound(ctx: click.Context) -> None:
@coro
async def clean(ctx: click.Context) -> None:
"""Click subcommand that runs the auto clean command."""
await run_with_login(ctx, CleanStart)
await run_with_login(ctx, Clean, cmd_args=[CleanAction.START])


@cli.command(
Expand All @@ -209,8 +202,8 @@ async def custom_area(ctx: click.Context, area: str, cleanings: int = 1) -> None
"""Click subcommand that runs a clean in a custom area."""
await run_with_login(
ctx,
CleanCustomArea,
cmd_args={"map_position": area, "cleanings": cleanings},
CleanArea,
cmd_args={"mode": CleanMode.CUSTOM_AREA, "area": area, "cleanings": cleanings},
)


Expand All @@ -227,8 +220,8 @@ async def spot_area(ctx: click.Context, rooms: str, cleanings: int = 1) -> None:
"""Click subcommand that runs a clean in a specific room."""
await run_with_login(
ctx,
CleanSpotArea,
cmd_args={"area": rooms, "cleanings": cleanings},
CleanArea,
cmd_args={"mode": CleanMode.CUSTOM_AREA, "area": rooms, "cleanings": cleanings},
)


Expand Down Expand Up @@ -263,15 +256,15 @@ async def charge(ctx: click.Context) -> None:
@coro
async def pause(ctx: click.Context) -> None:
"""Click subcommand that pauses the clean."""
await run_with_login(ctx, CleanPause)
await run_with_login(ctx, Clean, cmd_args=[CleanAction.PAUSE])


@cli.command(help="Resume the robot")
@click.pass_context
@coro
async def resume(ctx: click.Context) -> None:
"""Click subcommand that resumes the clean."""
await run_with_login(ctx, CleanResume)
await run_with_login(ctx, Clean, cmd_args=[CleanAction.RESUME])


@cli.command(name="getcleanlogs", help="Get Clean Logs")
Expand Down Expand Up @@ -386,7 +379,7 @@ async def components(ctx: click.Context) -> None:

event = asyncio.Event()

async def on_lifespan_event(lifespan_event: dict) -> None:
async def on_lifespan_event(lifespan_event: LifeSpanEvent) -> None:
for key, value in lifespan_event.items():
print(f"{key}: {value}%")
event.set()
Expand Down
64 changes: 58 additions & 6 deletions deebotozmo/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,74 @@
"""Commands module."""
from typing import Dict, List, Type

from .base import Command, SetCommand
from .base import Command, CommandWithHandling, SetCommand
from .battery import GetBattery
from .charge import Charge
from .charge_state import GetChargeState
from .clean import Clean, CleanArea, GetCleanInfo
from .clean_logs import GetCleanLogs
from .error import GetError
from .fan_speed import FanSpeedLevel, GetFanSpeed, SetFanSpeed
from .life_span import GetLifeSpan
from .map import (
GetCachedMapInfo,
GetMajorMap,
GetMapSet,
GetMapSubSet,
GetMapTrace,
GetMinorMap,
GetPos,
)
from .play_sound import PlaySound
from .relocation import SetRelocationState
from .stats import GetStats
from .water_info import GetWaterInfo, SetWaterInfo, WaterLevel

# fmt: off
_COMMANDS: List[Type[Command]] = [
GetWaterInfo,
SetWaterInfo,
# ordered by file asc
_COMMANDS: List[Type[CommandWithHandling]] = [
GetBattery,

Charge,

GetChargeState,

Clean,
CleanArea,
GetCleanInfo,

GetCleanLogs,

GetError,

GetFanSpeed,
SetFanSpeed
SetFanSpeed,

GetLifeSpan,

PlaySound,

SetRelocationState,

GetStats,

GetWaterInfo,
SetWaterInfo,
]
# fmt: on

COMMANDS: Dict[str, Type[Command]] = {cmd.name: cmd for cmd in _COMMANDS} # type: ignore
COMMANDS: Dict[str, Type[CommandWithHandling]] = {cmd.name: cmd for cmd in _COMMANDS}

SET_COMMAND_NAMES: List[str] = [
cmd.name for cmd in COMMANDS.values() if issubclass(cmd, SetCommand)
]

MAP_COMMANDS: List[Type[Command]] = [
GetMajorMap,
GetMapSet,
GetMinorMap,
GetPos,
GetMapTrace,
GetMapSubSet,
GetCachedMapInfo,
]
86 changes: 59 additions & 27 deletions deebotozmo/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,45 @@ def args(self) -> Union[Dict[str, Any], List]:
"""Command additional arguments."""
return self._args


class CommandWithHandling(Command, ABC):
"""Command, which handle response by itself."""

# required as name is class variable, will be overwritten in subclasses
name = "__invalid__"

@classmethod
@abstractmethod
def _handle_body_data(cls, events: VacuumEmitter, data: Dict[str, Any]) -> bool:
def _handle_body_data_list(cls, events: VacuumEmitter, data: List) -> bool:
"""Handle message->body->data and notify the correct event subscribers.
:return: True if data was valid and no error was included
"""
raise NotImplementedError

@classmethod
def _handle_body_data_dict(
cls, events: VacuumEmitter, data: Dict[str, Any]
) -> bool:
"""Handle message->body->data and notify the correct event subscribers.
:return: True if data was valid and no error was included
"""
raise NotImplementedError

@classmethod
def _handle_body_data(
cls, events: VacuumEmitter, data: Union[Dict[str, Any], List]
) -> bool:
"""Handle message->body->data and notify the correct event subscribers.
:return: True if data was valid and no error was included
"""
if isinstance(data, dict):
return cls._handle_body_data_dict(events, data)

if isinstance(data, list):
return cls._handle_body_data_list(events, data)

@classmethod
def _handle_body(cls, events: VacuumEmitter, body: Dict[str, Any]) -> bool:
"""Handle message->body and notify the correct event subscribers.
Expand Down Expand Up @@ -71,8 +101,8 @@ def handle_requested(self, events: VacuumEmitter, response: Dict[str, Any]) -> b
return False


class GetCommand(Command, ABC):
"""Base get command."""
class _NoArgsCommand(CommandWithHandling, ABC):
"""Command without args."""

# required as name is class variable, will be overwritten in subclasses
name = "__invalid__"
Expand All @@ -81,7 +111,27 @@ def __init__(self) -> None:
super().__init__()


class SetCommand(Command, ABC):
class _ExecuteCommand(CommandWithHandling, ABC):
"""Command, which is executing something (ex. Charge)."""

# required as name is class variable, will be overwritten in subclasses
name = "__invalid__"

@classmethod
def _handle_body(cls, events: VacuumEmitter, body: Dict[str, Any]) -> bool:
"""Handle message->body and notify the correct event subscribers.
:return: True if data was valid and no error was included
"""
# Success event looks like { "code": 0, "msg": "ok" }
if body.get(_CODE, -1) == 0:
return True

_LOGGER.warning('Command "%s" was not successfully. body=%s', cls.name, body)
return False


class SetCommand(_ExecuteCommand, ABC):
"""Base set command.
Command needs to be linked to the "get" command, for handling (updating) the sensors.
Expand All @@ -107,34 +157,16 @@ def __init__(

@property
@abstractmethod
def get_command(self) -> Type[Command]:
def get_command(self) -> Type[CommandWithHandling]:
"""Return the corresponding "get" command."""
raise NotImplementedError

@classmethod
def _handle_body(cls, events: VacuumEmitter, body: Dict[str, Any]) -> bool:
"""Handle message->body and notify the correct event subscribers.
:return: True if data was valid and no error was included
"""
# Success event looks like { "code": 0, "msg": "ok" }
if body.get(_CODE, -1) == 0:
return True

_LOGGER.warning('Command "%s" was not successfully. body=%s', cls.name, body)
return False

@classmethod
def _handle_body_data(cls, events: VacuumEmitter, data: Dict[str, Any]) -> bool:
# not required as we overwrite "_handle_body"
return True


@unique
class DisplayNameEnum(IntEnum):
class DisplayNameIntEnum(IntEnum):
"""Int enum with a property "display_name"."""

def __new__(cls, *args: Tuple, **_: Mapping) -> "DisplayNameEnum":
def __new__(cls, *args: Tuple, **_: Mapping) -> "DisplayNameIntEnum":
"""Create new enum."""
obj = int.__new__(cls)
obj._value_ = args[0]
Expand All @@ -153,7 +185,7 @@ def display_name(self) -> str:
return self.name.lower()

@classmethod
def get(cls, value: str) -> "DisplayNameEnum":
def get(cls, value: str) -> "DisplayNameIntEnum":
"""Get enum member from name or display_name."""
value = str(value).upper()
if value in cls.__members__:
Expand Down
28 changes: 28 additions & 0 deletions deebotozmo/commands/battery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Battery commands."""
import logging
from typing import Any, Dict

from ..events import BatteryEvent
from .base import VacuumEmitter, _NoArgsCommand

_LOGGER = logging.getLogger(__name__)


class GetBattery(_NoArgsCommand):
"""Get battery command."""

name = "getBattery"

@classmethod
def _handle_body_data_dict(
cls, events: VacuumEmitter, data: Dict[str, Any]
) -> bool:
"""Handle message->body->data and notify the correct event subscribers.
:return: True if data was valid and no error was included
"""
try:
events.battery.notify(BatteryEvent(data["value"]))
except ValueError:
_LOGGER.warning("Couldn't parse battery status: %s", data)
return True
Loading

0 comments on commit 754649b

Please sign in to comment.