From 6e050b14b2c41f6eaa1db35412fe88d1068bde90 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 21:50:45 +0100 Subject: [PATCH 1/6] Implement actions --- miio/device.py | 12 +++++++++++- miio/devicestatus.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/miio/device.py b/miio/device.py index 433cd4d12..6ce467555 100644 --- a/miio/device.py +++ b/miio/device.py @@ -1,5 +1,6 @@ import logging from enum import Enum +from inspect import getmembers from typing import Any, Dict, List, Optional, Union # noqa: F401 import click @@ -57,6 +58,7 @@ def __init__( self.token: Optional[str] = token self._model: Optional[str] = model self._info: Optional[DeviceInfo] = None + self._actions: Optional[List[ActionDescriptor]] = None timeout = timeout if timeout is not None else self.timeout self._protocol = MiIOProtocol( ip, token, start_id, debug, lazy_discover, timeout @@ -243,7 +245,15 @@ def status(self) -> DeviceStatus: def actions(self) -> Dict[str, ActionDescriptor]: """Return device actions.""" - return {} + if self._actions is None: + self._actions = [] + for action_tuple in getmembers(self, lambda o: hasattr(o, "_action")): + method_name, method = action_tuple + action = method._action + action.method = method # bind the method + self._actions.append(action) + + return self._actions def settings(self) -> Dict[str, SettingDescriptor]: """Return device settings.""" diff --git a/miio/devicestatus.py b/miio/devicestatus.py index d3fe0242f..17e08cb3c 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -14,6 +14,7 @@ ) from .descriptors import ( + ActionDescriptor, BooleanSettingDescriptor, EnumSettingDescriptor, NumberSettingDescriptor, @@ -236,3 +237,32 @@ def decorator_setting(func): return func return decorator_setting + + +def action(name: str, **kwargs): + """Syntactic sugar to create ActionDescriptor objects. + + The information can be used by users of the library to programmatically find out what + types of actions are available for the device. + + The interface is kept minimal, but you can pass any extra keyword arguments. + These extras are made accessible over :attr:`~miio.descriptors.ActionDescriptor.extras`, + and can be interpreted downstream users as they wish. + """ + + def decorator_action(func): + property_name = str(func.__name__) + qualified_name = str(func.__qualname__) + + descriptor = ActionDescriptor( + id=qualified_name, + name=name, + method_name=property_name, + method=None, + extras=kwargs, + ) + func._action = descriptor + + return func + + return decorator_action From 24cdc346f69db4dede312225d26546f338dcbeb2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 21:53:35 +0100 Subject: [PATCH 2/6] Add Start/Stop dust collection actions --- miio/integrations/vacuum/roborock/vacuum.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 3f45dfac2..f36a272f4 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -20,6 +20,7 @@ LiteralParamType, command, ) +from miio.devicestatus import action from miio.device import Device, DeviceInfo from miio.exceptions import DeviceInfoUnavailableException, UnsupportedFeatureException from miio.interfaces import FanspeedPresets, VacuumInterface @@ -866,12 +867,14 @@ def set_dust_collection_mode(self, mode: DustCollectionMode) -> bool: return self.send("set_dust_collection_mode", {"mode": mode.value})[0] == "ok" @command() + @action(name="Start dust collection", icon="mdi:turbine") def start_dust_collection(self): """Activate automatic dust collection.""" self._verify_auto_empty_support() return self.send("app_start_collect_dust") @command() + @action(name="Stop dust collection", icon="mdi:turbine") def stop_dust_collection(self): """Abort in progress dust collection.""" self._verify_auto_empty_support() From 5790d08a84c8dc6e504d9e413083a3d020a25af6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 22:35:43 +0100 Subject: [PATCH 3/6] make actions a dict --- miio/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/miio/device.py b/miio/device.py index 6ce467555..97006649e 100644 --- a/miio/device.py +++ b/miio/device.py @@ -58,7 +58,7 @@ def __init__( self.token: Optional[str] = token self._model: Optional[str] = model self._info: Optional[DeviceInfo] = None - self._actions: Optional[List[ActionDescriptor]] = None + self._actions: Optional[Dict[str, ActionDescriptor]] = None timeout = timeout if timeout is not None else self.timeout self._protocol = MiIOProtocol( ip, token, start_id, debug, lazy_discover, timeout @@ -246,12 +246,12 @@ def status(self) -> DeviceStatus: def actions(self) -> Dict[str, ActionDescriptor]: """Return device actions.""" if self._actions is None: - self._actions = [] + self._actions = {} for action_tuple in getmembers(self, lambda o: hasattr(o, "_action")): method_name, method = action_tuple action = method._action action.method = method # bind the method - self._actions.append(action) + self._actions[method_name] = action return self._actions From c411ad1430c78d54d496cf269d44f456b7296b72 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 22:48:55 +0100 Subject: [PATCH 4/6] Add docs --- docs/contributing.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index 9d64efbee..6afb51989 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -344,6 +344,28 @@ If the device has a setting with some pre-defined values, you want to use this. """Return the LED brightness.""" +Actions +""""""" + +Use :meth:`@action ` to create :class:`~miio.descriptors.ActionDescriptor` +objects for the device. +This will make all decorated actions accessible through :meth:`~miio.device.Device.actions` for downstream users. + +.. code-block:: python + + @command() + @action(name="Do Something", some_kwarg_for_downstream="hi there") + def do_something(self): + """Execute some action on the device.""" + +.. note:: + + All keywords arguments not defined in the decorator signature will be available + through the :attr:`~miio.descriptors.ActionDescriptor.extras` variable. + + This information can be used to pass information to the downstream users. + + .. _adding_tests: Adding tests From ae6a2b1a6ac905b5e22cc480a27ab7df5b9157eb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 22:54:31 +0100 Subject: [PATCH 5/6] fix isort --- miio/integrations/vacuum/roborock/vacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index f36a272f4..2502835c0 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -20,8 +20,8 @@ LiteralParamType, command, ) -from miio.devicestatus import action from miio.device import Device, DeviceInfo +from miio.devicestatus import action from miio.exceptions import DeviceInfoUnavailableException, UnsupportedFeatureException from miio.interfaces import FanspeedPresets, VacuumInterface From 7120c8f085c076d4941a6af0fac0d65d2545a025 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 14 Nov 2022 21:14:50 +0100 Subject: [PATCH 6/6] Add additional actions --- miio/integrations/vacuum/roborock/vacuum.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 2502835c0..4872342d4 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -229,6 +229,7 @@ def start(self): return self.send("app_start") @command() + @action(name="Stop cleaning", type="vacuum") def stop(self): """Stop cleaning. @@ -238,16 +239,19 @@ def stop(self): return self.send("app_stop") @command() + @action(name="Spot cleaning", type="vacuum") def spot(self): """Start spot cleaning.""" return self.send("app_spot") @command() + @action(name="Pause cleaning", type="vacuum") def pause(self): """Pause cleaning.""" return self.send("app_pause") @command() + @action(name="Start cleaning", type="vacuum") def resume_or_start(self): """A shortcut for resuming or starting cleaning.""" status = self.status() @@ -300,6 +304,7 @@ def create_dummy_mac(addr): return self._info @command() + @action(name="Home", type="vacuum") def home(self): """Stop cleaning and return home.""" @@ -556,6 +561,7 @@ def clean_details( return res @command() + @action(name="Find robot", type="vacuum") def find(self): """Find the robot.""" return self.send("find_me", [""]) @@ -733,6 +739,7 @@ def set_sound_volume(self, vol: int): return self.send("change_sound_volume", [vol]) @command() + @action(name="Test sound volume", type="vacuum") def test_sound_volume(self): """Test current sound volume.""" return self.send("test_sound_volume")