Skip to content

Commit

Permalink
Remove SwitchDescriptor in favor of BooleanSettingDescriptor (#1566)
Browse files Browse the repository at this point in the history
New `BooleanSettingDescriptor` allows defining boolean settings, so
having
a separate handling for switches is unnecessary and just complicates the
code.
  • Loading branch information
rytilahti authored Nov 1, 2022
1 parent 3a2d1b2 commit 93a4a7c
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 146 deletions.
49 changes: 27 additions & 22 deletions miio/descriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
If needed, you can override the methods listed to add more descriptors to your integration.
"""
from enum import Enum, auto
from typing import Callable, Dict, Optional
from typing import Callable, Dict, Optional, Type

import attr

Expand All @@ -22,9 +22,9 @@ class ButtonDescriptor:

id: str
name: str
method_name: str
method_name: Optional[str] = None
method: Optional[Callable] = None
extras: Optional[Dict] = None
extras: Optional[Dict] = attr.ib(default={})


@attr.s(auto_attribs=True)
Expand All @@ -42,19 +42,14 @@ class SensorDescriptor:
name: str
property: str
unit: Optional[str] = None
extras: Optional[Dict] = None
extras: Optional[Dict] = attr.ib(default={})


@attr.s(auto_attribs=True)
class SwitchDescriptor:
"""Presents toggleable switch."""

id: str
name: str
property: str
setter_name: Optional[str] = None
setter: Optional[Callable] = None
extras: Optional[Dict] = None
class SettingType(Enum):
Undefined = auto()
Number = auto()
Boolean = auto()
Enum = auto()


@attr.s(auto_attribs=True, kw_only=True)
Expand All @@ -64,15 +59,27 @@ class SettingDescriptor:
id: str
name: str
property: str
unit: str
unit: Optional[str] = None
type = SettingType.Undefined
setter: Optional[Callable] = None
setter_name: Optional[str] = None
extras: Optional[Dict] = attr.ib(default={})

def cast_value(self, value):
"""Casts value to the expected type."""
cast_map = {
SettingType.Boolean: bool,
SettingType.Enum: int,
SettingType.Number: int,
}
return cast_map[self.type](int(value))

class SettingType(Enum):
Number = auto()
Boolean = auto()
Enum = auto()

@attr.s(auto_attribs=True, kw_only=True)
class BooleanSettingDescriptor(SettingDescriptor):
"""Presents a settable boolean value."""

type: SettingType = SettingType.Boolean


@attr.s(auto_attribs=True, kw_only=True)
Expand All @@ -81,8 +88,7 @@ class EnumSettingDescriptor(SettingDescriptor):

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


@attr.s(auto_attribs=True, kw_only=True)
Expand All @@ -93,4 +99,3 @@ class NumberSettingDescriptor(SettingDescriptor):
max_value: int
step: int
type: SettingType = SettingType.Number
extras: Optional[Dict] = None
22 changes: 1 addition & 21 deletions miio/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
import click

from .click_common import DeviceGroupMeta, LiteralParamType, command, format_output
from .descriptors import (
ButtonDescriptor,
SensorDescriptor,
SettingDescriptor,
SwitchDescriptor,
)
from .descriptors import ButtonDescriptor, SensorDescriptor, SettingDescriptor
from .deviceinfo import DeviceInfo
from .devicestatus import DeviceStatus
from .exceptions import DeviceInfoUnavailableException, PayloadDecodeException
Expand Down Expand Up @@ -272,20 +267,5 @@ def sensors(self) -> Dict[str, SensorDescriptor]:
sensors = self.status().sensors()
return sensors

def switches(self) -> Dict[str, SwitchDescriptor]:
"""Return toggleable switches."""
switches = self.status().switches()
for switch in switches.values():
# TODO: Bind setter methods, this should probably done only once during init.
if switch.setter is None:
if switch.setter_name is None:
# 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}"
)
switch.setter = getattr(self, switch.setter_name)

return switches

def __repr__(self):
return f"<{self.__class__.__name__ }: {self.ip} (token: {self.token})>"
77 changes: 17 additions & 60 deletions miio/devicestatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
)

from .descriptors import (
BooleanSettingDescriptor,
EnumSettingDescriptor,
NumberSettingDescriptor,
SensorDescriptor,
SettingDescriptor,
SwitchDescriptor,
SettingType,
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -32,14 +33,12 @@ def __new__(metacls, name, bases, namespace, **kwargs):

# TODO: clean up to contain all of these in a single container
cls._sensors: Dict[str, SensorDescriptor] = {}
cls._switches: Dict[str, SwitchDescriptor] = {}
cls._settings: Dict[str, SettingDescriptor] = {}

cls._embedded: Dict[str, "DeviceStatus"] = {}

descriptor_map = {
"sensor": cls._sensors,
"switch": cls._switches,
"setting": cls._settings,
}
for n in namespace:
Expand Down Expand Up @@ -93,17 +92,10 @@ def sensors(self) -> Dict[str, SensorDescriptor]:
"""
return self._sensors # type: ignore[attr-defined]

def switches(self) -> Dict[str, SwitchDescriptor]:
"""Return the dict of sensors exposed by the status container.
You can use @sensor decorator to define sensors inside your status class.
"""
return self._switches # type: ignore[attr-defined]

def settings(self) -> Dict[str, SettingDescriptor]:
"""Return the dict of settings exposed by the status container.
You can use @setting decorator to define sensors inside your status class.
You can use @setting decorator to define settings inside your status class.
"""
return self._settings # type: ignore[attr-defined]

Expand All @@ -126,10 +118,6 @@ def embed(self, other: "DeviceStatus"):

self._sensors[final_name] = attr.evolve(sensor, property=final_name)

for name, switch in other.switches().items():
final_name = f"{other_name}:{name}"
self._switches[final_name] = attr.evolve(switch, property=final_name)

for name, setting in other.settings().items():
final_name = f"{other_name}:{name}"
self._settings[final_name] = attr.evolve(setting, property=final_name)
Expand Down Expand Up @@ -183,34 +171,6 @@ def _sensor_type_for_return_type(func):
return decorator_sensor


def switch(name: str, *, setter_name: str, **kwargs):
"""Syntactic sugar to create SwitchDescriptor objects.
The information can be used by users of the library to programmatically 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.SwitchDescriptor.extras`,
and can be interpreted downstream users as they wish.
"""

def decorator_sensor(func):
property_name = func.__name__

descriptor = SwitchDescriptor(
id=str(property_name),
property=str(property_name),
name=name,
setter_name=setter_name,
extras=kwargs,
)
func._switch = descriptor

return func

return decorator_sensor


def setting(
name: str,
*,
Expand All @@ -222,6 +182,7 @@ def setting(
step: Optional[int] = None,
choices: Optional[Type[Enum]] = None,
choices_attribute: Optional[str] = None,
type: Optional[SettingType] = None,
**kwargs,
):
"""Syntactic sugar to create SettingDescriptor objects.
Expand All @@ -240,39 +201,35 @@ def decorator_setting(func):
if setter is None and setter_name is None:
raise Exception("Either setter or setter_name needs to be defined")

common_values = {
"id": str(property_name),
"property": str(property_name),
"name": name,
"unit": unit,
"setter": setter,
"setter_name": setter_name,
"extras": kwargs,
}

if min_value or max_value:
descriptor = NumberSettingDescriptor(
id=str(property_name),
property=str(property_name),
name=name,
unit=unit,
setter=setter,
setter_name=setter_name,
**common_values,
min_value=min_value or 0,
max_value=max_value,
step=step or 1,
extras=kwargs,
)
elif choices or choices_attribute:
if choices_attribute is not None:
# TODO: adding choices from attribute is a bit more complex, as it requires a way to
# construct enums pointed by the attribute
raise NotImplementedError("choices_attribute is not yet implemented")
descriptor = EnumSettingDescriptor(
id=str(property_name),
property=str(property_name),
name=name,
unit=unit,
setter=setter,
setter_name=setter_name,
**common_values,
choices=choices,
choices_attribute=choices_attribute,
extras=kwargs,
)
else:
raise Exception(
"Neither {min,max}_value or choices_{attribute} was defined"
)
descriptor = BooleanSettingDescriptor(**common_values)

func._setting = descriptor

Expand Down
12 changes: 6 additions & 6 deletions miio/integrations/fan/zhimi/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from miio import Device, DeviceStatus
from miio.click_common import EnumType, command, format_output
from miio.devicestatus import sensor, setting, switch
from miio.devicestatus import sensor, setting
from miio.fan_common import LedBrightness, MoveDirection

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -82,7 +82,7 @@ def power(self) -> str:
return self.data["power"]

@property
@switch("Power", setter_name="set_power")
@setting("Power", setter_name="set_power")
def is_on(self) -> bool:
"""True if device is currently on."""
return self.power == "on"
Expand All @@ -104,7 +104,7 @@ def temperature(self) -> Optional[float]:
return None

@property
@switch("LED", setter_name="set_led")
@setting("LED", setter_name="set_led")
def led(self) -> Optional[bool]:
"""True if LED is turned on, if available."""
if "led" in self.data and self.data["led"] is not None:
Expand All @@ -120,13 +120,13 @@ def led_brightness(self) -> Optional[LedBrightness]:
return None

@property
@switch("Buzzer", setter_name="set_buzzer")
@setting("Buzzer", setter_name="set_buzzer")
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] in ["on", 1, 2]

@property
@switch("Child Lock", setter_name="set_child_lock")
@setting("Child Lock", setter_name="set_child_lock")
def child_lock(self) -> bool:
"""True if child lock is on."""
return self.data["child_lock"] == "on"
Expand All @@ -148,7 +148,7 @@ def direct_speed(self) -> Optional[int]:
return None

@property
@switch("Oscillate", setter_name="set_oscillate")
@setting("Oscillate", setter_name="set_oscillate")
def oscillate(self) -> bool:
"""True if oscillation is enabled."""
return self.data["angle_enable"] == "on"
Expand Down
8 changes: 4 additions & 4 deletions miio/integrations/humidifier/zhimi/airhumidifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from miio import Device, DeviceError, DeviceInfo, DeviceStatus
from miio.click_common import EnumType, command, format_output
from miio.devicestatus import sensor, setting, switch
from miio.devicestatus import sensor, setting

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -112,7 +112,7 @@ def humidity(self) -> int:
return self.data["humidity"]

@property
@switch(
@setting(
name="Buzzer",
icon="mdi:volume-high",
setter_name="set_buzzer",
Expand All @@ -138,7 +138,7 @@ def led_brightness(self) -> Optional[LedBrightness]:
return None

@property
@switch(
@setting(
name="Child Lock",
icon="mdi:lock",
setter_name="set_child_lock",
Expand Down Expand Up @@ -279,7 +279,7 @@ def water_tank_detached(self) -> Optional[bool]:
return None

@property
@switch(
@setting(
name="Dry Mode",
icon="mdi:hair-dryer",
setter_name="set_dry",
Expand Down
Loading

0 comments on commit 93a4a7c

Please sign in to comment.