Skip to content

Commit

Permalink
Implement introspectable actions (#1588)
Browse files Browse the repository at this point in the history
Adds support for introspectable actions and implements them for RoborockVacuum.
  • Loading branch information
starkillerOG authored Nov 15, 2022
1 parent 6ee7db0 commit 0db7997
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 1 deletion.
22 changes: 22 additions & 0 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <miio.devicestatus.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
Expand Down
12 changes: 11 additions & 1 deletion miio/device.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
30 changes: 30 additions & 0 deletions miio/devicestatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)

from .descriptors import (
ActionDescriptor,
BooleanSettingDescriptor,
EnumSettingDescriptor,
NumberSettingDescriptor,
Expand Down Expand Up @@ -235,3 +236,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
10 changes: 10 additions & 0 deletions miio/integrations/vacuum/roborock/vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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

Expand Down Expand Up @@ -137,6 +138,7 @@ def start(self):
return self.send("app_start")

@command()
@action(name="Stop cleaning", type="vacuum")
def stop(self):
"""Stop cleaning.
Expand All @@ -146,16 +148,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()
Expand Down Expand Up @@ -208,6 +213,7 @@ def create_dummy_mac(addr):
return self._info

@command()
@action(name="Home", type="vacuum")
def home(self):
"""Stop cleaning and return home."""

Expand Down Expand Up @@ -470,6 +476,7 @@ def clean_details(
return res

@command()
@action(name="Find robot", type="vacuum")
def find(self):
"""Find the robot."""
return self.send("find_me", [""])
Expand Down Expand Up @@ -647,6 +654,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")
Expand Down Expand Up @@ -781,12 +789,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()
Expand Down

0 comments on commit 0db7997

Please sign in to comment.