Make Light and Fan a common module interface (#911)

This commit is contained in:
Steven B
2024-05-13 17:34:44 +01:00
committed by GitHub
parent d7b00336f4
commit 33d839866e
32 changed files with 544 additions and 342 deletions

View File

@@ -11,12 +11,20 @@ from pydantic.v1 import BaseModel, Field, root_validator
from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..feature import Feature
from ..interfaces.light import HSV, ColorTempRange, Light, LightPreset
from ..interfaces.light import HSV, ColorTempRange, LightPreset
from ..module import Module
from ..protocol import BaseProtocol
from .iotdevice import IotDevice, KasaException, requires_update
from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage
from .modules import (
Antitheft,
Cloud,
Countdown,
Emeter,
Light,
Schedule,
Time,
Usage,
)
class BehaviorMode(str, Enum):
@@ -88,7 +96,7 @@ NON_COLOR_MODE_FLAGS = {"transition_period", "on_off"}
_LOGGER = logging.getLogger(__name__)
class IotBulb(IotDevice, Light):
class IotBulb(IotDevice):
r"""Representation of a TP-Link Smart Bulb.
To initialize, you have to await :func:`update()` at least once.
@@ -199,6 +207,10 @@ class IotBulb(IotDevice, Light):
) -> None:
super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.Bulb
async def _initialize_modules(self):
"""Initialize modules not added in init."""
await super()._initialize_modules()
self.add_module(
Module.IotSchedule, Schedule(self, "smartlife.iot.common.schedule")
)
@@ -210,39 +222,7 @@ class IotBulb(IotDevice, Light):
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
self.add_module(Module.IotCloud, Cloud(self, "smartlife.iot.common.cloud"))
async def _initialize_features(self):
await super()._initialize_features()
if bool(self.sys_info["is_dimmable"]): # pragma: no branch
self._add_feature(
Feature(
device=self,
id="brightness",
name="Brightness",
attribute_getter="brightness",
attribute_setter="set_brightness",
minimum_value=1,
maximum_value=100,
type=Feature.Type.Number,
category=Feature.Category.Primary,
)
)
if self.is_variable_color_temp:
self._add_feature(
Feature(
device=self,
id="color_temperature",
name="Color temperature",
container=self,
attribute_getter="color_temp",
attribute_setter="set_color_temp",
range_getter="valid_temperature_range",
category=Feature.Category.Primary,
type=Feature.Type.Number,
)
)
self.add_module(Module.Light, Light(self, "light"))
@property # type: ignore
@requires_update
@@ -458,6 +438,10 @@ class IotBulb(IotDevice, Light):
return await self.set_light_state(light_state, transition=transition)
def _raise_for_invalid_brightness(self, value):
if not isinstance(value, int) or not (0 <= value <= 100):
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
@property # type: ignore
@requires_update
def brightness(self) -> int:

View File

@@ -307,6 +307,9 @@ class IotDevice(Device):
self._last_update = response
self._set_sys_info(response["system"]["get_sysinfo"])
if not self._modules:
await self._initialize_modules()
await self._modular_update(req)
if not self._features:
@@ -314,6 +317,9 @@ class IotDevice(Device):
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
async def _initialize_modules(self):
"""Initialize modules not added in init."""
async def _initialize_features(self):
self._add_feature(
Feature(

View File

@@ -7,12 +7,11 @@ from typing import Any
from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..feature import Feature
from ..module import Module
from ..protocol import BaseProtocol
from .iotdevice import KasaException, requires_update
from .iotplug import IotPlug
from .modules import AmbientLight, Motion
from .modules import AmbientLight, Light, Motion
class ButtonAction(Enum):
@@ -80,29 +79,15 @@ class IotDimmer(IotPlug):
) -> None:
super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.Dimmer
async def _initialize_modules(self):
"""Initialize modules."""
await super()._initialize_modules()
# TODO: need to be verified if it's okay to call these on HS220 w/o these
# TODO: need to be figured out what's the best approach to detect support
self.add_module(Module.IotMotion, Motion(self, "smartlife.iot.PIR"))
self.add_module(Module.IotAmbientLight, AmbientLight(self, "smartlife.iot.LAS"))
async def _initialize_features(self):
await super()._initialize_features()
if "brightness" in self.sys_info: # pragma: no branch
self._add_feature(
Feature(
device=self,
id="brightness",
name="Brightness",
attribute_getter="brightness",
attribute_setter="set_brightness",
minimum_value=1,
maximum_value=100,
unit="%",
type=Feature.Type.Number,
category=Feature.Category.Primary,
)
)
self.add_module(Module.Light, Light(self, "light"))
@property # type: ignore
@requires_update

View File

@@ -56,6 +56,10 @@ class IotLightStrip(IotBulb):
) -> None:
super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.LightStrip
async def _initialize_modules(self):
"""Initialize modules not added in init."""
await super()._initialize_modules()
self.add_module(
Module.LightEffect,
LightEffect(self, "smartlife.iot.lighting_effect"),

View File

@@ -53,6 +53,10 @@ class IotPlug(IotDevice):
) -> None:
super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.Plug
async def _initialize_modules(self):
"""Initialize modules."""
await super()._initialize_modules()
self.add_module(Module.IotSchedule, Schedule(self, "schedule"))
self.add_module(Module.IotUsage, Usage(self, "schedule"))
self.add_module(Module.IotAntitheft, Antitheft(self, "anti_theft"))

View File

@@ -255,6 +255,10 @@ class IotStripPlug(IotPlug):
self._set_sys_info(parent.sys_info)
self._device_type = DeviceType.StripSocket
self.protocol = parent.protocol # Must use the same connection as the parent
async def _initialize_modules(self):
"""Initialize modules not added in init."""
await super()._initialize_modules()
self.add_module("time", Time(self, "time"))
async def update(self, update_children: bool = True):

View File

@@ -6,6 +6,7 @@ from .cloud import Cloud
from .countdown import Countdown
from .emeter import Emeter
from .led import Led
from .light import Light
from .lighteffect import LightEffect
from .motion import Motion
from .rulemodule import Rule, RuleModule
@@ -20,6 +21,7 @@ __all__ = [
"Countdown",
"Emeter",
"Led",
"Light",
"LightEffect",
"Motion",
"Rule",

188
kasa/iot/modules/light.py Normal file
View File

@@ -0,0 +1,188 @@
"""Implementation of brightness module."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from ...exceptions import KasaException
from ...feature import Feature
from ...interfaces.light import HSV, ColorTempRange
from ...interfaces.light import Light as LightInterface
from ..iotmodule import IotModule
if TYPE_CHECKING:
from ..iotbulb import IotBulb
from ..iotdimmer import IotDimmer
BRIGHTNESS_MIN = 0
BRIGHTNESS_MAX = 100
class Light(IotModule, LightInterface):
"""Implementation of brightness module."""
_device: IotBulb | IotDimmer
def _initialize_features(self):
"""Initialize features."""
super()._initialize_features()
device = self._device
if self._device.is_dimmable:
self._add_feature(
Feature(
device,
id="brightness",
name="Brightness",
container=self,
attribute_getter="brightness",
attribute_setter="set_brightness",
minimum_value=BRIGHTNESS_MIN,
maximum_value=BRIGHTNESS_MAX,
type=Feature.Type.Number,
category=Feature.Category.Primary,
)
)
if self._device.is_variable_color_temp:
self._add_feature(
Feature(
device=device,
id="color_temperature",
name="Color temperature",
container=self,
attribute_getter="color_temp",
attribute_setter="set_color_temp",
range_getter="valid_temperature_range",
category=Feature.Category.Primary,
type=Feature.Type.Number,
)
)
if self._device.is_color:
self._add_feature(
Feature(
device=device,
id="hsv",
name="HSV",
container=self,
attribute_getter="hsv",
attribute_setter="set_hsv",
# TODO proper type for setting hsv
type=Feature.Type.Unknown,
)
)
def query(self) -> dict:
"""Query to execute during the update cycle."""
# Brightness is contained in the main device info response.
return {}
def _get_bulb_device(self) -> IotBulb | None:
if self._device.is_bulb or self._device.is_light_strip:
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
@property # type: ignore
def brightness(self) -> int:
"""Return the current brightness in percentage."""
return self._device.brightness
async def set_brightness(
self, brightness: int, *, transition: int | None = None
) -> dict:
"""Set the brightness in percentage.
:param int brightness: brightness in percent
:param int transition: transition in milliseconds.
"""
return await self._device.set_brightness(brightness, transition=transition)
@property
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
@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: hue, saturation and value (degrees, %, %)
"""
if (bulb := self._get_bulb_device()) is None or not bulb.is_color:
raise KasaException("Light does not support color.")
return bulb.hsv
async def set_hsv(
self,
hue: int,
saturation: int,
value: int | None = None,
*,
transition: int | None = None,
) -> dict:
"""Set new HSV.
Note, transition is not supported and will be ignored.
:param int hue: hue in degrees
:param int saturation: saturation in percentage [0,100]
: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:
raise KasaException("Light does not support color.")
return await bulb.set_hsv(hue, saturation, value, transition=transition)
@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 (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:
raise KasaException("Light does not support colortemp.")
return bulb.color_temp
async def set_color_temp(
self, temp: int, *, brightness=None, transition: int | None = None
) -> dict:
"""Set the color temperature of the device in kelvin.
Note, transition is not supported and will be ignored.
: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:
raise KasaException("Light does not support colortemp.")
return await bulb.set_color_temp(
temp, brightness=brightness, transition=transition
)