Skip to content

Commit

Permalink
Deprecate is_something attributes (#912)
Browse files Browse the repository at this point in the history
Deprecates the is_something attributes like is_bulb and is_dimmable in favour of the modular approach.
  • Loading branch information
sdb9696 committed May 13, 2024
1 parent 33d8398 commit ef49f44
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 93 deletions.
106 changes: 51 additions & 55 deletions kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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
Expand Down Expand Up @@ -208,61 +209,6 @@ def get_child_device(self, id_: str) -> Device:
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:
Expand Down Expand Up @@ -383,3 +329,53 @@ def __repr__(self):
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}")
20 changes: 10 additions & 10 deletions kasa/iot/iotbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,21 @@ async def _initialize_modules(self):

@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"])
Expand All @@ -252,7 +252,7 @@ def valid_temperature_range(self) -> ColorTempRange:
: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():
Expand Down Expand Up @@ -352,7 +352,7 @@ def hsv(self) -> HSV:
: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)
Expand All @@ -379,7 +379,7 @@ async def set_hsv(
: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):
Expand All @@ -406,7 +406,7 @@ async def set_hsv(
@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
Expand All @@ -421,7 +421,7 @@ async def set_color_temp(
: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
Expand All @@ -446,7 +446,7 @@ def _raise_for_invalid_brightness(self, value):
@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
Expand All @@ -461,7 +461,7 @@ async def set_brightness(
: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)
Expand Down
6 changes: 3 additions & 3 deletions kasa/iot/iotdimmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def brightness(self) -> int:
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
Expand All @@ -109,7 +109,7 @@ async def set_brightness(self, brightness: int, *, transition: int | None = None
: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):
Expand Down Expand Up @@ -218,7 +218,7 @@ async def set_fade_time(self, fade_type: FadeType, time: int):

@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
30 changes: 21 additions & 9 deletions kasa/iot/modules/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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
Expand Down Expand Up @@ -78,14 +79,19 @@ def query(self) -> dict:
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:
Expand All @@ -107,14 +113,14 @@ def is_color(self) -> bool:
"""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:
Expand All @@ -129,7 +135,7 @@ def hsv(self) -> HSV:
: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

Expand All @@ -150,7 +156,7 @@ async def set_hsv(
: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)

Expand All @@ -160,14 +166,18 @@ def valid_temperature_range(self) -> ColorTempRange:
: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

Expand All @@ -181,7 +191,9 @@ async def set_color_temp(
: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
Expand Down
11 changes: 0 additions & 11 deletions kasa/smart/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
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
Expand Down Expand Up @@ -444,11 +443,6 @@ def has_emeter(self) -> bool:
"""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."""
Expand Down Expand Up @@ -648,8 +642,3 @@ def _get_device_type_from_components(
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 []
1 change: 0 additions & 1 deletion kasa/tests/smart/modules/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions kasa/tests/test_bulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit ef49f44

Please sign in to comment.