mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Add ColorModule for smart devices (#840)
Adds support L530 hw_version 1.0
This commit is contained in:
@@ -6,6 +6,7 @@ from .battery import BatterySensor
|
||||
from .brightness import Brightness
|
||||
from .childdevicemodule import ChildDeviceModule
|
||||
from .cloudmodule import CloudModule
|
||||
from .colormodule import ColorModule
|
||||
from .colortemp import ColorTemperatureModule
|
||||
from .devicemodule import DeviceModule
|
||||
from .energymodule import EnergyModule
|
||||
@@ -36,4 +37,5 @@ __all__ = [
|
||||
"CloudModule",
|
||||
"LightTransitionModule",
|
||||
"ColorTemperatureModule",
|
||||
"ColorModule",
|
||||
]
|
||||
|
94
kasa/smart/modules/colormodule.py
Normal file
94
kasa/smart/modules/colormodule.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Implementation of color module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...bulb import HSV
|
||||
from ...feature import Feature
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class ColorModule(SmartModule):
|
||||
"""Implementation of color module."""
|
||||
|
||||
REQUIRED_COMPONENT = "color"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
"HSV",
|
||||
container=self,
|
||||
attribute_getter="hsv",
|
||||
# TODO proper type for setting hsv
|
||||
attribute_setter="set_hsv",
|
||||
)
|
||||
)
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
# HSV is contained in the main device info response.
|
||||
return {}
|
||||
|
||||
@property
|
||||
def hsv(self) -> HSV:
|
||||
"""Return the current HSV state of the bulb.
|
||||
|
||||
:return: hue, saturation and value (degrees, %, 1-100)
|
||||
"""
|
||||
h, s, v = (
|
||||
self.data.get("hue", 0),
|
||||
self.data.get("saturation", 0),
|
||||
self.data.get("brightness", 0),
|
||||
)
|
||||
|
||||
return HSV(hue=h, saturation=s, value=v)
|
||||
|
||||
def _raise_for_invalid_brightness(self, value: int):
|
||||
"""Raise error on invalid brightness value."""
|
||||
if not isinstance(value, int) or not (1 <= value <= 100):
|
||||
raise ValueError(f"Invalid brightness value: {value} (valid range: 1-100%)")
|
||||
|
||||
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 not isinstance(hue, int) or not (0 <= hue <= 360):
|
||||
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
|
||||
|
||||
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
|
||||
raise ValueError(
|
||||
f"Invalid saturation value: {saturation} (valid range: 0-100%)"
|
||||
)
|
||||
|
||||
if value is not None:
|
||||
self._raise_for_invalid_brightness(value)
|
||||
|
||||
request_payload = {
|
||||
"color_temp": 0, # If set, color_temp takes precedence over hue&sat
|
||||
"hue": hue,
|
||||
"saturation": saturation,
|
||||
}
|
||||
# The device errors on invalid brightness values.
|
||||
if value is not None:
|
||||
request_payload["brightness"] = value
|
||||
|
||||
return await self.call("set_device_info", {**request_payload})
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...bulb import ColorTempRange
|
||||
@@ -12,6 +13,11 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TEMP_RANGE = [2500, 6500]
|
||||
|
||||
|
||||
class ColorTemperatureModule(SmartModule):
|
||||
"""Implementation of color temp module."""
|
||||
|
||||
@@ -38,7 +44,14 @@ class ColorTemperatureModule(SmartModule):
|
||||
@property
|
||||
def valid_temperature_range(self) -> ColorTempRange:
|
||||
"""Return valid color-temp range."""
|
||||
return ColorTempRange(*self.data.get("color_temp_range"))
|
||||
if (ct_range := self.data.get("color_temp_range")) is None:
|
||||
_LOGGER.debug(
|
||||
"Device doesn't report color temperature range, "
|
||||
"falling back to default %s",
|
||||
DEFAULT_TEMP_RANGE,
|
||||
)
|
||||
ct_range = DEFAULT_TEMP_RANGE
|
||||
return ColorTempRange(*ct_range)
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
@@ -56,3 +69,7 @@ class ColorTemperatureModule(SmartModule):
|
||||
)
|
||||
|
||||
return await self.call("set_device_info", {"color_temp": temp})
|
||||
|
||||
async def _check_supported(self) -> bool:
|
||||
"""Check the color_temp_range has more than one value."""
|
||||
return self.valid_temperature_range.min != self.valid_temperature_range.max
|
||||
|
Reference in New Issue
Block a user