Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement introspectable settings #1500

Merged
merged 4 commits into from
Aug 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,24 @@ that is passed to the method as string, and an optional integer argument.
Status containers
~~~~~~~~~~~~~~~~~

The status container (returned by `status()` method of the device class)
The status container (returned by the :meth:`~miio.device.Device.status` method of the device class)
is the main way for library users to access properties exposed by the device.
The status container should inherit :class:`~miio.devicestatus.DeviceStatus`.
This ensures a generic :meth:`__repr__` that is helpful for debugging,
and allows defining properties that are especially interesting for end users.

The properties can be decorated using special decorators to define meta information
that enables introspection and programatic creation of user interface elements.
Doing so ensures that a developer-friendly :meth:`~miio.devicestatus.DeviceStatus.__repr__` based on the defined
properties is there to help with debugging.
Furthermore, it allows defining meta information about properties that are especially interesting for end users.

.. note::

The helper decorators are just syntactic sugar to create the corresponding descriptor classes
and binding them to the status class.

.. note::

The descriptors are merely hints to downstream users about the device capabilities.
In practice this means that neither the input nor the output values of functions decorated with
the descriptors are enforced automatically by this library.


Sensors
"""""""
Expand Down Expand Up @@ -210,7 +214,7 @@ Switches
""""""""

Use :meth:`@switch <miio.devicestatus.switch>` to create :class:`~miio.descriptors.SwitchDescriptor` objects.
This will make all decorated sensors accessible through :meth:`~miio.device.Device.switches` for downstream users.
This will make all decorated switches accessible through :meth:`~miio.device.Device.switches` for downstream users.

.. code-block::

Expand All @@ -219,8 +223,60 @@ This will make all decorated sensors accessible through :meth:`~miio.device.Devi
def power(self) -> bool:
"""Return if device is turned on."""

The mandatory *setter_name* will be used to bind the method to be accessible using
the :meth:`~miio.descriptors.SwitchDescriptor.setter` callable.
You can either use *setter* to define a callable that can be used to adjust the value of the property,
or alternatively define *setter_name* which will be used to bind the method during the initialization
to the the :meth:`~miio.descriptors.SwitchDescriptor.setter` callable.


Settings
""""""""

Use :meth:`@switch <miio.devicestatus.setting>` to create :meth:`~miio.descriptors.SettingDescriptor` objects.
This will make all decorated settings accessible through :meth:`~miio.device.Device.settings` for downstream users.

The type of the descriptor depends on the input parameters:

* Passing *min_value* or *max_value* will create a :class:`~miio.descriptors.NumberSettingDescriptor`,
which is useful for presenting ranges of values.
* Passing an Enum object using *choices* will create a :class:`~miio.descriptors.EnumSettingDescriptor`,
which is useful for presenting a fixed set of options.


You can either use *setter* to define a callable that can be used to adjust the value of the property,
or alternatively define *setter_name* which will be used to bind the method during the initialization
to the the :meth:`~miio.descriptors.SettingDescriptor.setter` callable.

Numerical Settings
^^^^^^^^^^^^^^^^^^

The number descriptor allows defining a range of values and information about the steps.
The *max_value* is the only mandatory parameter. If not given, *min_value* defaults to ``0`` and *steps* to ``1``.

.. code-block::

@property
@switch(name="Fan Speed", min_value=0, max_value=100, steps=5, setter_name="set_fan_speed")
def fan_speed(self) -> int:
"""Return the current fan speed."""


Enum-based Settings
^^^^^^^^^^^^^^^^^^^

If the device has a setting with some pre-defined values, you want to use this.

.. code-block::

class LedBrightness(Enum):
Dim = 0
Bright = 1
Off = 2

@property
@switch(name="LED Brightness", choices=SomeEnum, setter_name="set_led_brightness")
def led_brightness(self) -> LedBrightness:
"""Return the LED brightness."""


.. _adding_tests:

Expand Down
64 changes: 32 additions & 32 deletions miio/cooker.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,113 +323,113 @@ def __init__(self, settings: str = None):
Bit 5-8: Unused
"""
if settings is None:
self.settings = [0, 4]
self._settings = [0, 4]
else:
self.settings = [
self._settings = [
int(settings[i : i + 2], 16) for i in range(0, len(settings), 2)
]

@property
def pressure_supported(self) -> bool:
return self.settings[0] & 1 != 0
return self._settings[0] & 1 != 0

@pressure_supported.setter
def pressure_supported(self, supported: bool):
if supported:
self.settings[0] |= 1
self._settings[0] |= 1
else:
self.settings[0] &= 254
self._settings[0] &= 254

@property
def led_on(self) -> bool:
return self.settings[0] & 2 != 0
return self._settings[0] & 2 != 0

@led_on.setter
def led_on(self, on: bool):
if on:
self.settings[0] |= 2
self._settings[0] |= 2
else:
self.settings[0] &= 253
self._settings[0] &= 253

@property
def auto_keep_warm(self) -> bool:
return self.settings[0] & 4 != 0
return self._settings[0] & 4 != 0

@auto_keep_warm.setter
def auto_keep_warm(self, keep_warm: bool):
if keep_warm:
self.settings[0] |= 4
self._settings[0] |= 4
else:
self.settings[0] &= 251
self._settings[0] &= 251

@property
def lid_open_warning(self) -> bool:
return self.settings[0] & 8 != 0
return self._settings[0] & 8 != 0

@lid_open_warning.setter
def lid_open_warning(self, alarm: bool):
if alarm:
self.settings[0] |= 8
self._settings[0] |= 8
else:
self.settings[0] &= 247
self._settings[0] &= 247

@property
def lid_open_warning_delayed(self) -> bool:
return self.settings[0] & 16 != 0
return self._settings[0] & 16 != 0

@lid_open_warning_delayed.setter
def lid_open_warning_delayed(self, alarm: bool):
if alarm:
self.settings[0] |= 16
self._settings[0] |= 16
else:
self.settings[0] &= 239
self._settings[0] &= 239

@property
def jingzhu_auto_keep_warm(self) -> bool:
return self.settings[1] & 1 != 0
return self._settings[1] & 1 != 0

@jingzhu_auto_keep_warm.setter
def jingzhu_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 1
self._settings[1] |= 1
else:
self.settings[1] &= 254
self._settings[1] &= 254

@property
def kuaizhu_auto_keep_warm(self) -> bool:
return self.settings[1] & 2 != 0
return self._settings[1] & 2 != 0

@kuaizhu_auto_keep_warm.setter
def kuaizhu_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 2
self._settings[1] |= 2
else:
self.settings[1] &= 253
self._settings[1] &= 253

@property
def zhuzhou_auto_keep_warm(self) -> bool:
return self.settings[1] & 4 != 0
return self._settings[1] & 4 != 0

@zhuzhou_auto_keep_warm.setter
def zhuzhou_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 4
self._settings[1] |= 4
else:
self.settings[1] &= 251
self._settings[1] &= 251

@property
def favorite_auto_keep_warm(self) -> bool:
return self.settings[1] & 8 != 0
return self._settings[1] & 8 != 0

@favorite_auto_keep_warm.setter
def favorite_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 8
self._settings[1] |= 8
else:
self.settings[1] &= 247
self._settings[1] &= 247

def __str__(self) -> str:
return "".join([f"{value:02x}" for value in self.settings])
return "".join([f"{value:02x}" for value in self._settings])


class CookerStatus(DeviceStatus):
Expand Down Expand Up @@ -540,7 +540,7 @@ def duration(self) -> int:
return int(self.data["t_cook"])

@property
def settings(self) -> CookerSettings:
def cooker_settings(self) -> CookerSettings:
"""Settings of the cooker."""
return CookerSettings(self.data["setting"])

Expand Down Expand Up @@ -593,7 +593,7 @@ class Cooker(Device):
"Remaining: {result.remaining}\n"
"Cooking delayed: {result.cooking_delayed}\n"
"Duration: {result.duration}\n"
"Settings: {result.settings}\n"
"Settings: {result.cooker_settings}\n"
"Interaction timeouts: {result.interaction_timeouts}\n"
"Hardware version: {result.hardware_version}\n"
"Firmware version: {result.firmware_version}\n"
Expand Down
23 changes: 15 additions & 8 deletions miio/descriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
"""
from dataclasses import dataclass
from enum import Enum, auto
from typing import Callable, Dict, List, Optional
from typing import Callable, Dict, Optional

from attrs import define


@dataclass
class ButtonDescriptor:
"""Describes a button exposed by the device."""

id: str
name: str
method: Callable
method_name: str
method: Optional[Callable] = None
extras: Optional[Dict] = None


Expand Down Expand Up @@ -44,20 +49,21 @@ class SwitchDescriptor:
id: str
name: str
property: str
setter_name: str
setter_name: Optional[str] = None
setter: Optional[Callable] = None
extras: Optional[Dict] = None


@dataclass
@define(kw_only=True)
class SettingDescriptor:
"""Presents a settable value."""

id: str
name: str
property: str
setter: Callable
unit: str
setter: Optional[Callable] = None
setter_name: Optional[str] = None


class SettingType(Enum):
Expand All @@ -66,16 +72,17 @@ class SettingType(Enum):
Enum = auto()


@dataclass
@define(kw_only=True)
class EnumSettingDescriptor(SettingDescriptor):
"""Presents a settable, enum-based value."""

choices: List
type: SettingType = SettingType.Enum
choices_attribute: Optional[str] = None
choices: Optional[Enum] = None
extras: Optional[Dict] = None


@dataclass
@define(kw_only=True)
class NumberSettingDescriptor(SettingDescriptor):
"""Presents a settable, numerical value."""

Expand Down
23 changes: 20 additions & 3 deletions miio/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,20 @@ def buttons(self) -> List[ButtonDescriptor]:
"""Return a list of button-like, clickable actions of the device."""
return []

def settings(self) -> List[SettingDescriptor]:
def settings(self) -> Dict[str, SettingDescriptor]:
"""Return list of settings."""
return []
settings = self.status().settings()
for setting in settings.values():
# TODO: Bind setter methods, this should probably done only once during init.
if setting.setter is None and setting.setter_name is not None:
setting.setter = getattr(self, setting.setter_name)
else:
# TODO: this is ugly, how to fix the issue where setter_name is optional and thus not acceptable for getattr?
raise Exception(
f"Neither setter or setter_name was defined for {setting}"
)

return settings

def sensors(self) -> Dict[str, SensorDescriptor]:
"""Return sensors."""
Expand All @@ -350,7 +361,13 @@ def switches(self) -> Dict[str, SwitchDescriptor]:
switches = self.status().switches()
for switch in switches.values():
# TODO: Bind setter methods, this should probably done only once during init.
switch.setter = getattr(self, switch.setter_name)
if switch.setter is None and switch.setter_name is not None:
switch.setter = getattr(self, switch.setter_name)
else:
# TODO: this is ugly, how to fix the issue where setter_name is optional and thus not acceptable for getattr?
raise Exception(
f"Neither setter or setter_name was defined for {switch}"
)

return switches

Expand Down
Loading