mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +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:
parent
1ba5c73279
commit
273c541fcc
@ -99,5 +99,5 @@ False
|
|||||||
True
|
True
|
||||||
>>> for feat in dev.features.values():
|
>>> for feat in dev.features.values():
|
||||||
>>> print(f"{feat.name}: {feat.value}")
|
>>> print(f"{feat.name}: {feat.value}")
|
||||||
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: False\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: 1.1.6 Build 240130 Rel.173828\nLight effect: Party\nSmooth transition on: 2\nSmooth transition off: 2\nTime: 2024-02-23 02:40:15+01:00
|
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: False\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: 1.1.6 Build 240130 Rel.173828\nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nTime: 2024-02-23 02:40:15+01:00
|
||||||
"""
|
"""
|
||||||
|
@ -35,7 +35,7 @@ from kasa.exceptions import (
|
|||||||
UnsupportedDeviceError,
|
UnsupportedDeviceError,
|
||||||
)
|
)
|
||||||
from kasa.feature import Feature
|
from kasa.feature import Feature
|
||||||
from kasa.interfaces.light import Light, LightPreset
|
from kasa.interfaces.light import Light, LightState
|
||||||
from kasa.iotprotocol import (
|
from kasa.iotprotocol import (
|
||||||
IotProtocol,
|
IotProtocol,
|
||||||
_deprecated_TPLinkSmartHomeProtocol, # noqa: F401
|
_deprecated_TPLinkSmartHomeProtocol, # noqa: F401
|
||||||
@ -52,7 +52,7 @@ __all__ = [
|
|||||||
"BaseProtocol",
|
"BaseProtocol",
|
||||||
"IotProtocol",
|
"IotProtocol",
|
||||||
"SmartProtocol",
|
"SmartProtocol",
|
||||||
"LightPreset",
|
"LightState",
|
||||||
"TurnOnBehaviors",
|
"TurnOnBehaviors",
|
||||||
"TurnOnBehavior",
|
"TurnOnBehavior",
|
||||||
"DeviceType",
|
"DeviceType",
|
||||||
@ -75,6 +75,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
from . import iot
|
from . import iot
|
||||||
|
from .iot.modules.lightpreset import IotLightPreset
|
||||||
|
|
||||||
deprecated_names = ["TPLinkSmartHomeProtocol"]
|
deprecated_names = ["TPLinkSmartHomeProtocol"]
|
||||||
deprecated_smart_devices = {
|
deprecated_smart_devices = {
|
||||||
@ -84,7 +85,7 @@ deprecated_smart_devices = {
|
|||||||
"SmartLightStrip": iot.IotLightStrip,
|
"SmartLightStrip": iot.IotLightStrip,
|
||||||
"SmartStrip": iot.IotStrip,
|
"SmartStrip": iot.IotStrip,
|
||||||
"SmartDimmer": iot.IotDimmer,
|
"SmartDimmer": iot.IotDimmer,
|
||||||
"SmartBulbPreset": LightPreset,
|
"SmartBulbPreset": IotLightPreset,
|
||||||
}
|
}
|
||||||
deprecated_exceptions = {
|
deprecated_exceptions = {
|
||||||
"SmartDeviceException": KasaException,
|
"SmartDeviceException": KasaException,
|
||||||
@ -124,7 +125,7 @@ if TYPE_CHECKING:
|
|||||||
SmartLightStrip = iot.IotLightStrip
|
SmartLightStrip = iot.IotLightStrip
|
||||||
SmartStrip = iot.IotStrip
|
SmartStrip = iot.IotStrip
|
||||||
SmartDimmer = iot.IotDimmer
|
SmartDimmer = iot.IotDimmer
|
||||||
SmartBulbPreset = LightPreset
|
SmartBulbPreset = IotLightPreset
|
||||||
|
|
||||||
SmartDeviceException = KasaException
|
SmartDeviceException = KasaException
|
||||||
UnsupportedDeviceException = UnsupportedDeviceError
|
UnsupportedDeviceException = UnsupportedDeviceError
|
||||||
|
@ -364,6 +364,7 @@ class Device(ABC):
|
|||||||
"set_color_temp": (Module.Light, ["set_color_temp"]),
|
"set_color_temp": (Module.Light, ["set_color_temp"]),
|
||||||
"valid_temperature_range": (Module.Light, ["valid_temperature_range"]),
|
"valid_temperature_range": (Module.Light, ["valid_temperature_range"]),
|
||||||
"has_effects": (Module.Light, ["has_effects"]),
|
"has_effects": (Module.Light, ["has_effects"]),
|
||||||
|
"_deprecated_set_light_state": (Module.Light, ["has_effects"]),
|
||||||
# led attributes
|
# led attributes
|
||||||
"led": (Module.Led, ["led"]),
|
"led": (Module.Led, ["led"]),
|
||||||
"set_led": (Module.Led, ["set_led"]),
|
"set_led": (Module.Led, ["set_led"]),
|
||||||
@ -376,6 +377,9 @@ class Device(ABC):
|
|||||||
"effect_list": (Module.LightEffect, ["_deprecated_effect_list", "effect_list"]),
|
"effect_list": (Module.LightEffect, ["_deprecated_effect_list", "effect_list"]),
|
||||||
"set_effect": (Module.LightEffect, ["set_effect"]),
|
"set_effect": (Module.LightEffect, ["set_effect"]),
|
||||||
"set_custom_effect": (Module.LightEffect, ["set_custom_effect"]),
|
"set_custom_effect": (Module.LightEffect, ["set_custom_effect"]),
|
||||||
|
# light preset attributes
|
||||||
|
"presets": (Module.LightPreset, ["_deprecated_presets", "preset_states_list"]),
|
||||||
|
"save_preset": (Module.LightPreset, ["_deprecated_save_preset"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
from .fan import Fan
|
from .fan import Fan
|
||||||
from .led import Led
|
from .led import Led
|
||||||
from .light import Light, LightPreset
|
from .light import Light, LightState
|
||||||
from .lighteffect import LightEffect
|
from .lighteffect import LightEffect
|
||||||
|
from .lightpreset import LightPreset
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Fan",
|
"Fan",
|
||||||
"Led",
|
"Led",
|
||||||
"Light",
|
"Light",
|
||||||
"LightEffect",
|
"LightEffect",
|
||||||
|
"LightState",
|
||||||
"LightPreset",
|
"LightPreset",
|
||||||
]
|
]
|
||||||
|
@ -3,13 +3,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import NamedTuple, Optional
|
from dataclasses import dataclass
|
||||||
|
from typing import NamedTuple
|
||||||
from pydantic.v1 import BaseModel
|
|
||||||
|
|
||||||
from ..module import Module
|
from ..module import Module
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LightState:
|
||||||
|
"""Class for smart light preset info."""
|
||||||
|
|
||||||
|
light_on: bool | None = None
|
||||||
|
brightness: int | None = None
|
||||||
|
hue: int | None = None
|
||||||
|
saturation: int | None = None
|
||||||
|
color_temp: int | None = None
|
||||||
|
transition: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class ColorTempRange(NamedTuple):
|
class ColorTempRange(NamedTuple):
|
||||||
"""Color temperature range."""
|
"""Color temperature range."""
|
||||||
|
|
||||||
@ -25,23 +36,6 @@ class HSV(NamedTuple):
|
|||||||
value: int
|
value: int
|
||||||
|
|
||||||
|
|
||||||
class LightPreset(BaseModel):
|
|
||||||
"""Light configuration preset."""
|
|
||||||
|
|
||||||
index: int
|
|
||||||
brightness: int
|
|
||||||
|
|
||||||
# These are not available for effect mode presets on light strips
|
|
||||||
hue: Optional[int] # noqa: UP007
|
|
||||||
saturation: Optional[int] # noqa: UP007
|
|
||||||
color_temp: Optional[int] # noqa: UP007
|
|
||||||
|
|
||||||
# Variables for effect mode presets
|
|
||||||
custom: Optional[int] # noqa: UP007
|
|
||||||
id: Optional[str] # noqa: UP007
|
|
||||||
mode: Optional[int] # noqa: UP007
|
|
||||||
|
|
||||||
|
|
||||||
class Light(Module, ABC):
|
class Light(Module, ABC):
|
||||||
"""Base class for TP-Link Light."""
|
"""Base class for TP-Link Light."""
|
||||||
|
|
||||||
@ -133,3 +127,7 @@ class Light(Module, ABC):
|
|||||||
:param int brightness: brightness in percent
|
:param int brightness: brightness in percent
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def set_state(self, state: LightState) -> dict:
|
||||||
|
"""Set the light state."""
|
||||||
|
76
kasa/interfaces/lightpreset.py
Normal file
76
kasa/interfaces/lightpreset.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"""Module for LightPreset base class."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from ..feature import Feature
|
||||||
|
from ..module import Module
|
||||||
|
from .light import LightState
|
||||||
|
|
||||||
|
|
||||||
|
class LightPreset(Module):
|
||||||
|
"""Base interface for light preset module."""
|
||||||
|
|
||||||
|
PRESET_NOT_SET = "Not set"
|
||||||
|
|
||||||
|
def _initialize_features(self):
|
||||||
|
"""Initialize features."""
|
||||||
|
device = self._device
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device,
|
||||||
|
id="light_preset",
|
||||||
|
name="Light preset",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="preset",
|
||||||
|
attribute_setter="set_preset",
|
||||||
|
category=Feature.Category.Config,
|
||||||
|
type=Feature.Type.Choice,
|
||||||
|
choices_getter="preset_list",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def preset_list(self) -> list[str]:
|
||||||
|
"""Return list of preset names.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
['Off', 'Preset 1', 'Preset 2', ...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def preset_states_list(self) -> Sequence[LightState]:
|
||||||
|
"""Return list of preset states.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
['Off', 'Preset 1', 'Preset 2', ...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def preset(self) -> str:
|
||||||
|
"""Return current preset name."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def set_preset(
|
||||||
|
self,
|
||||||
|
preset_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Set a light preset for the device."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def save_preset(
|
||||||
|
self,
|
||||||
|
preset_name: str,
|
||||||
|
preset_info: LightState,
|
||||||
|
) -> None:
|
||||||
|
"""Update the preset with *preset_name* with the new *preset_info*."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def has_save_preset(self) -> bool:
|
||||||
|
"""Return True if the device supports updating presets."""
|
@ -11,7 +11,7 @@ from pydantic.v1 import BaseModel, Field, root_validator
|
|||||||
|
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..interfaces.light import HSV, ColorTempRange, LightPreset
|
from ..interfaces.light import HSV, ColorTempRange
|
||||||
from ..module import Module
|
from ..module import Module
|
||||||
from ..protocol import BaseProtocol
|
from ..protocol import BaseProtocol
|
||||||
from .iotdevice import IotDevice, KasaException, requires_update
|
from .iotdevice import IotDevice, KasaException, requires_update
|
||||||
@ -21,6 +21,7 @@ from .modules import (
|
|||||||
Countdown,
|
Countdown,
|
||||||
Emeter,
|
Emeter,
|
||||||
Light,
|
Light,
|
||||||
|
LightPreset,
|
||||||
Schedule,
|
Schedule,
|
||||||
Time,
|
Time,
|
||||||
Usage,
|
Usage,
|
||||||
@ -178,7 +179,7 @@ class IotBulb(IotDevice):
|
|||||||
Bulb configuration presets can be accessed using the :func:`presets` property:
|
Bulb configuration presets can be accessed using the :func:`presets` property:
|
||||||
|
|
||||||
>>> bulb.presets
|
>>> 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`
|
To modify an existing preset, pass :class:`~kasa.interfaces.light.LightPreset`
|
||||||
instance to :func:`save_preset` method:
|
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.IotEmeter, Emeter(self, self.emeter_type))
|
||||||
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
|
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
|
||||||
self.add_module(Module.IotCloud, Cloud(self, "smartlife.iot.common.cloud"))
|
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
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -320,7 +322,7 @@ class IotBulb(IotDevice):
|
|||||||
# TODO: add warning and refer to use light.state?
|
# TODO: add warning and refer to use light.state?
|
||||||
return await self._query_helper(self.LIGHT_SERVICE, "get_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
|
self, state: dict, *, transition: int | None = None
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Set the light state."""
|
"""Set the light state."""
|
||||||
@ -400,7 +402,7 @@ class IotBulb(IotDevice):
|
|||||||
self._raise_for_invalid_brightness(value)
|
self._raise_for_invalid_brightness(value)
|
||||||
light_state["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
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -436,7 +438,7 @@ class IotBulb(IotDevice):
|
|||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
light_state["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)
|
||||||
|
|
||||||
def _raise_for_invalid_brightness(self, value):
|
def _raise_for_invalid_brightness(self, value):
|
||||||
if not isinstance(value, int) or not (0 <= value <= 100):
|
if not isinstance(value, int) or not (0 <= value <= 100):
|
||||||
@ -467,7 +469,7 @@ class IotBulb(IotDevice):
|
|||||||
self._raise_for_invalid_brightness(brightness)
|
self._raise_for_invalid_brightness(brightness)
|
||||||
|
|
||||||
light_state = {"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
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -481,14 +483,14 @@ class IotBulb(IotDevice):
|
|||||||
|
|
||||||
:param int transition: transition in milliseconds.
|
: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:
|
async def turn_on(self, *, transition: int | None = None, **kwargs) -> dict:
|
||||||
"""Turn the bulb on.
|
"""Turn the bulb on.
|
||||||
|
|
||||||
:param int transition: transition in milliseconds.
|
: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
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -505,28 +507,6 @@ class IotBulb(IotDevice):
|
|||||||
"smartlife.iot.common.system", "set_dev_alias", {"alias": alias}
|
"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
|
@property
|
||||||
def max_device_response_size(self) -> int:
|
def max_device_response_size(self) -> int:
|
||||||
"""Returns the maximum response size the device can safely construct."""
|
"""Returns the maximum response size the device can safely construct."""
|
||||||
|
@ -312,11 +312,13 @@ class IotDevice(Device):
|
|||||||
|
|
||||||
await self._modular_update(req)
|
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:
|
if not self._features:
|
||||||
await self._initialize_features()
|
await self._initialize_features()
|
||||||
|
|
||||||
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
|
|
||||||
|
|
||||||
async def _initialize_modules(self):
|
async def _initialize_modules(self):
|
||||||
"""Initialize modules not added in init."""
|
"""Initialize modules not added in init."""
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from .emeter import Emeter
|
|||||||
from .led import Led
|
from .led import Led
|
||||||
from .light import Light
|
from .light import Light
|
||||||
from .lighteffect import LightEffect
|
from .lighteffect import LightEffect
|
||||||
|
from .lightpreset import IotLightPreset, LightPreset
|
||||||
from .motion import Motion
|
from .motion import Motion
|
||||||
from .rulemodule import Rule, RuleModule
|
from .rulemodule import Rule, RuleModule
|
||||||
from .schedule import Schedule
|
from .schedule import Schedule
|
||||||
@ -23,6 +24,8 @@ __all__ = [
|
|||||||
"Led",
|
"Led",
|
||||||
"Light",
|
"Light",
|
||||||
"LightEffect",
|
"LightEffect",
|
||||||
|
"LightPreset",
|
||||||
|
"IotLightPreset",
|
||||||
"Motion",
|
"Motion",
|
||||||
"Rule",
|
"Rule",
|
||||||
"RuleModule",
|
"RuleModule",
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, 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
|
from ...interfaces.light import HSV, ColorTempRange, LightState
|
||||||
from ...interfaces.light import Light as LightInterface
|
from ...interfaces.light import Light as LightInterface
|
||||||
from ..iotmodule import IotModule
|
from ..iotmodule import IotModule
|
||||||
|
|
||||||
@ -198,3 +199,23 @@ class Light(IotModule, LightInterface):
|
|||||||
return await bulb._set_color_temp(
|
return await bulb._set_color_temp(
|
||||||
temp, brightness=brightness, transition=transition
|
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))
|
@ -36,6 +36,7 @@ class Module(ABC):
|
|||||||
LightEffect: Final[ModuleName[interfaces.LightEffect]] = ModuleName("LightEffect")
|
LightEffect: Final[ModuleName[interfaces.LightEffect]] = ModuleName("LightEffect")
|
||||||
Led: Final[ModuleName[interfaces.Led]] = ModuleName("Led")
|
Led: Final[ModuleName[interfaces.Led]] = ModuleName("Led")
|
||||||
Light: Final[ModuleName[interfaces.Light]] = ModuleName("Light")
|
Light: Final[ModuleName[interfaces.Light]] = ModuleName("Light")
|
||||||
|
LightPreset: Final[ModuleName[interfaces.LightPreset]] = ModuleName("LightPreset")
|
||||||
|
|
||||||
# IOT only Modules
|
# IOT only Modules
|
||||||
IotAmbientLight: Final[ModuleName[iot.AmbientLight]] = ModuleName("ambient")
|
IotAmbientLight: Final[ModuleName[iot.AmbientLight]] = ModuleName("ambient")
|
||||||
|
@ -18,6 +18,7 @@ from .humiditysensor import HumiditySensor
|
|||||||
from .led import Led
|
from .led import Led
|
||||||
from .light import Light
|
from .light import Light
|
||||||
from .lighteffect import LightEffect
|
from .lighteffect import LightEffect
|
||||||
|
from .lightpreset import LightPreset
|
||||||
from .lightstripeffect import LightStripEffect
|
from .lightstripeffect import LightStripEffect
|
||||||
from .lighttransition import LightTransition
|
from .lighttransition import LightTransition
|
||||||
from .reportmode import ReportMode
|
from .reportmode import ReportMode
|
||||||
@ -41,6 +42,7 @@ __all__ = [
|
|||||||
"Led",
|
"Led",
|
||||||
"Brightness",
|
"Brightness",
|
||||||
"Fan",
|
"Fan",
|
||||||
|
"LightPreset",
|
||||||
"Firmware",
|
"Firmware",
|
||||||
"Cloud",
|
"Cloud",
|
||||||
"Light",
|
"Light",
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
from ...exceptions import KasaException
|
from ...exceptions import KasaException
|
||||||
from ...interfaces.light import HSV, ColorTempRange
|
from ...interfaces.light import HSV, ColorTempRange, LightState
|
||||||
from ...interfaces.light import Light as LightInterface
|
from ...interfaces.light import Light as LightInterface
|
||||||
from ...module import Module
|
from ...module import Module
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
@ -124,3 +126,14 @@ class Light(SmartModule, LightInterface):
|
|||||||
def has_effects(self) -> bool:
|
def has_effects(self) -> bool:
|
||||||
"""Return True if the device supports effects."""
|
"""Return True if the device supports effects."""
|
||||||
return Module.LightEffect in self._device.modules
|
return Module.LightEffect in self._device.modules
|
||||||
|
|
||||||
|
async def set_state(self, state: LightState) -> dict:
|
||||||
|
"""Set the light state."""
|
||||||
|
state_dict = asdict(state)
|
||||||
|
# brightness of 0 turns off the light, it's not a valid brightness
|
||||||
|
if state.brightness and state.brightness == 0:
|
||||||
|
state_dict["device_on"] = False
|
||||||
|
del state_dict["brightness"]
|
||||||
|
|
||||||
|
params = {k: v for k, v in state_dict.items() if v is not None}
|
||||||
|
return await self.call("set_device_info", params)
|
||||||
|
142
kasa/smart/modules/lightpreset.py
Normal file
142
kasa/smart/modules/lightpreset.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
"""Module for light effects."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from typing import TYPE_CHECKING, Sequence
|
||||||
|
|
||||||
|
from ...interfaces import LightPreset as LightPresetInterface
|
||||||
|
from ...interfaces import LightState
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
|
class LightPreset(SmartModule, LightPresetInterface):
|
||||||
|
"""Implementation of light presets."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "preset"
|
||||||
|
QUERY_GETTER_NAME = "get_preset_rules"
|
||||||
|
|
||||||
|
SYS_INFO_STATE_KEY = "preset_state"
|
||||||
|
|
||||||
|
_presets: dict[str, LightState]
|
||||||
|
_preset_list: list[str]
|
||||||
|
|
||||||
|
def __init__(self, device: SmartDevice, module: str):
|
||||||
|
super().__init__(device, module)
|
||||||
|
self._state_in_sysinfo = self.SYS_INFO_STATE_KEY in device.sys_info
|
||||||
|
self._brightness_only: bool = False
|
||||||
|
|
||||||
|
def _post_update_hook(self):
|
||||||
|
"""Update the internal presets."""
|
||||||
|
index = 0
|
||||||
|
self._presets = {}
|
||||||
|
|
||||||
|
state_key = "states" if not self._state_in_sysinfo else self.SYS_INFO_STATE_KEY
|
||||||
|
if preset_states := self.data.get(state_key):
|
||||||
|
for preset_state in preset_states:
|
||||||
|
color_temp = preset_state.get("color_temp")
|
||||||
|
hue = preset_state.get("hue")
|
||||||
|
saturation = preset_state.get("saturation")
|
||||||
|
self._presets[f"Light preset {index + 1}"] = LightState(
|
||||||
|
brightness=preset_state["brightness"],
|
||||||
|
color_temp=color_temp,
|
||||||
|
hue=hue,
|
||||||
|
saturation=saturation,
|
||||||
|
)
|
||||||
|
if color_temp is None and hue is None and saturation is None:
|
||||||
|
self._brightness_only = True
|
||||||
|
index = index + 1
|
||||||
|
elif preset_brightnesses := self.data.get("brightness"):
|
||||||
|
self._brightness_only = True
|
||||||
|
for preset_brightness in preset_brightnesses:
|
||||||
|
self._presets[f"Brightness preset {index + 1}"] = LightState(
|
||||||
|
brightness=preset_brightness,
|
||||||
|
)
|
||||||
|
index = index + 1
|
||||||
|
|
||||||
|
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', 'Light preset 1', 'Light preset 2', ...]
|
||||||
|
"""
|
||||||
|
return self._preset_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_states_list(self) -> Sequence[LightState]:
|
||||||
|
"""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[SmartModule.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
|
||||||
|
and preset.saturation == s
|
||||||
|
):
|
||||||
|
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[SmartModule.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 self._device.modules[SmartModule.Light].set_state(preset)
|
||||||
|
|
||||||
|
async def save_preset(
|
||||||
|
self,
|
||||||
|
preset_name: str,
|
||||||
|
preset_state: LightState,
|
||||||
|
) -> None:
|
||||||
|
"""Update the preset with preset_name with the new preset_info."""
|
||||||
|
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)
|
||||||
|
if self._brightness_only:
|
||||||
|
bright_list = [state.brightness for state in self._presets.values()]
|
||||||
|
bright_list[index] = preset_state.brightness
|
||||||
|
await self.call("set_preset_rules", {"brightness": bright_list})
|
||||||
|
else:
|
||||||
|
state_params = asdict(preset_state)
|
||||||
|
new_info = {k: v for k, v in state_params.items() if v is not None}
|
||||||
|
await self.call("edit_preset_rules", {"index": index, "state": new_info})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_save_preset(self) -> bool:
|
||||||
|
"""Return True if the device supports updating presets."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
if self._state_in_sysinfo: # Child lights can have states in the child info
|
||||||
|
return {}
|
||||||
|
return {self.QUERY_GETTER_NAME: None}
|
@ -338,7 +338,6 @@ class SmartDevice(Device):
|
|||||||
module._initialize_features()
|
module._initialize_features()
|
||||||
for feat in module._module_features.values():
|
for feat in module._module_features.values():
|
||||||
self._add_feature(feat)
|
self._add_feature(feat)
|
||||||
|
|
||||||
for child in self._children.values():
|
for child in self._children.values():
|
||||||
await child._initialize_features()
|
await child._initialize_features()
|
||||||
|
|
||||||
|
@ -157,6 +157,8 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
elif child_method == "set_device_info":
|
elif child_method == "set_device_info":
|
||||||
info.update(child_params)
|
info.update(child_params)
|
||||||
return {"error_code": 0}
|
return {"error_code": 0}
|
||||||
|
elif child_method == "set_preset_rules":
|
||||||
|
return self._set_child_preset_rules(info, child_params)
|
||||||
elif (
|
elif (
|
||||||
# FIXTURE_MISSING is for service calls not in place when
|
# FIXTURE_MISSING is for service calls not in place when
|
||||||
# SMART fixtures started to be generated
|
# SMART fixtures started to be generated
|
||||||
@ -205,6 +207,30 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
info["get_led_info"]["led_status"] = params["led_rule"] != "never"
|
info["get_led_info"]["led_status"] = params["led_rule"] != "never"
|
||||||
info["get_led_info"]["led_rule"] = params["led_rule"]
|
info["get_led_info"]["led_rule"] = params["led_rule"]
|
||||||
|
|
||||||
|
def _set_preset_rules(self, info, params):
|
||||||
|
"""Set or remove values as per the device behaviour."""
|
||||||
|
if "brightness" not in info["get_preset_rules"]:
|
||||||
|
return {"error_code": SmartErrorCode.PARAMS_ERROR}
|
||||||
|
info["get_preset_rules"]["brightness"] = params["brightness"]
|
||||||
|
return {"error_code": 0}
|
||||||
|
|
||||||
|
def _set_child_preset_rules(self, info, params):
|
||||||
|
"""Set or remove values as per the device behaviour."""
|
||||||
|
# So far the only child device with light preset (KS240) has the
|
||||||
|
# data available to read in the device_info. If a child device
|
||||||
|
# appears that doesn't have this this will need to be extended.
|
||||||
|
if "preset_state" not in info:
|
||||||
|
return {"error_code": SmartErrorCode.PARAMS_ERROR}
|
||||||
|
info["preset_state"] = [{"brightness": b} for b in params["brightness"]]
|
||||||
|
return {"error_code": 0}
|
||||||
|
|
||||||
|
def _edit_preset_rules(self, info, params):
|
||||||
|
"""Set or remove values as per the device behaviour."""
|
||||||
|
if "states" not in info["get_preset_rules"] is None:
|
||||||
|
return {"error_code": SmartErrorCode.PARAMS_ERROR}
|
||||||
|
info["get_preset_rules"]["states"][params["index"]] = params["state"]
|
||||||
|
return {"error_code": 0}
|
||||||
|
|
||||||
def _send_request(self, request_dict: dict):
|
def _send_request(self, request_dict: dict):
|
||||||
method = request_dict["method"]
|
method = request_dict["method"]
|
||||||
params = request_dict["params"]
|
params = request_dict["params"]
|
||||||
@ -276,6 +302,10 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
elif method == "set_led_info":
|
elif method == "set_led_info":
|
||||||
self._set_led_info(info, params)
|
self._set_led_info(info, params)
|
||||||
return {"error_code": 0}
|
return {"error_code": 0}
|
||||||
|
elif method == "set_preset_rules":
|
||||||
|
return self._set_preset_rules(info, params)
|
||||||
|
elif method == "edit_preset_rules":
|
||||||
|
return self._edit_preset_rules(info, params)
|
||||||
elif method[:4] == "set_":
|
elif method[:4] == "set_":
|
||||||
target_method = f"get_{method[4:]}"
|
target_method = f"get_{method[4:]}"
|
||||||
info[target_method].update(params)
|
info[target_method].update(params)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from voluptuous import (
|
from voluptuous import (
|
||||||
All,
|
All,
|
||||||
@ -7,7 +9,7 @@ from voluptuous import (
|
|||||||
Schema,
|
Schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
from kasa import Device, DeviceType, KasaException, LightPreset, Module
|
from kasa import Device, DeviceType, IotLightPreset, KasaException, Module
|
||||||
from kasa.iot import IotBulb, IotDimmer
|
from kasa.iot import IotBulb, IotDimmer
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
@ -85,7 +87,7 @@ async def test_hsv(dev: Device, turn_on):
|
|||||||
|
|
||||||
@color_bulb_iot
|
@color_bulb_iot
|
||||||
async def test_set_hsv_transition(dev: IotBulb, mocker):
|
async def test_set_hsv_transition(dev: IotBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||||
await dev.set_hsv(10, 10, 100, transition=1000)
|
await dev.set_hsv(10, 10, 100, transition=1000)
|
||||||
|
|
||||||
set_light_state.assert_called_with(
|
set_light_state.assert_called_with(
|
||||||
@ -158,7 +160,7 @@ async def test_try_set_colortemp(dev: Device, turn_on):
|
|||||||
|
|
||||||
@variable_temp_iot
|
@variable_temp_iot
|
||||||
async def test_set_color_temp_transition(dev: IotBulb, mocker):
|
async def test_set_color_temp_transition(dev: IotBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||||
await dev.set_color_temp(2700, transition=100)
|
await dev.set_color_temp(2700, transition=100)
|
||||||
|
|
||||||
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
|
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
|
||||||
@ -224,7 +226,7 @@ async def test_dimmable_brightness(dev: IotBulb, turn_on):
|
|||||||
|
|
||||||
@bulb_iot
|
@bulb_iot
|
||||||
async def test_turn_on_transition(dev: IotBulb, mocker):
|
async def test_turn_on_transition(dev: IotBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||||
await dev.turn_on(transition=1000)
|
await dev.turn_on(transition=1000)
|
||||||
|
|
||||||
set_light_state.assert_called_with({"on_off": 1}, transition=1000)
|
set_light_state.assert_called_with({"on_off": 1}, transition=1000)
|
||||||
@ -236,7 +238,7 @@ async def test_turn_on_transition(dev: IotBulb, mocker):
|
|||||||
|
|
||||||
@bulb_iot
|
@bulb_iot
|
||||||
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
|
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||||
await dev.set_brightness(10, transition=1000)
|
await dev.set_brightness(10, transition=1000)
|
||||||
|
|
||||||
set_light_state.assert_called_with({"brightness": 10}, transition=1000)
|
set_light_state.assert_called_with({"brightness": 10}, transition=1000)
|
||||||
@ -297,14 +299,14 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
|||||||
if not dev.presets:
|
if not dev.presets:
|
||||||
pytest.skip("Some strips do not support presets")
|
pytest.skip("Some strips do not support presets")
|
||||||
|
|
||||||
data = {
|
data: dict[str, int | None] = {
|
||||||
"index": 0,
|
"index": 0,
|
||||||
"brightness": 10,
|
"brightness": 10,
|
||||||
"hue": 0,
|
"hue": 0,
|
||||||
"saturation": 0,
|
"saturation": 0,
|
||||||
"color_temp": 0,
|
"color_temp": 0,
|
||||||
}
|
}
|
||||||
preset = LightPreset(**data)
|
preset = IotLightPreset(**data) # type: ignore[call-arg, arg-type]
|
||||||
|
|
||||||
assert preset.index == 0
|
assert preset.index == 0
|
||||||
assert preset.brightness == 10
|
assert preset.brightness == 10
|
||||||
@ -318,7 +320,7 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
|||||||
|
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
await dev.save_preset(
|
await dev.save_preset(
|
||||||
LightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
|
IotLightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) # type: ignore[call-arg]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -327,11 +329,11 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
|||||||
("preset", "payload"),
|
("preset", "payload"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
LightPreset(index=0, hue=0, brightness=1, saturation=0),
|
IotLightPreset(index=0, hue=0, brightness=1, saturation=0), # type: ignore[call-arg]
|
||||||
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
|
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
LightPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
|
IotLightPreset(index=0, brightness=1, id="testid", mode=2, custom=0), # type: ignore[call-arg]
|
||||||
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
|
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from kasa import Device, Module
|
from kasa import Device, LightState, Module
|
||||||
from kasa.tests.device_fixtures import (
|
from kasa.tests.device_fixtures import (
|
||||||
|
bulb_iot,
|
||||||
dimmable_iot,
|
dimmable_iot,
|
||||||
dimmer_iot,
|
dimmer_iot,
|
||||||
lightstrip_iot,
|
lightstrip_iot,
|
||||||
@ -33,6 +34,12 @@ dimmable_smart = parametrize(
|
|||||||
)
|
)
|
||||||
dimmable = parametrize_combine([dimmable_smart, dimmer_iot, dimmable_iot])
|
dimmable = parametrize_combine([dimmable_smart, dimmer_iot, dimmable_iot])
|
||||||
|
|
||||||
|
light_preset_smart = parametrize(
|
||||||
|
"has light preset smart", component_filter="preset", protocol_filter={"SMART"}
|
||||||
|
)
|
||||||
|
|
||||||
|
light_preset = parametrize_combine([light_preset_smart, bulb_iot])
|
||||||
|
|
||||||
|
|
||||||
@led
|
@led
|
||||||
async def test_led_module(dev: Device, mocker: MockerFixture):
|
async def test_led_module(dev: Device, mocker: MockerFixture):
|
||||||
@ -130,3 +137,80 @@ async def test_light_brightness(dev: Device):
|
|||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await light.set_brightness(feature.maximum_value + 10)
|
await light.set_brightness(feature.maximum_value + 10)
|
||||||
|
|
||||||
|
|
||||||
|
@light_preset
|
||||||
|
async def test_light_preset_module(dev: Device, mocker: MockerFixture):
|
||||||
|
"""Test light preset module."""
|
||||||
|
preset_mod = dev.modules[Module.LightPreset]
|
||||||
|
assert preset_mod
|
||||||
|
light_mod = dev.modules[Module.Light]
|
||||||
|
assert light_mod
|
||||||
|
feat = dev.features["light_preset"]
|
||||||
|
|
||||||
|
call = mocker.spy(light_mod, "set_state")
|
||||||
|
preset_list = preset_mod.preset_list
|
||||||
|
assert "Not set" in preset_list
|
||||||
|
assert preset_list.index("Not set") == 0
|
||||||
|
assert preset_list == feat.choices
|
||||||
|
|
||||||
|
assert preset_mod.has_save_preset is True
|
||||||
|
|
||||||
|
await light_mod.set_brightness(33) # Value that should not be a preset
|
||||||
|
assert call.call_count == 0
|
||||||
|
await dev.update()
|
||||||
|
assert preset_mod.preset == "Not set"
|
||||||
|
assert feat.value == "Not set"
|
||||||
|
|
||||||
|
if len(preset_list) == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
second_preset = preset_list[1]
|
||||||
|
await preset_mod.set_preset(second_preset)
|
||||||
|
assert call.call_count == 1
|
||||||
|
await dev.update()
|
||||||
|
assert preset_mod.preset == second_preset
|
||||||
|
assert feat.value == second_preset
|
||||||
|
|
||||||
|
last_preset = preset_list[len(preset_list) - 1]
|
||||||
|
await preset_mod.set_preset(last_preset)
|
||||||
|
assert call.call_count == 2
|
||||||
|
await dev.update()
|
||||||
|
assert preset_mod.preset == last_preset
|
||||||
|
assert feat.value == last_preset
|
||||||
|
|
||||||
|
# Test feature set
|
||||||
|
await feat.set_value(second_preset)
|
||||||
|
assert call.call_count == 3
|
||||||
|
await dev.update()
|
||||||
|
assert preset_mod.preset == second_preset
|
||||||
|
assert feat.value == second_preset
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await preset_mod.set_preset("foobar")
|
||||||
|
assert call.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
@light_preset
|
||||||
|
async def test_light_preset_save(dev: Device, mocker: MockerFixture):
|
||||||
|
"""Test saving a new preset value."""
|
||||||
|
preset_mod = dev.modules[Module.LightPreset]
|
||||||
|
assert preset_mod
|
||||||
|
preset_list = preset_mod.preset_list
|
||||||
|
if len(preset_list) == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
second_preset = preset_list[1]
|
||||||
|
if preset_mod.preset_states_list[0].hue is None:
|
||||||
|
new_preset = LightState(brightness=52)
|
||||||
|
else:
|
||||||
|
new_preset = LightState(brightness=52, color_temp=3000, hue=20, saturation=30)
|
||||||
|
await preset_mod.save_preset(second_preset, new_preset)
|
||||||
|
await dev.update()
|
||||||
|
new_preset_state = preset_mod.preset_states_list[0]
|
||||||
|
assert (
|
||||||
|
new_preset_state.brightness == new_preset.brightness
|
||||||
|
and new_preset_state.hue == new_preset.hue
|
||||||
|
and new_preset_state.saturation == new_preset.saturation
|
||||||
|
and new_preset_state.color_temp == new_preset.color_temp
|
||||||
|
)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Tests for all devices."""
|
"""Tests for all devices."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import pkgutil
|
import pkgutil
|
||||||
@ -11,6 +13,7 @@ import pytest
|
|||||||
import kasa
|
import kasa
|
||||||
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
|
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
|
||||||
from kasa.iot import IotDevice
|
from kasa.iot import IotDevice
|
||||||
|
from kasa.iot.modules import IotLightPreset
|
||||||
from kasa.smart import SmartChildDevice, SmartDevice
|
from kasa.smart import SmartChildDevice, SmartDevice
|
||||||
|
|
||||||
|
|
||||||
@ -238,3 +241,28 @@ async def test_deprecated_other_attributes(dev: Device):
|
|||||||
|
|
||||||
await _test_attribute(dev, "led", bool(led_module), "Led")
|
await _test_attribute(dev, "led", bool(led_module), "Led")
|
||||||
await _test_attribute(dev, "set_led", bool(led_module), "Led", True)
|
await _test_attribute(dev, "set_led", bool(led_module), "Led", True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_deprecated_light_preset_attributes(dev: Device):
|
||||||
|
preset = dev.modules.get(Module.LightPreset)
|
||||||
|
|
||||||
|
exc: type[AttributeError] | type[KasaException] | None = (
|
||||||
|
AttributeError if not preset else None
|
||||||
|
)
|
||||||
|
await _test_attribute(dev, "presets", bool(preset), "LightPreset", will_raise=exc)
|
||||||
|
|
||||||
|
exc = None
|
||||||
|
# deprecated save_preset not implemented for smart devices as it's unlikely anyone
|
||||||
|
# has an existing reliance on this for the newer devices.
|
||||||
|
if not preset or isinstance(dev, SmartDevice):
|
||||||
|
exc = AttributeError
|
||||||
|
elif len(preset.preset_states_list) == 0:
|
||||||
|
exc = KasaException
|
||||||
|
await _test_attribute(
|
||||||
|
dev,
|
||||||
|
"save_preset",
|
||||||
|
bool(preset),
|
||||||
|
"LightPreset",
|
||||||
|
IotLightPreset(index=0, hue=100, brightness=100, saturation=0, color_temp=0), # type: ignore[call-arg]
|
||||||
|
will_raise=exc,
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user