-
-
Notifications
You must be signed in to change notification settings - Fork 569
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement introspectable sensors (#1488)
- Loading branch information
Showing
8 changed files
with
293 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
"""This module contains integration descriptors. | ||
These can be used to make specifically interesting pieces of functionality | ||
visible to downstream users. | ||
TBD: Some descriptors are created automatically based on the status container classes, | ||
but developers can override :func:buttons(), :func:sensors(), .. to expose more features. | ||
""" | ||
from dataclasses import dataclass | ||
from enum import Enum, auto | ||
from typing import Callable, Dict, List, Optional | ||
|
||
|
||
@dataclass | ||
class ButtonDescriptor: | ||
id: str | ||
name: str | ||
method: Callable | ||
extras: Optional[Dict] = None | ||
|
||
|
||
@dataclass | ||
class SensorDescriptor: | ||
"""Describes a sensor exposed by the device. | ||
This information can be used by library users to programatically | ||
access information what types of data is available to display to users. | ||
Prefer :meth:`@sensor <miio.devicestatus.sensor>` for constructing these. | ||
""" | ||
|
||
id: str | ||
type: str | ||
name: str | ||
property: str | ||
unit: Optional[str] = None | ||
extras: Optional[Dict] = None | ||
|
||
|
||
@dataclass | ||
class SwitchDescriptor: | ||
"""Presents toggleable switch.""" | ||
|
||
id: str | ||
name: str | ||
property: str | ||
setter: Callable | ||
|
||
|
||
@dataclass | ||
class SettingDescriptor: | ||
"""Presents a settable value.""" | ||
|
||
id: str | ||
name: str | ||
property: str | ||
setter: Callable | ||
unit: str | ||
|
||
|
||
class SettingType(Enum): | ||
Number = auto() | ||
Boolean = auto() | ||
Enum = auto() | ||
|
||
|
||
@dataclass | ||
class EnumSettingDescriptor(SettingDescriptor): | ||
"""Presents a settable, enum-based value.""" | ||
|
||
choices: List | ||
type: SettingType = SettingType.Enum | ||
extras: Optional[Dict] = None | ||
|
||
|
||
@dataclass | ||
class NumberSettingDescriptor(SettingDescriptor): | ||
"""Presents a settable, numerical value.""" | ||
|
||
min_value: int | ||
max_value: int | ||
step: int | ||
type: SettingType = SettingType.Number | ||
extras: Optional[Dict] = None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import inspect | ||
import logging | ||
import warnings | ||
from typing import Dict, Union, get_args, get_origin, get_type_hints | ||
|
||
from .descriptors import SensorDescriptor | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class _StatusMeta(type): | ||
"""Meta class to provide introspectable properties.""" | ||
|
||
def __new__(metacls, name, bases, namespace, **kwargs): | ||
cls = super().__new__(metacls, name, bases, namespace) | ||
cls._sensors: Dict[str, SensorDescriptor] = {} | ||
for n in namespace: | ||
prop = getattr(namespace[n], "fget", None) | ||
if prop: | ||
sensor = getattr(prop, "_sensor", None) | ||
if sensor: | ||
_LOGGER.debug(f"Found sensor: {sensor} for {name}") | ||
cls._sensors[n] = sensor | ||
|
||
return cls | ||
|
||
|
||
class DeviceStatus(metaclass=_StatusMeta): | ||
"""Base class for status containers. | ||
All status container classes should inherit from this class: | ||
* This class allows downstream users to access the available information in an | ||
introspectable way. | ||
* The __repr__ implementation returns all defined properties and their values. | ||
""" | ||
|
||
def __repr__(self): | ||
props = inspect.getmembers(self.__class__, lambda o: isinstance(o, property)) | ||
|
||
s = f"<{self.__class__.__name__}" | ||
for prop_tuple in props: | ||
name, prop = prop_tuple | ||
try: | ||
# ignore deprecation warnings | ||
with warnings.catch_warnings(record=True): | ||
prop_value = prop.fget(self) | ||
except Exception as ex: | ||
prop_value = ex.__class__.__name__ | ||
|
||
s += f" {name}={prop_value}" | ||
s += ">" | ||
return s | ||
|
||
def sensors(self) -> Dict[str, SensorDescriptor]: | ||
"""Return the dict of sensors exposed by the status container. | ||
You can use @sensor decorator to define sensors inside your status class. | ||
""" | ||
return self._sensors # type: ignore[attr-defined] | ||
|
||
|
||
def sensor(*, name: str, unit: str = "", **kwargs): | ||
"""Syntactic sugar to create SensorDescriptor objects. | ||
The information can be used by users of the library to programatically find out what | ||
types of sensors 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.SensorDescriptor.extras`, | ||
and can be interpreted downstream users as they wish. | ||
""" | ||
|
||
def decorator_sensor(func): | ||
property_name = func.__name__ | ||
|
||
def _sensor_type_for_return_type(func): | ||
rtype = get_type_hints(func).get("return") | ||
if get_origin(rtype) is Union: # Unwrap Optional[] | ||
rtype, _ = get_args(rtype) | ||
|
||
if rtype == bool: | ||
return "binary" | ||
else: | ||
return "sensor" | ||
|
||
sensor_type = _sensor_type_for_return_type(func) | ||
descriptor = SensorDescriptor( | ||
id=str(property_name), | ||
property=str(property_name), | ||
name=name, | ||
unit=unit, | ||
type=sensor_type, | ||
extras=kwargs, | ||
) | ||
func._sensor = descriptor | ||
|
||
return func | ||
|
||
return decorator_sensor |
Oops, something went wrong.