mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
Migrate Light module to feature based capability checks
This commit is contained in:
parent
c5830a4cdc
commit
8bfddbdd71
@ -52,9 +52,9 @@ True
|
|||||||
>>> await dev.update()
|
>>> await dev.update()
|
||||||
>>> light.brightness
|
>>> light.brightness
|
||||||
50
|
50
|
||||||
>>> light.is_color
|
>>> light.has_feature(light.set_hsv)
|
||||||
True
|
True
|
||||||
>>> if light.is_color:
|
>>> if light.has_feature(light.set_hsv):
|
||||||
>>> print(light.hsv)
|
>>> print(light.hsv)
|
||||||
HSV(hue=0, saturation=100, value=50)
|
HSV(hue=0, saturation=100, value=50)
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, tzinfo
|
from datetime import datetime, tzinfo
|
||||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||||
@ -525,10 +525,47 @@ class Device(ABC):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_deprecated_callable_attribute(self, name: str) -> Any | None:
|
||||||
|
vals: dict[str, tuple[ModuleName, Callable[[Any], Any], str]] = {
|
||||||
|
"is_dimmable": (
|
||||||
|
Module.Light,
|
||||||
|
lambda c: c.has_feature("set_brightness"),
|
||||||
|
"light_module.has_feature('set_brightness')",
|
||||||
|
),
|
||||||
|
"is_color": (
|
||||||
|
Module.Light,
|
||||||
|
lambda c: c.has_feature("set_hsv"),
|
||||||
|
"light_module.has_feature('set_hsv')",
|
||||||
|
),
|
||||||
|
"is_variable_color_temp": (
|
||||||
|
Module.Light,
|
||||||
|
lambda c: c.has_feature("set_color_temp"),
|
||||||
|
"light_module.has_feature('set_color_temp')",
|
||||||
|
),
|
||||||
|
"valid_temperature_range": (
|
||||||
|
Module.Light,
|
||||||
|
lambda c: c._deprecated_valid_temperature_range(),
|
||||||
|
"minimum and maximum value of get_feature('set_color_temp')",
|
||||||
|
),
|
||||||
|
"has_effects": (
|
||||||
|
Module.Light,
|
||||||
|
lambda c: Module.LightEffect in c._device.modules,
|
||||||
|
"Module.LightEffect in c._device.modules",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if mod_call_msg := vals.get(name):
|
||||||
|
mod, call, msg = mod_call_msg
|
||||||
|
msg = f"{name} is deprecated, use: {msg} instead"
|
||||||
|
warn(msg, DeprecationWarning, stacklevel=2)
|
||||||
|
if (module := self.modules.get(mod)) is None:
|
||||||
|
raise AttributeError(f"Device has no attribute {name!r}")
|
||||||
|
return call(module)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
_deprecated_other_attributes = {
|
_deprecated_other_attributes = {
|
||||||
# light attributes
|
# light attributes
|
||||||
"is_color": (Module.Light, ["is_color"]),
|
"is_color": (Module.Light, ["is_color"]),
|
||||||
"is_dimmable": (Module.Light, ["is_dimmable"]),
|
|
||||||
"is_variable_color_temp": (Module.Light, ["is_variable_color_temp"]),
|
"is_variable_color_temp": (Module.Light, ["is_variable_color_temp"]),
|
||||||
"brightness": (Module.Light, ["brightness"]),
|
"brightness": (Module.Light, ["brightness"]),
|
||||||
"set_brightness": (Module.Light, ["set_brightness"]),
|
"set_brightness": (Module.Light, ["set_brightness"]),
|
||||||
@ -536,8 +573,6 @@ class Device(ABC):
|
|||||||
"set_hsv": (Module.Light, ["set_hsv"]),
|
"set_hsv": (Module.Light, ["set_hsv"]),
|
||||||
"color_temp": (Module.Light, ["color_temp"]),
|
"color_temp": (Module.Light, ["color_temp"]),
|
||||||
"set_color_temp": (Module.Light, ["set_color_temp"]),
|
"set_color_temp": (Module.Light, ["set_color_temp"]),
|
||||||
"valid_temperature_range": (Module.Light, ["valid_temperature_range"]),
|
|
||||||
"has_effects": (Module.Light, ["has_effects"]),
|
|
||||||
"_deprecated_set_light_state": (Module.Light, ["has_effects"]),
|
"_deprecated_set_light_state": (Module.Light, ["has_effects"]),
|
||||||
# led attributes
|
# led attributes
|
||||||
"led": (Module.Led, ["led"]),
|
"led": (Module.Led, ["led"]),
|
||||||
@ -576,6 +611,9 @@ class Device(ABC):
|
|||||||
msg = f"{name} is deprecated, use device_type property instead"
|
msg = f"{name} is deprecated, use device_type property instead"
|
||||||
warn(msg, DeprecationWarning, stacklevel=2)
|
warn(msg, DeprecationWarning, stacklevel=2)
|
||||||
return self.device_type == dep_device_type_attr[1]
|
return self.device_type == dep_device_type_attr[1]
|
||||||
|
# callable
|
||||||
|
if result := self._get_deprecated_callable_attributes(name) is not None:
|
||||||
|
return result
|
||||||
# Other deprecated attributes
|
# Other deprecated attributes
|
||||||
if (dep_attr := self._deprecated_other_attributes.get(name)) and (
|
if (dep_attr := self._deprecated_other_attributes.get(name)) and (
|
||||||
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
|
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
|
||||||
|
@ -25,11 +25,11 @@ Get the light module to interact:
|
|||||||
|
|
||||||
You can use the ``is_``-prefixed properties to check for supported features:
|
You can use the ``is_``-prefixed properties to check for supported features:
|
||||||
|
|
||||||
>>> light.is_dimmable
|
>>> light.has_feature(light.set_brightness)
|
||||||
True
|
True
|
||||||
>>> light.is_color
|
>>> light.has_feature(light.set_hsv)
|
||||||
True
|
True
|
||||||
>>> light.is_variable_color_temp
|
>>> light.has_feature(light.set_color_temp)
|
||||||
True
|
True
|
||||||
|
|
||||||
All known bulbs support changing the brightness:
|
All known bulbs support changing the brightness:
|
||||||
@ -43,8 +43,9 @@ All known bulbs support changing the brightness:
|
|||||||
|
|
||||||
Bulbs supporting color temperature can be queried for the supported range:
|
Bulbs supporting color temperature can be queried for the supported range:
|
||||||
|
|
||||||
>>> light.valid_temperature_range
|
>>> if color_temp_feature := light.get_feature(light.set_color_temp):
|
||||||
ColorTempRange(min=2500, max=6500)
|
>>> print(f"{color_temp_feature.minimum_value}, {color_temp_feature.maximum_value}")
|
||||||
|
2500, 6500
|
||||||
>>> await light.set_color_temp(3000)
|
>>> await light.set_color_temp(3000)
|
||||||
>>> await dev.update()
|
>>> await dev.update()
|
||||||
>>> light.color_temp
|
>>> light.color_temp
|
||||||
@ -63,10 +64,13 @@ HSV(hue=180, saturation=100, value=80)
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import NamedTuple
|
from typing import Annotated, Any, NamedTuple
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from ..module import Module
|
from ..exceptions import KasaException
|
||||||
|
from ..module import FeatureAttribute, Module
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -101,35 +105,7 @@ class Light(Module, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_dimmable(self) -> bool:
|
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
|
||||||
"""Whether the light supports brightness changes."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def is_color(self) -> bool:
|
|
||||||
"""Whether the bulb supports color changes."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def is_variable_color_temp(self) -> bool:
|
|
||||||
"""Whether the bulb supports color temperature changes."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def valid_temperature_range(self) -> ColorTempRange:
|
|
||||||
"""Return the device-specific white temperature range (in Kelvin).
|
|
||||||
|
|
||||||
:return: White temperature range in Kelvin (minimum, maximum)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def has_effects(self) -> bool:
|
|
||||||
"""Return True if the device supports effects."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def hsv(self) -> HSV:
|
|
||||||
"""Return the current HSV state of the bulb.
|
"""Return the current HSV state of the bulb.
|
||||||
|
|
||||||
:return: hue, saturation and value (degrees, %, %)
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
@ -137,12 +113,12 @@ class Light(Module, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def color_temp(self) -> int:
|
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
|
||||||
"""Whether the bulb supports color temperature changes."""
|
"""Whether the bulb supports color temperature changes."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def brightness(self) -> int:
|
def brightness(self) -> Annotated[int, FeatureAttribute()]:
|
||||||
"""Return the current brightness in percentage."""
|
"""Return the current brightness in percentage."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -153,7 +129,7 @@ class Light(Module, ABC):
|
|||||||
value: int | None = None,
|
value: int | None = None,
|
||||||
*,
|
*,
|
||||||
transition: int | None = None,
|
transition: int | None = None,
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set new HSV.
|
"""Set new HSV.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -167,7 +143,7 @@ class Light(Module, ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def set_color_temp(
|
async def set_color_temp(
|
||||||
self, temp: int, *, brightness: int | None = None, transition: int | None = None
|
self, temp: int, *, brightness: int | None = None, transition: int | None = None
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set the color temperature of the device in kelvin.
|
"""Set the color temperature of the device in kelvin.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -179,7 +155,7 @@ class Light(Module, ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def set_brightness(
|
async def set_brightness(
|
||||||
self, brightness: int, *, transition: int | None = None
|
self, brightness: int, *, transition: int | None = None
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set the brightness in percentage.
|
"""Set the brightness in percentage.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -196,3 +172,42 @@ class Light(Module, ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def set_state(self, state: LightState) -> dict:
|
async def set_state(self, state: LightState) -> dict:
|
||||||
"""Set the light state."""
|
"""Set the light state."""
|
||||||
|
|
||||||
|
def _deprecated_valid_temperature_range(self) -> ColorTempRange:
|
||||||
|
if not (temp := self.get_feature(self.set_color_temp)):
|
||||||
|
raise KasaException("Color temperature not supported")
|
||||||
|
return ColorTempRange(temp.minimum_value, temp.maximum_value)
|
||||||
|
|
||||||
|
def _deprecated_attributes(self, dep_name: str) -> Callable | None:
|
||||||
|
map: dict[str, Callable] = {
|
||||||
|
"is_color": self.set_hsv,
|
||||||
|
"is_dimmable": self.set_brightness,
|
||||||
|
"is_variable_color_temp": self.set_color_temp,
|
||||||
|
}
|
||||||
|
return map.get(dep_name)
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
if name == "valid_temperature_range":
|
||||||
|
res = self._deprecated_valid_temperature_range()
|
||||||
|
msg = (
|
||||||
|
"valid_temperature_range is deprecated, use "
|
||||||
|
"get_feature(self.set_color_temp) minimum_value "
|
||||||
|
" and maximum_value instead"
|
||||||
|
)
|
||||||
|
warn(msg, DeprecationWarning, stacklevel=2)
|
||||||
|
return res
|
||||||
|
|
||||||
|
if name == "has_effects":
|
||||||
|
msg = (
|
||||||
|
"has_effects is deprecated, use Module.LightEffect "
|
||||||
|
"in device.modules instead"
|
||||||
|
)
|
||||||
|
warn(msg, DeprecationWarning, stacklevel=2)
|
||||||
|
return Module.LightEffect in self._device.modules
|
||||||
|
|
||||||
|
if attr := self._deprecated_attributes(name):
|
||||||
|
msg = f"{name} is deprecated, use has_feature({attr}) instead"
|
||||||
|
warn(msg, DeprecationWarning, stacklevel=2)
|
||||||
|
return self.has_feature(attr)
|
||||||
|
|
||||||
|
raise AttributeError(f"Energy module has no attribute {name!r}")
|
||||||
|
@ -13,8 +13,7 @@ Living Room Bulb
|
|||||||
|
|
||||||
Light effects are accessed via the LightPreset module. To list available presets
|
Light effects are accessed via the LightPreset module. To list available presets
|
||||||
|
|
||||||
>>> if dev.modules[Module.Light].has_effects:
|
>>> light_effect = dev.modules[Module.LightEffect]
|
||||||
>>> light_effect = dev.modules[Module.LightEffect]
|
|
||||||
>>> light_effect.effect_list
|
>>> light_effect.effect_list
|
||||||
['Off', 'Party', 'Relax']
|
['Off', 'Party', 'Relax']
|
||||||
|
|
||||||
|
@ -3,13 +3,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, Annotated, cast
|
||||||
|
|
||||||
from ...device_type import DeviceType
|
from ...device_type import DeviceType
|
||||||
from ...exceptions import KasaException
|
from ...exceptions import KasaException
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
from ...interfaces.light import HSV, ColorTempRange, LightState
|
from ...interfaces.light import HSV, LightState
|
||||||
from ...interfaces.light import Light as LightInterface
|
from ...interfaces.light import Light as LightInterface
|
||||||
|
from ...module import FeatureAttribute
|
||||||
from ..iotmodule import IotModule
|
from ..iotmodule import IotModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -32,7 +33,7 @@ class Light(IotModule, LightInterface):
|
|||||||
super()._initialize_features()
|
super()._initialize_features()
|
||||||
device = self._device
|
device = self._device
|
||||||
|
|
||||||
if self._device._is_dimmable:
|
if device._is_dimmable:
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
Feature(
|
Feature(
|
||||||
device,
|
device,
|
||||||
@ -46,7 +47,9 @@ class Light(IotModule, LightInterface):
|
|||||||
category=Feature.Category.Primary,
|
category=Feature.Category.Primary,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if self._device._is_variable_color_temp:
|
if device._is_variable_color_temp:
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(device, IotBulb)
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
Feature(
|
Feature(
|
||||||
device=device,
|
device=device,
|
||||||
@ -55,12 +58,12 @@ class Light(IotModule, LightInterface):
|
|||||||
container=self,
|
container=self,
|
||||||
attribute_getter="color_temp",
|
attribute_getter="color_temp",
|
||||||
attribute_setter="set_color_temp",
|
attribute_setter="set_color_temp",
|
||||||
range_getter="valid_temperature_range",
|
range_getter=lambda: device._valid_temperature_range,
|
||||||
category=Feature.Category.Primary,
|
category=Feature.Category.Primary,
|
||||||
type=Feature.Type.Number,
|
type=Feature.Type.Number,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if self._device._is_color:
|
if device._is_color:
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
Feature(
|
Feature(
|
||||||
device=device,
|
device=device,
|
||||||
@ -90,18 +93,13 @@ class Light(IotModule, LightInterface):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
def is_dimmable(self) -> int:
|
def brightness(self) -> Annotated[int, FeatureAttribute()]:
|
||||||
"""Whether the bulb supports brightness changes."""
|
|
||||||
return self._device._is_dimmable
|
|
||||||
|
|
||||||
@property # type: ignore
|
|
||||||
def brightness(self) -> int:
|
|
||||||
"""Return the current brightness in percentage."""
|
"""Return the current brightness in percentage."""
|
||||||
return self._device._brightness
|
return self._device._brightness
|
||||||
|
|
||||||
async def set_brightness(
|
async def set_brightness(
|
||||||
self, brightness: int, *, transition: int | None = None
|
self, brightness: int, *, transition: int | None = None
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set the brightness in percentage. A value of 0 will turn off the light.
|
"""Set the brightness in percentage. A value of 0 will turn off the light.
|
||||||
|
|
||||||
:param int brightness: brightness in percent
|
:param int brightness: brightness in percent
|
||||||
@ -112,28 +110,7 @@ class Light(IotModule, LightInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_color(self) -> bool:
|
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
|
||||||
"""Whether the light supports color changes."""
|
|
||||||
if (bulb := self._get_bulb_device()) is None:
|
|
||||||
return False
|
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_effects(self) -> bool:
|
|
||||||
"""Return True if the device supports effects."""
|
|
||||||
if (bulb := self._get_bulb_device()) is None:
|
|
||||||
return False
|
|
||||||
return bulb._has_effects
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hsv(self) -> HSV:
|
|
||||||
"""Return the current HSV state of the bulb.
|
"""Return the current HSV state of the bulb.
|
||||||
|
|
||||||
:return: hue, saturation and value (degrees, %, %)
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
@ -149,7 +126,7 @@ class Light(IotModule, LightInterface):
|
|||||||
value: int | None = None,
|
value: int | None = None,
|
||||||
*,
|
*,
|
||||||
transition: int | None = None,
|
transition: int | None = None,
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set new HSV.
|
"""Set new HSV.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -164,19 +141,7 @@ class Light(IotModule, LightInterface):
|
|||||||
return await bulb._set_hsv(hue, saturation, value, transition=transition)
|
return await bulb._set_hsv(hue, saturation, value, transition=transition)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valid_temperature_range(self) -> ColorTempRange:
|
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
|
||||||
"""Return the device-specific white temperature range (in Kelvin).
|
|
||||||
|
|
||||||
:return: White temperature range in Kelvin (minimum, maximum)
|
|
||||||
"""
|
|
||||||
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."""
|
"""Whether the bulb supports color temperature changes."""
|
||||||
if (
|
if (
|
||||||
bulb := self._get_bulb_device()
|
bulb := self._get_bulb_device()
|
||||||
@ -186,7 +151,7 @@ class Light(IotModule, LightInterface):
|
|||||||
|
|
||||||
async def set_color_temp(
|
async def set_color_temp(
|
||||||
self, temp: int, *, brightness: int | None = None, transition: int | None = None
|
self, temp: int, *, brightness: int | None = None, transition: int | None = None
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set the color temperature of the device in kelvin.
|
"""Set the color temperature of the device in kelvin.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -246,13 +211,13 @@ class Light(IotModule, LightInterface):
|
|||||||
state = LightState(light_on=False)
|
state = LightState(light_on=False)
|
||||||
else:
|
else:
|
||||||
state = LightState(light_on=True)
|
state = LightState(light_on=True)
|
||||||
if self.is_dimmable:
|
if self._device._is_dimmable:
|
||||||
state.brightness = self.brightness
|
state.brightness = self.brightness
|
||||||
if self.is_color:
|
if self._device._is_color:
|
||||||
hsv = self.hsv
|
hsv = self.hsv
|
||||||
state.hue = hsv.hue
|
state.hue = hsv.hue
|
||||||
state.saturation = hsv.saturation
|
state.saturation = hsv.saturation
|
||||||
if self.is_variable_color_temp:
|
if self._device._is_variable_color_temp:
|
||||||
state.color_temp = self.color_temp
|
state.color_temp = self.color_temp
|
||||||
self._light_state = state
|
self._light_state = state
|
||||||
|
|
||||||
|
@ -147,6 +147,11 @@ class Module(ABC):
|
|||||||
self._module = module
|
self._module = module
|
||||||
self._module_features: dict[str, Feature] = {}
|
self._module_features: dict[str, Feature] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _all_features(self) -> dict[str, Feature]:
|
||||||
|
"""Get the features for this module and any sub modules."""
|
||||||
|
return self._module_features
|
||||||
|
|
||||||
def has_feature(self, attribute: str | property | Callable) -> bool:
|
def has_feature(self, attribute: str | property | Callable) -> bool:
|
||||||
"""Return True if the module attribute feature is supported."""
|
"""Return True if the module attribute feature is supported."""
|
||||||
return bool(self.get_feature(attribute))
|
return bool(self.get_feature(attribute))
|
||||||
@ -247,7 +252,7 @@ def _get_bound_feature(
|
|||||||
)
|
)
|
||||||
|
|
||||||
check = {attribute_name, attribute_callable}
|
check = {attribute_name, attribute_callable}
|
||||||
for feature in module._module_features.values():
|
for feature in module._all_features.values():
|
||||||
if (getter := feature.attribute_getter) and getter in check:
|
if (getter := feature.attribute_getter) and getter in check:
|
||||||
return feature
|
return feature
|
||||||
|
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from ...exceptions import KasaException
|
from ...exceptions import KasaException
|
||||||
from ...interfaces.light import HSV, ColorTempRange, LightState
|
from ...feature import Feature
|
||||||
|
from ...interfaces.light import HSV, LightState
|
||||||
from ...interfaces.light import Light as LightInterface
|
from ...interfaces.light import Light as LightInterface
|
||||||
from ...module import Module
|
from ...module import FeatureAttribute, Module
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
@ -16,59 +18,45 @@ class Light(SmartModule, LightInterface):
|
|||||||
|
|
||||||
_light_state: LightState
|
_light_state: LightState
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _all_features(self) -> dict[str, Feature]:
|
||||||
|
"""Get the features for this module and any sub modules."""
|
||||||
|
ret: dict[str, Feature] = {}
|
||||||
|
if brightness := self._device.modules.get(Module.Brightness):
|
||||||
|
ret.update(**brightness._module_features)
|
||||||
|
if color := self._device.modules.get(Module.Color):
|
||||||
|
ret.update(**color._module_features)
|
||||||
|
if temp := self._device.modules.get(Module.ColorTemperature):
|
||||||
|
ret.update(**temp._module_features)
|
||||||
|
return ret
|
||||||
|
|
||||||
def query(self) -> dict:
|
def query(self) -> dict:
|
||||||
"""Query to execute during the update cycle."""
|
"""Query to execute during the update cycle."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_color(self) -> bool:
|
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
|
||||||
"""Whether the bulb supports color changes."""
|
|
||||||
return Module.Color in self._device.modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dimmable(self) -> bool:
|
|
||||||
"""Whether the bulb supports brightness changes."""
|
|
||||||
return Module.Brightness in self._device.modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_variable_color_temp(self) -> bool:
|
|
||||||
"""Whether the bulb supports color temperature changes."""
|
|
||||||
return Module.ColorTemperature in self._device.modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def valid_temperature_range(self) -> ColorTempRange:
|
|
||||||
"""Return the device-specific white temperature range (in Kelvin).
|
|
||||||
|
|
||||||
:return: White temperature range in Kelvin (minimum, maximum)
|
|
||||||
"""
|
|
||||||
if not self.is_variable_color_temp:
|
|
||||||
raise KasaException("Color temperature not supported")
|
|
||||||
|
|
||||||
return self._device.modules[Module.ColorTemperature].valid_temperature_range
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hsv(self) -> HSV:
|
|
||||||
"""Return the current HSV state of the bulb.
|
"""Return the current HSV state of the bulb.
|
||||||
|
|
||||||
:return: hue, saturation and value (degrees, %, %)
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
"""
|
"""
|
||||||
if not self.is_color:
|
if Module.Color not in self._device.modules:
|
||||||
raise KasaException("Bulb does not support color.")
|
raise KasaException("Bulb does not support color.")
|
||||||
|
|
||||||
return self._device.modules[Module.Color].hsv
|
return self._device.modules[Module.Color].hsv
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color_temp(self) -> int:
|
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
|
||||||
"""Whether the bulb supports color temperature changes."""
|
"""Whether the bulb supports color temperature changes."""
|
||||||
if not self.is_variable_color_temp:
|
if Module.ColorTemperature not in self._device.modules:
|
||||||
raise KasaException("Bulb does not support colortemp.")
|
raise KasaException("Bulb does not support colortemp.")
|
||||||
|
|
||||||
return self._device.modules[Module.ColorTemperature].color_temp
|
return self._device.modules[Module.ColorTemperature].color_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self) -> int:
|
def brightness(self) -> Annotated[int, FeatureAttribute()]:
|
||||||
"""Return the current brightness in percentage."""
|
"""Return the current brightness in percentage."""
|
||||||
if not self.is_dimmable: # pragma: no cover
|
if Module.Brightness not in self._device.modules: # pragma: no cover
|
||||||
raise KasaException("Bulb is not dimmable.")
|
raise KasaException("Bulb is not dimmable.")
|
||||||
|
|
||||||
return self._device.modules[Module.Brightness].brightness
|
return self._device.modules[Module.Brightness].brightness
|
||||||
@ -80,7 +68,7 @@ class Light(SmartModule, LightInterface):
|
|||||||
value: int | None = None,
|
value: int | None = None,
|
||||||
*,
|
*,
|
||||||
transition: int | None = None,
|
transition: int | None = None,
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set new HSV.
|
"""Set new HSV.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -90,14 +78,14 @@ class Light(SmartModule, LightInterface):
|
|||||||
:param int value: value between 1 and 100
|
:param int value: value between 1 and 100
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
if not self.is_color:
|
if Module.Color not in self._device.modules:
|
||||||
raise KasaException("Bulb does not support color.")
|
raise KasaException("Bulb does not support color.")
|
||||||
|
|
||||||
return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
|
return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
|
||||||
|
|
||||||
async def set_color_temp(
|
async def set_color_temp(
|
||||||
self, temp: int, *, brightness: int | None = None, transition: int | None = None
|
self, temp: int, *, brightness: int | None = None, transition: int | None = None
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set the color temperature of the device in kelvin.
|
"""Set the color temperature of the device in kelvin.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -105,7 +93,7 @@ class Light(SmartModule, LightInterface):
|
|||||||
:param int temp: The new color temperature, in Kelvin
|
:param int temp: The new color temperature, in Kelvin
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
if not self.is_variable_color_temp:
|
if Module.ColorTemperature not in self._device.modules:
|
||||||
raise KasaException("Bulb does not support colortemp.")
|
raise KasaException("Bulb does not support colortemp.")
|
||||||
return await self._device.modules[Module.ColorTemperature].set_color_temp(
|
return await self._device.modules[Module.ColorTemperature].set_color_temp(
|
||||||
temp, brightness=brightness
|
temp, brightness=brightness
|
||||||
@ -113,7 +101,7 @@ class Light(SmartModule, LightInterface):
|
|||||||
|
|
||||||
async def set_brightness(
|
async def set_brightness(
|
||||||
self, brightness: int, *, transition: int | None = None
|
self, brightness: int, *, transition: int | None = None
|
||||||
) -> dict:
|
) -> Annotated[dict, FeatureAttribute()]:
|
||||||
"""Set the brightness in percentage.
|
"""Set the brightness in percentage.
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
Note, transition is not supported and will be ignored.
|
||||||
@ -121,16 +109,11 @@ class Light(SmartModule, LightInterface):
|
|||||||
:param int brightness: brightness in percent
|
:param int brightness: brightness in percent
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
if not self.is_dimmable: # pragma: no cover
|
if Module.Brightness not in self._device.modules: # pragma: no cover
|
||||||
raise KasaException("Bulb is not dimmable.")
|
raise KasaException("Bulb is not dimmable.")
|
||||||
|
|
||||||
return await self._device.modules[Module.Brightness].set_brightness(brightness)
|
return await self._device.modules[Module.Brightness].set_brightness(brightness)
|
||||||
|
|
||||||
@property
|
|
||||||
def has_effects(self) -> bool:
|
|
||||||
"""Return True if the device supports effects."""
|
|
||||||
return Module.LightEffect in self._device.modules
|
|
||||||
|
|
||||||
async def set_state(self, state: LightState) -> dict:
|
async def set_state(self, state: LightState) -> dict:
|
||||||
"""Set the light state."""
|
"""Set the light state."""
|
||||||
state_dict = asdict(state)
|
state_dict = asdict(state)
|
||||||
@ -157,12 +140,12 @@ class Light(SmartModule, LightInterface):
|
|||||||
state = LightState(light_on=False)
|
state = LightState(light_on=False)
|
||||||
else:
|
else:
|
||||||
state = LightState(light_on=True)
|
state = LightState(light_on=True)
|
||||||
if self.is_dimmable:
|
if Module.Brightness in self._device.modules:
|
||||||
state.brightness = self.brightness
|
state.brightness = self.brightness
|
||||||
if self.is_color:
|
if Module.Color in self._device.modules:
|
||||||
hsv = self.hsv
|
hsv = self.hsv
|
||||||
state.hue = hsv.hue
|
state.hue = hsv.hue
|
||||||
state.saturation = hsv.saturation
|
state.saturation = hsv.saturation
|
||||||
if self.is_variable_color_temp:
|
if Module.ColorTemperature in self._device.modules:
|
||||||
state.color_temp = self.color_temp
|
state.color_temp = self.color_temp
|
||||||
self._light_state = state
|
self._light_state = state
|
||||||
|
@ -96,13 +96,20 @@ class LightPreset(SmartModule, LightPresetInterface):
|
|||||||
"""Return current preset name."""
|
"""Return current preset name."""
|
||||||
light = self._device.modules[SmartModule.Light]
|
light = self._device.modules[SmartModule.Light]
|
||||||
brightness = light.brightness
|
brightness = light.brightness
|
||||||
color_temp = light.color_temp if light.is_variable_color_temp else None
|
color_temp = (
|
||||||
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
|
light.color_temp if light.has_feature(light.set_color_temp) else None
|
||||||
|
)
|
||||||
|
h, s = (
|
||||||
|
(light.hsv.hue, light.hsv.saturation)
|
||||||
|
if light.has_feature(light.set_hsv)
|
||||||
|
else (None, None)
|
||||||
|
)
|
||||||
for preset_name, preset in self._presets.items():
|
for preset_name, preset in self._presets.items():
|
||||||
if (
|
if (
|
||||||
preset.brightness == brightness
|
preset.brightness == brightness
|
||||||
and (
|
and (
|
||||||
preset.color_temp == color_temp or not light.is_variable_color_temp
|
preset.color_temp == color_temp
|
||||||
|
or not light.has_feature(light.set_color_temp)
|
||||||
)
|
)
|
||||||
and preset.hue == h
|
and preset.hue == h
|
||||||
and preset.saturation == s
|
and preset.saturation == s
|
||||||
@ -117,7 +124,7 @@ class LightPreset(SmartModule, LightPresetInterface):
|
|||||||
"""Set a light preset for the device."""
|
"""Set a light preset for the device."""
|
||||||
light = self._device.modules[SmartModule.Light]
|
light = self._device.modules[SmartModule.Light]
|
||||||
if preset_name == self.PRESET_NOT_SET:
|
if preset_name == self.PRESET_NOT_SET:
|
||||||
if light.is_color:
|
if light.has_feature(light.set_hsv):
|
||||||
preset = LightState(hue=0, saturation=0, brightness=100)
|
preset = LightState(hue=0, saturation=0, brightness=100)
|
||||||
else:
|
else:
|
||||||
preset = LightState(brightness=100)
|
preset = LightState(brightness=100)
|
||||||
|
Loading…
Reference in New Issue
Block a user