Deprecate is_something attributes (#912)

Deprecates the is_something attributes like is_bulb and is_dimmable in favour of the modular approach.
This commit is contained in:
Steven B 2024-05-13 18:52:08 +01:00 committed by GitHub
parent 33d839866e
commit ef49f44eac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 142 additions and 93 deletions

View File

@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any, Mapping, Sequence
from warnings import warn
from .credentials import Credentials
from .device_type import DeviceType
@ -208,61 +209,6 @@ class Device(ABC):
def sys_info(self) -> dict[str, Any]:
"""Returns the device info."""
@property
def is_bulb(self) -> bool:
"""Return True if the device is a bulb."""
return self.device_type == DeviceType.Bulb
@property
def is_light_strip(self) -> bool:
"""Return True if the device is a led strip."""
return self.device_type == DeviceType.LightStrip
@property
def is_plug(self) -> bool:
"""Return True if the device is a plug."""
return self.device_type == DeviceType.Plug
@property
def is_wallswitch(self) -> bool:
"""Return True if the device is a switch."""
return self.device_type == DeviceType.WallSwitch
@property
def is_strip(self) -> bool:
"""Return True if the device is a strip."""
return self.device_type == DeviceType.Strip
@property
def is_strip_socket(self) -> bool:
"""Return True if the device is a strip socket."""
return self.device_type == DeviceType.StripSocket
@property
def is_dimmer(self) -> bool:
"""Return True if the device is a dimmer."""
return self.device_type == DeviceType.Dimmer
@property
def is_dimmable(self) -> bool:
"""Return True if the device is dimmable."""
return False
@property
def is_fan(self) -> bool:
"""Return True if the device is a fan."""
return self.device_type == DeviceType.Fan
@property
def is_variable_color_temp(self) -> bool:
"""Return True if the device supports color temperature."""
return False
@property
def is_color(self) -> bool:
"""Return True if the device supports color changes."""
return False
def get_plug_by_name(self, name: str) -> Device:
"""Return child device for the given name."""
for p in self.children:
@ -383,3 +329,53 @@ class Device(ABC):
if self._last_update is None:
return f"<{self.device_type} at {self.host} - update() needed>"
return f"<{self.device_type} at {self.host} - {self.alias} ({self.model})>"
_deprecated_attributes = {
# is_type
"is_bulb": (Module.Light, lambda self: self.device_type == DeviceType.Bulb),
"is_dimmer": (
Module.Light,
lambda self: self.device_type == DeviceType.Dimmer,
),
"is_light_strip": (
Module.LightEffect,
lambda self: self.device_type == DeviceType.LightStrip,
),
"is_plug": (Module.Led, lambda self: self.device_type == DeviceType.Plug),
"is_wallswitch": (
Module.Led,
lambda self: self.device_type == DeviceType.WallSwitch,
),
"is_strip": (None, lambda self: self.device_type == DeviceType.Strip),
"is_strip_socket": (
None,
lambda self: self.device_type == DeviceType.StripSocket,
), # TODO
# is_light_function
"is_color": (
Module.Light,
lambda self: Module.Light in self.modules
and self.modules[Module.Light].is_color,
),
"is_dimmable": (
Module.Light,
lambda self: Module.Light in self.modules
and self.modules[Module.Light].is_dimmable,
),
"is_variable_color_temp": (
Module.Light,
lambda self: Module.Light in self.modules
and self.modules[Module.Light].is_variable_color_temp,
),
}
def __getattr__(self, name) -> bool:
if name in self._deprecated_attributes:
module = self._deprecated_attributes[name][0]
func = self._deprecated_attributes[name][1]
msg = f"{name} is deprecated"
if module:
msg += f", use: {module} in device.modules instead"
warn(msg, DeprecationWarning, stacklevel=1)
return func(self)
raise AttributeError(f"Device has no attribute {name!r}")

View File

@ -226,21 +226,21 @@ class IotBulb(IotDevice):
@property # type: ignore
@requires_update
def is_color(self) -> bool:
def _is_color(self) -> bool:
"""Whether the bulb supports color changes."""
sys_info = self.sys_info
return bool(sys_info["is_color"])
@property # type: ignore
@requires_update
def is_dimmable(self) -> bool:
def _is_dimmable(self) -> bool:
"""Whether the bulb supports brightness changes."""
sys_info = self.sys_info
return bool(sys_info["is_dimmable"])
@property # type: ignore
@requires_update
def is_variable_color_temp(self) -> bool:
def _is_variable_color_temp(self) -> bool:
"""Whether the bulb supports color temperature changes."""
sys_info = self.sys_info
return bool(sys_info["is_variable_color_temp"])
@ -252,7 +252,7 @@ class IotBulb(IotDevice):
:return: White temperature range in Kelvin (minimum, maximum)
"""
if not self.is_variable_color_temp:
if not self._is_variable_color_temp:
raise KasaException("Color temperature not supported")
for model, temp_range in TPLINK_KELVIN.items():
@ -352,7 +352,7 @@ class IotBulb(IotDevice):
:return: hue, saturation and value (degrees, %, %)
"""
if not self.is_color:
if not self._is_color:
raise KasaException("Bulb does not support color.")
light_state = cast(dict, self.light_state)
@ -379,7 +379,7 @@ class IotBulb(IotDevice):
:param int value: value in percentage [0, 100]
:param int transition: transition in milliseconds.
"""
if not self.is_color:
if not self._is_color:
raise KasaException("Bulb does not support color.")
if not isinstance(hue, int) or not (0 <= hue <= 360):
@ -406,7 +406,7 @@ class IotBulb(IotDevice):
@requires_update
def color_temp(self) -> int:
"""Return color temperature of the device in kelvin."""
if not self.is_variable_color_temp:
if not self._is_variable_color_temp:
raise KasaException("Bulb does not support colortemp.")
light_state = self.light_state
@ -421,7 +421,7 @@ class IotBulb(IotDevice):
:param int temp: The new color temperature, in Kelvin
:param int transition: transition in milliseconds.
"""
if not self.is_variable_color_temp:
if not self._is_variable_color_temp:
raise KasaException("Bulb does not support colortemp.")
valid_temperature_range = self.valid_temperature_range
@ -446,7 +446,7 @@ class IotBulb(IotDevice):
@requires_update
def brightness(self) -> int:
"""Return the current brightness in percentage."""
if not self.is_dimmable: # pragma: no cover
if not self._is_dimmable: # pragma: no cover
raise KasaException("Bulb is not dimmable.")
light_state = self.light_state
@ -461,7 +461,7 @@ class IotBulb(IotDevice):
:param int brightness: brightness in percent
:param int transition: transition in milliseconds.
"""
if not self.is_dimmable: # pragma: no cover
if not self._is_dimmable: # pragma: no cover
raise KasaException("Bulb is not dimmable.")
self._raise_for_invalid_brightness(brightness)

View File

@ -96,7 +96,7 @@ class IotDimmer(IotPlug):
Will return a range between 0 - 100.
"""
if not self.is_dimmable:
if not self._is_dimmable:
raise KasaException("Device is not dimmable.")
sys_info = self.sys_info
@ -109,7 +109,7 @@ class IotDimmer(IotPlug):
:param int transition: transition duration in milliseconds.
Using a transition will cause the dimmer to turn on.
"""
if not self.is_dimmable:
if not self._is_dimmable:
raise KasaException("Device is not dimmable.")
if not isinstance(brightness, int):
@ -218,7 +218,7 @@ class IotDimmer(IotPlug):
@property # type: ignore
@requires_update
def is_dimmable(self) -> bool:
def _is_dimmable(self) -> bool:
"""Whether the switch supports brightness changes."""
sys_info = self.sys_info
return "brightness" in sys_info

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, cast
from ...device_type import DeviceType
from ...exceptions import KasaException
from ...feature import Feature
from ...interfaces.light import HSV, ColorTempRange
@ -78,14 +79,19 @@ class Light(IotModule, LightInterface):
return {}
def _get_bulb_device(self) -> IotBulb | None:
if self._device.is_bulb or self._device.is_light_strip:
"""For type checker this gets an IotBulb.
IotDimmer is not a subclass of IotBulb and using isinstance
here at runtime would create a circular import.
"""
if self._device.device_type in {DeviceType.Bulb, DeviceType.LightStrip}:
return cast("IotBulb", self._device)
return None
@property # type: ignore
def is_dimmable(self) -> int:
"""Whether the bulb supports brightness changes."""
return self._device.is_dimmable
return self._device._is_dimmable
@property # type: ignore
def brightness(self) -> int:
@ -107,14 +113,14 @@ class Light(IotModule, LightInterface):
"""Whether the light supports color changes."""
if (bulb := self._get_bulb_device()) is None:
return False
return bulb.is_color
return bulb._is_color
@property
def is_variable_color_temp(self) -> bool:
"""Whether the bulb supports color temperature changes."""
if (bulb := self._get_bulb_device()) is None:
return False
return bulb.is_variable_color_temp
return bulb._is_variable_color_temp
@property
def has_effects(self) -> bool:
@ -129,7 +135,7 @@ class Light(IotModule, LightInterface):
:return: hue, saturation and value (degrees, %, %)
"""
if (bulb := self._get_bulb_device()) is None or not bulb.is_color:
if (bulb := self._get_bulb_device()) is None or not bulb._is_color:
raise KasaException("Light does not support color.")
return bulb.hsv
@ -150,7 +156,7 @@ class Light(IotModule, LightInterface):
:param int value: value in percentage [0, 100]
:param int transition: transition in milliseconds.
"""
if (bulb := self._get_bulb_device()) is None or not bulb.is_color:
if (bulb := self._get_bulb_device()) is None or not bulb._is_color:
raise KasaException("Light does not support color.")
return await bulb.set_hsv(hue, saturation, value, transition=transition)
@ -160,14 +166,18 @@ class Light(IotModule, LightInterface):
:return: White temperature range in Kelvin (minimum, maximum)
"""
if (bulb := self._get_bulb_device()) is None or not bulb.is_variable_color_temp:
if (
bulb := self._get_bulb_device()
) is None or not bulb._is_variable_color_temp:
raise KasaException("Light does not support colortemp.")
return bulb.valid_temperature_range
@property
def color_temp(self) -> int:
"""Whether the bulb supports color temperature changes."""
if (bulb := self._get_bulb_device()) is None or not bulb.is_variable_color_temp:
if (
bulb := self._get_bulb_device()
) is None or not bulb._is_variable_color_temp:
raise KasaException("Light does not support colortemp.")
return bulb.color_temp
@ -181,7 +191,9 @@ class Light(IotModule, LightInterface):
:param int temp: The new color temperature, in Kelvin
:param int transition: transition in milliseconds.
"""
if (bulb := self._get_bulb_device()) is None or not bulb.is_variable_color_temp:
if (
bulb := self._get_bulb_device()
) is None or not bulb._is_variable_color_temp:
raise KasaException("Light does not support colortemp.")
return await bulb.set_color_temp(
temp, brightness=brightness, transition=transition

View File

@ -14,7 +14,6 @@ from ..deviceconfig import DeviceConfig
from ..emeterstatus import EmeterStatus
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
from ..feature import Feature
from ..interfaces.light import LightPreset
from ..module import Module
from ..modulemapping import ModuleMapping, ModuleName
from ..smartprotocol import SmartProtocol
@ -444,11 +443,6 @@ class SmartDevice(Device):
"""Return if the device has emeter."""
return Module.Energy in self.modules
@property
def is_dimmer(self) -> bool:
"""Whether the device acts as a dimmer."""
return self.is_dimmable
@property
def is_on(self) -> bool:
"""Return true if the device is on."""
@ -648,8 +642,3 @@ class SmartDevice(Device):
return DeviceType.Thermostat
_LOGGER.warning("Unknown device type, falling back to plug")
return DeviceType.Plug
@property
def presets(self) -> list[LightPreset]:
"""Return a list of available bulb setting presets."""
return []

View File

@ -58,7 +58,6 @@ async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
fan = dev.modules.get(Module.Fan)
assert fan
device = fan._device
assert device.is_fan
await fan.set_fan_speed_level(1)
await dev.update()

View File

@ -208,7 +208,7 @@ async def test_non_variable_temp(dev: Device):
async def test_dimmable_brightness(dev: IotBulb, turn_on):
assert isinstance(dev, (IotBulb, IotDimmer))
await handle_turn_on(dev, turn_on)
assert dev.is_dimmable
assert dev._is_dimmable
await dev.set_brightness(50)
await dev.update()
@ -244,7 +244,7 @@ async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
@dimmable_iot
async def test_invalid_brightness(dev: IotBulb):
assert dev.is_dimmable
assert dev._is_dimmable
with pytest.raises(ValueError):
await dev.set_brightness(110)
@ -255,7 +255,7 @@ async def test_invalid_brightness(dev: IotBulb):
@non_dimmable_iot
async def test_non_dimmable(dev: IotBulb):
assert not dev.is_dimmable
assert not dev._is_dimmable
with pytest.raises(KasaException):
assert dev.brightness == 0

View File

@ -9,7 +9,7 @@ from unittest.mock import Mock, patch
import pytest
import kasa
from kasa import Credentials, Device, DeviceConfig
from kasa import Credentials, Device, DeviceConfig, DeviceType
from kasa.iot import IotDevice
from kasa.smart import SmartChildDevice, SmartDevice
@ -121,3 +121,56 @@ def test_deprecated_exceptions(exceptions_class, use_class):
with pytest.deprecated_call(match=msg):
getattr(kasa, exceptions_class)
getattr(kasa, use_class.__name__)
deprecated_is_device_type = {
"is_bulb": DeviceType.Bulb,
"is_plug": DeviceType.Plug,
"is_dimmer": DeviceType.Dimmer,
"is_light_strip": DeviceType.LightStrip,
"is_wallswitch": DeviceType.WallSwitch,
"is_strip": DeviceType.Strip,
"is_strip_socket": DeviceType.StripSocket,
}
deprecated_is_light_function_smart_module = {
"is_color": "Color",
"is_dimmable": "Brightness",
"is_variable_color_temp": "ColorTemperature",
}
def test_deprecated_attributes(dev: SmartDevice):
"""Test deprecated attributes on all devices."""
tested_keys = set()
def _test_attr(attribute):
tested_keys.add(attribute)
msg = f"{attribute} is deprecated"
if module := Device._deprecated_attributes[attribute][0]:
msg += f", use: {module} in device.modules instead"
with pytest.deprecated_call(match=msg):
val = getattr(dev, attribute)
return val
for attribute in deprecated_is_device_type:
val = _test_attr(attribute)
expected_val = dev.device_type == deprecated_is_device_type[attribute]
assert val == expected_val
for attribute in deprecated_is_light_function_smart_module:
val = _test_attr(attribute)
if isinstance(dev, SmartDevice):
expected_val = (
deprecated_is_light_function_smart_module[attribute] in dev.modules
)
elif hasattr(dev, f"_{attribute}"):
expected_val = getattr(dev, f"_{attribute}")
else:
expected_val = False
assert val == expected_val
assert len(tested_keys) == len(Device._deprecated_attributes)
untested_keys = [
key for key in Device._deprecated_attributes if key not in tested_keys
]
assert len(untested_keys) == 0