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 diff --git a/miio/device.py b/miio/device.py index 098502656..076346cb4 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 @@ -62,6 +63,7 @@ def __init__( self.token: Optional[str] = token self._model: Optional[str] = model self._info: Optional[DeviceInfo] = 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 @@ -248,7 +250,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[method_name] = action + + return self._actions def settings(self) -> Dict[str, SettingDescriptor]: """Return device settings.""" diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 18bf37f08..81e70a397 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -14,6 +14,7 @@ ) from .descriptors import ( + ActionDescriptor, BooleanSettingDescriptor, EnumSettingDescriptor, NumberSettingDescriptor, @@ -238,3 +239,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 diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 3f45dfac2..4872342d4 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -21,6 +21,7 @@ command, ) from miio.device import Device, DeviceInfo +from miio.devicestatus import action from miio.exceptions import DeviceInfoUnavailableException, UnsupportedFeatureException from miio.interfaces import FanspeedPresets, VacuumInterface @@ -228,6 +229,7 @@ def start(self): return self.send("app_start") @command() + @action(name="Stop cleaning", type="vacuum") def stop(self): """Stop cleaning. @@ -237,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() @@ -299,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.""" @@ -555,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", [""]) @@ -732,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") @@ -866,12 +874,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()