mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Add light presets common module to devices. (#907)
Adds light preset common module for switching to presets and saving presets. Deprecates the `presets` attribute and `save_preset` method from the `bulb` interface in favour of the modular approach. Allows setting preset for `iot` which was not previously supported.
This commit is contained in:
@@ -11,7 +11,7 @@ from pydantic.v1 import BaseModel, Field, root_validator
|
||||
|
||||
from ..device_type import DeviceType
|
||||
from ..deviceconfig import DeviceConfig
|
||||
from ..interfaces.light import HSV, ColorTempRange, LightPreset
|
||||
from ..interfaces.light import HSV, ColorTempRange
|
||||
from ..module import Module
|
||||
from ..protocol import BaseProtocol
|
||||
from .iotdevice import IotDevice, KasaException, requires_update
|
||||
@@ -21,6 +21,7 @@ from .modules import (
|
||||
Countdown,
|
||||
Emeter,
|
||||
Light,
|
||||
LightPreset,
|
||||
Schedule,
|
||||
Time,
|
||||
Usage,
|
||||
@@ -178,7 +179,7 @@ class IotBulb(IotDevice):
|
||||
Bulb configuration presets can be accessed using the :func:`presets` property:
|
||||
|
||||
>>> bulb.presets
|
||||
[LightPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), LightPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
|
||||
[IotLightPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), IotLightPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), IotLightPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), IotLightPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
|
||||
|
||||
To modify an existing preset, pass :class:`~kasa.interfaces.light.LightPreset`
|
||||
instance to :func:`save_preset` method:
|
||||
@@ -222,7 +223,8 @@ class IotBulb(IotDevice):
|
||||
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"))
|
||||
self.add_module(Module.Light, Light(self, "light"))
|
||||
self.add_module(Module.Light, Light(self, self.LIGHT_SERVICE))
|
||||
self.add_module(Module.LightPreset, LightPreset(self, self.LIGHT_SERVICE))
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@@ -320,7 +322,7 @@ class IotBulb(IotDevice):
|
||||
# TODO: add warning and refer to use light.state?
|
||||
return await self._query_helper(self.LIGHT_SERVICE, "get_light_state")
|
||||
|
||||
async def set_light_state(
|
||||
async def _set_light_state(
|
||||
self, state: dict, *, transition: int | None = None
|
||||
) -> dict:
|
||||
"""Set the light state."""
|
||||
@@ -400,7 +402,7 @@ class IotBulb(IotDevice):
|
||||
self._raise_for_invalid_brightness(value)
|
||||
light_state["brightness"] = value
|
||||
|
||||
return await self.set_light_state(light_state, transition=transition)
|
||||
return await self._set_light_state(light_state, transition=transition)
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@@ -436,7 +438,7 @@ class IotBulb(IotDevice):
|
||||
if brightness is not None:
|
||||
light_state["brightness"] = brightness
|
||||
|
||||
return await self.set_light_state(light_state, transition=transition)
|
||||
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):
|
||||
@@ -467,7 +469,7 @@ class IotBulb(IotDevice):
|
||||
self._raise_for_invalid_brightness(brightness)
|
||||
|
||||
light_state = {"brightness": brightness}
|
||||
return await self.set_light_state(light_state, transition=transition)
|
||||
return await self._set_light_state(light_state, transition=transition)
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@@ -481,14 +483,14 @@ class IotBulb(IotDevice):
|
||||
|
||||
:param int transition: transition in milliseconds.
|
||||
"""
|
||||
return await self.set_light_state({"on_off": 0}, transition=transition)
|
||||
return await self._set_light_state({"on_off": 0}, transition=transition)
|
||||
|
||||
async def turn_on(self, *, transition: int | None = None, **kwargs) -> dict:
|
||||
"""Turn the bulb on.
|
||||
|
||||
:param int transition: transition in milliseconds.
|
||||
"""
|
||||
return await self.set_light_state({"on_off": 1}, transition=transition)
|
||||
return await self._set_light_state({"on_off": 1}, transition=transition)
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@@ -505,28 +507,6 @@ class IotBulb(IotDevice):
|
||||
"smartlife.iot.common.system", "set_dev_alias", {"alias": alias}
|
||||
)
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def presets(self) -> list[LightPreset]:
|
||||
"""Return a list of available bulb setting presets."""
|
||||
return [LightPreset(**vals) for vals in self.sys_info["preferred_state"]]
|
||||
|
||||
async def save_preset(self, preset: LightPreset):
|
||||
"""Save a setting preset.
|
||||
|
||||
You can either construct a preset object manually, or pass an existing one
|
||||
obtained using :func:`presets`.
|
||||
"""
|
||||
if len(self.presets) == 0:
|
||||
raise KasaException("Device does not supported saving presets")
|
||||
|
||||
if preset.index >= len(self.presets):
|
||||
raise KasaException("Invalid preset index")
|
||||
|
||||
return await self._query_helper(
|
||||
self.LIGHT_SERVICE, "set_preferred_state", preset.dict(exclude_none=True)
|
||||
)
|
||||
|
||||
@property
|
||||
def max_device_response_size(self) -> int:
|
||||
"""Returns the maximum response size the device can safely construct."""
|
||||
|
@@ -312,11 +312,13 @@ class IotDevice(Device):
|
||||
|
||||
await self._modular_update(req)
|
||||
|
||||
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
|
||||
for module in self._modules.values():
|
||||
module._post_update_hook()
|
||||
|
||||
if not self._features:
|
||||
await self._initialize_features()
|
||||
|
||||
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
|
||||
|
||||
async def _initialize_modules(self):
|
||||
"""Initialize modules not added in init."""
|
||||
|
||||
|
@@ -8,6 +8,7 @@ from .emeter import Emeter
|
||||
from .led import Led
|
||||
from .light import Light
|
||||
from .lighteffect import LightEffect
|
||||
from .lightpreset import IotLightPreset, LightPreset
|
||||
from .motion import Motion
|
||||
from .rulemodule import Rule, RuleModule
|
||||
from .schedule import Schedule
|
||||
@@ -23,6 +24,8 @@ __all__ = [
|
||||
"Led",
|
||||
"Light",
|
||||
"LightEffect",
|
||||
"LightPreset",
|
||||
"IotLightPreset",
|
||||
"Motion",
|
||||
"Rule",
|
||||
"RuleModule",
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
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
|
||||
from ...interfaces.light import HSV, ColorTempRange, LightState
|
||||
from ...interfaces.light import Light as LightInterface
|
||||
from ..iotmodule import IotModule
|
||||
|
||||
@@ -198,3 +199,23 @@ class Light(IotModule, LightInterface):
|
||||
return await bulb._set_color_temp(
|
||||
temp, brightness=brightness, transition=transition
|
||||
)
|
||||
|
||||
async def set_state(self, state: LightState) -> dict:
|
||||
"""Set the light state."""
|
||||
if (bulb := self._get_bulb_device()) is None:
|
||||
return await self.set_brightness(state.brightness or 0)
|
||||
else:
|
||||
transition = state.transition
|
||||
state_dict = asdict(state)
|
||||
state_dict = {k: v for k, v in state_dict.items() if v is not None}
|
||||
state_dict["on_off"] = 1 if state.light_on is None else int(state.light_on)
|
||||
return await bulb._set_light_state(state_dict, transition=transition)
|
||||
|
||||
async def _deprecated_set_light_state(
|
||||
self, state: dict, *, transition: int | None = None
|
||||
) -> dict:
|
||||
"""Set the light state."""
|
||||
if (bulb := self._get_bulb_device()) is None:
|
||||
raise KasaException("Device does not support set_light_state")
|
||||
else:
|
||||
return await bulb._set_light_state(state, transition=transition)
|
||||
|
151
kasa/iot/modules/lightpreset.py
Normal file
151
kasa/iot/modules/lightpreset.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Light preset module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Optional, Sequence
|
||||
|
||||
from pydantic.v1 import BaseModel, Field
|
||||
|
||||
from ...exceptions import KasaException
|
||||
from ...interfaces import LightPreset as LightPresetInterface
|
||||
from ...interfaces import LightState
|
||||
from ...module import Module
|
||||
from ..iotmodule import IotModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class IotLightPreset(BaseModel, LightState):
|
||||
"""Light configuration preset."""
|
||||
|
||||
index: int = Field(kw_only=True)
|
||||
brightness: int = Field(kw_only=True)
|
||||
|
||||
# These are not available for effect mode presets on light strips
|
||||
hue: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
|
||||
saturation: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
|
||||
color_temp: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
|
||||
|
||||
# Variables for effect mode presets
|
||||
custom: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
|
||||
id: Optional[str] = Field(kw_only=True, default=None) # noqa: UP007
|
||||
mode: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
|
||||
|
||||
|
||||
class LightPreset(IotModule, LightPresetInterface):
|
||||
"""Class for setting light presets."""
|
||||
|
||||
_presets: dict[str, IotLightPreset]
|
||||
_preset_list: list[str]
|
||||
|
||||
def _post_update_hook(self):
|
||||
"""Update the internal presets."""
|
||||
self._presets = {
|
||||
f"Light preset {index+1}": IotLightPreset(**vals)
|
||||
for index, vals in enumerate(self.data["preferred_state"])
|
||||
}
|
||||
self._preset_list = [self.PRESET_NOT_SET]
|
||||
self._preset_list.extend(self._presets.keys())
|
||||
|
||||
@property
|
||||
def preset_list(self) -> list[str]:
|
||||
"""Return built-in effects list.
|
||||
|
||||
Example:
|
||||
['Off', 'Preset 1', 'Preset 2', ...]
|
||||
"""
|
||||
return self._preset_list
|
||||
|
||||
@property
|
||||
def preset_states_list(self) -> Sequence[IotLightPreset]:
|
||||
"""Return built-in effects list.
|
||||
|
||||
Example:
|
||||
['Off', 'Preset 1', 'Preset 2', ...]
|
||||
"""
|
||||
return list(self._presets.values())
|
||||
|
||||
@property
|
||||
def preset(self) -> str:
|
||||
"""Return current preset name."""
|
||||
light = self._device.modules[Module.Light]
|
||||
brightness = light.brightness
|
||||
color_temp = light.color_temp if light.is_variable_color_temp else None
|
||||
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
|
||||
for preset_name, preset in self._presets.items():
|
||||
if (
|
||||
preset.brightness == brightness
|
||||
and (
|
||||
preset.color_temp == color_temp or not light.is_variable_color_temp
|
||||
)
|
||||
and (preset.hue == h or not light.is_color)
|
||||
and (preset.saturation == s or not light.is_color)
|
||||
):
|
||||
return preset_name
|
||||
return self.PRESET_NOT_SET
|
||||
|
||||
async def set_preset(
|
||||
self,
|
||||
preset_name: str,
|
||||
) -> None:
|
||||
"""Set a light preset for the device."""
|
||||
light = self._device.modules[Module.Light]
|
||||
if preset_name == self.PRESET_NOT_SET:
|
||||
if light.is_color:
|
||||
preset = LightState(hue=0, saturation=0, brightness=100)
|
||||
else:
|
||||
preset = LightState(brightness=100)
|
||||
elif (preset := self._presets.get(preset_name)) is None: # type: ignore[assignment]
|
||||
raise ValueError(f"{preset_name} is not a valid preset: {self.preset_list}")
|
||||
|
||||
await light.set_state(preset)
|
||||
|
||||
@property
|
||||
def has_save_preset(self) -> bool:
|
||||
"""Return True if the device supports updating presets."""
|
||||
return True
|
||||
|
||||
async def save_preset(
|
||||
self,
|
||||
preset_name: str,
|
||||
preset_state: LightState,
|
||||
) -> None:
|
||||
"""Update the preset with preset_name with the new preset_info."""
|
||||
if len(self._presets) == 0:
|
||||
raise KasaException("Device does not supported saving presets")
|
||||
if preset_name not in self._presets:
|
||||
raise ValueError(f"{preset_name} is not a valid preset: {self.preset_list}")
|
||||
|
||||
index = list(self._presets.keys()).index(preset_name)
|
||||
state = asdict(preset_state)
|
||||
state = {k: v for k, v in state.items() if v is not None}
|
||||
state["index"] = index
|
||||
|
||||
return await self.call("set_preferred_state", state)
|
||||
|
||||
def query(self):
|
||||
"""Return the base query."""
|
||||
return {}
|
||||
|
||||
@property # type: ignore
|
||||
def _deprecated_presets(self) -> list[IotLightPreset]:
|
||||
"""Return a list of available bulb setting presets."""
|
||||
return [
|
||||
IotLightPreset(**vals) for vals in self._device.sys_info["preferred_state"]
|
||||
]
|
||||
|
||||
async def _deprecated_save_preset(self, preset: IotLightPreset):
|
||||
"""Save a setting preset.
|
||||
|
||||
You can either construct a preset object manually, or pass an existing one
|
||||
obtained using :func:`presets`.
|
||||
"""
|
||||
if len(self._presets) == 0:
|
||||
raise KasaException("Device does not supported saving presets")
|
||||
|
||||
if preset.index >= len(self._presets):
|
||||
raise KasaException("Invalid preset index")
|
||||
|
||||
return await self.call("set_preferred_state", preset.dict(exclude_none=True))
|
Reference in New Issue
Block a user