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

Remove SwitchDescriptor in favor of BooleanSettingDescriptor #1566

Merged
merged 2 commits into from
Nov 1, 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
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