Skip to content

Commit 0db7997

Browse files
authored
Implement introspectable actions (#1588)
Adds support for introspectable actions and implements them for RoborockVacuum.
1 parent 6ee7db0 commit 0db7997

File tree

4 files changed

+73
-1
lines changed

4 files changed

+73
-1
lines changed

docs/contributing.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,28 @@ If the device has a setting with some pre-defined values, you want to use this.
344344
"""Return the LED brightness."""
345345
346346
347+
Actions
348+
"""""""
349+
350+
Use :meth:`@action <miio.devicestatus.action>` to create :class:`~miio.descriptors.ActionDescriptor`
351+
objects for the device.
352+
This will make all decorated actions accessible through :meth:`~miio.device.Device.actions` for downstream users.
353+
354+
.. code-block:: python
355+
356+
@command()
357+
@action(name="Do Something", some_kwarg_for_downstream="hi there")
358+
def do_something(self):
359+
"""Execute some action on the device."""
360+
361+
.. note::
362+
363+
All keywords arguments not defined in the decorator signature will be available
364+
through the :attr:`~miio.descriptors.ActionDescriptor.extras` variable.
365+
366+
This information can be used to pass information to the downstream users.
367+
368+
347369
.. _adding_tests:
348370

349371
Adding tests

miio/device.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from enum import Enum
3+
from inspect import getmembers
34
from typing import Any, Dict, List, Optional, Union # noqa: F401
45

56
import click
@@ -62,6 +63,7 @@ def __init__(
6263
self.token: Optional[str] = token
6364
self._model: Optional[str] = model
6465
self._info: Optional[DeviceInfo] = None
66+
self._actions: Optional[Dict[str, ActionDescriptor]] = None
6567
timeout = timeout if timeout is not None else self.timeout
6668
self._protocol = MiIOProtocol(
6769
ip, token, start_id, debug, lazy_discover, timeout
@@ -248,7 +250,15 @@ def status(self) -> DeviceStatus:
248250

249251
def actions(self) -> Dict[str, ActionDescriptor]:
250252
"""Return device actions."""
251-
return {}
253+
if self._actions is None:
254+
self._actions = {}
255+
for action_tuple in getmembers(self, lambda o: hasattr(o, "_action")):
256+
method_name, method = action_tuple
257+
action = method._action
258+
action.method = method # bind the method
259+
self._actions[method_name] = action
260+
261+
return self._actions
252262

253263
def settings(self) -> Dict[str, SettingDescriptor]:
254264
"""Return device settings."""

miio/devicestatus.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515

1616
from .descriptors import (
17+
ActionDescriptor,
1718
BooleanSettingDescriptor,
1819
EnumSettingDescriptor,
1920
NumberSettingDescriptor,
@@ -235,3 +236,32 @@ def decorator_setting(func):
235236
return func
236237

237238
return decorator_setting
239+
240+
241+
def action(name: str, **kwargs):
242+
"""Syntactic sugar to create ActionDescriptor objects.
243+
244+
The information can be used by users of the library to programmatically find out what
245+
types of actions are available for the device.
246+
247+
The interface is kept minimal, but you can pass any extra keyword arguments.
248+
These extras are made accessible over :attr:`~miio.descriptors.ActionDescriptor.extras`,
249+
and can be interpreted downstream users as they wish.
250+
"""
251+
252+
def decorator_action(func):
253+
property_name = str(func.__name__)
254+
qualified_name = str(func.__qualname__)
255+
256+
descriptor = ActionDescriptor(
257+
id=qualified_name,
258+
name=name,
259+
method_name=property_name,
260+
method=None,
261+
extras=kwargs,
262+
)
263+
func._action = descriptor
264+
265+
return func
266+
267+
return decorator_action

miio/integrations/vacuum/roborock/vacuum.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
command,
2121
)
2222
from miio.device import Device, DeviceInfo
23+
from miio.devicestatus import action
2324
from miio.exceptions import DeviceInfoUnavailableException, UnsupportedFeatureException
2425
from miio.interfaces import FanspeedPresets, VacuumInterface
2526

@@ -137,6 +138,7 @@ def start(self):
137138
return self.send("app_start")
138139

139140
@command()
141+
@action(name="Stop cleaning", type="vacuum")
140142
def stop(self):
141143
"""Stop cleaning.
142144
@@ -146,16 +148,19 @@ def stop(self):
146148
return self.send("app_stop")
147149

148150
@command()
151+
@action(name="Spot cleaning", type="vacuum")
149152
def spot(self):
150153
"""Start spot cleaning."""
151154
return self.send("app_spot")
152155

153156
@command()
157+
@action(name="Pause cleaning", type="vacuum")
154158
def pause(self):
155159
"""Pause cleaning."""
156160
return self.send("app_pause")
157161

158162
@command()
163+
@action(name="Start cleaning", type="vacuum")
159164
def resume_or_start(self):
160165
"""A shortcut for resuming or starting cleaning."""
161166
status = self.status()
@@ -208,6 +213,7 @@ def create_dummy_mac(addr):
208213
return self._info
209214

210215
@command()
216+
@action(name="Home", type="vacuum")
211217
def home(self):
212218
"""Stop cleaning and return home."""
213219

@@ -470,6 +476,7 @@ def clean_details(
470476
return res
471477

472478
@command()
479+
@action(name="Find robot", type="vacuum")
473480
def find(self):
474481
"""Find the robot."""
475482
return self.send("find_me", [""])
@@ -647,6 +654,7 @@ def set_sound_volume(self, vol: int):
647654
return self.send("change_sound_volume", [vol])
648655

649656
@command()
657+
@action(name="Test sound volume", type="vacuum")
650658
def test_sound_volume(self):
651659
"""Test current sound volume."""
652660
return self.send("test_sound_volume")
@@ -781,12 +789,14 @@ def set_dust_collection_mode(self, mode: DustCollectionMode) -> bool:
781789
return self.send("set_dust_collection_mode", {"mode": mode.value})[0] == "ok"
782790

783791
@command()
792+
@action(name="Start dust collection", icon="mdi:turbine")
784793
def start_dust_collection(self):
785794
"""Activate automatic dust collection."""
786795
self._verify_auto_empty_support()
787796
return self.send("app_start_collect_dust")
788797

789798
@command()
799+
@action(name="Stop dust collection", icon="mdi:turbine")
790800
def stop_dust_collection(self):
791801
"""Abort in progress dust collection."""
792802
self._verify_auto_empty_support()

0 commit comments

Comments
 (0)