mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Add LightEffectModule for dynamic light effects on SMART bulbs (#887)
Support the `light_effect` module which allows setting the effect to Off or Party or Relax. Uses the new `Feature.Type.Choice`. Does not currently allow editing of effects.
This commit is contained in:
@@ -15,6 +15,7 @@ from .firmware import Firmware
|
||||
from .frostprotection import FrostProtectionModule
|
||||
from .humidity import HumiditySensor
|
||||
from .ledmodule import LedModule
|
||||
from .lighteffectmodule import LightEffectModule
|
||||
from .lighttransitionmodule import LightTransitionModule
|
||||
from .reportmodule import ReportModule
|
||||
from .temperature import TemperatureSensor
|
||||
@@ -39,6 +40,7 @@ __all__ = [
|
||||
"FanModule",
|
||||
"Firmware",
|
||||
"CloudModule",
|
||||
"LightEffectModule",
|
||||
"LightTransitionModule",
|
||||
"ColorTemperatureModule",
|
||||
"ColorModule",
|
||||
|
112
kasa/smart/modules/lighteffectmodule.py
Normal file
112
kasa/smart/modules/lighteffectmodule.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Module for light effects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import copy
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...feature import Feature
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class LightEffectModule(SmartModule):
|
||||
"""Implementation of dynamic light effects."""
|
||||
|
||||
REQUIRED_COMPONENT = "light_effect"
|
||||
QUERY_GETTER_NAME = "get_dynamic_light_effect_rules"
|
||||
AVAILABLE_BULB_EFFECTS = {
|
||||
"L1": "Party",
|
||||
"L2": "Relax",
|
||||
}
|
||||
LIGHT_EFFECTS_OFF = "Off"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
self._scenes_names_to_id: dict[str, str] = {}
|
||||
|
||||
def _initialize_features(self):
|
||||
"""Initialize features."""
|
||||
device = self._device
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
"Light effect",
|
||||
container=self,
|
||||
attribute_getter="effect",
|
||||
attribute_setter="set_effect",
|
||||
category=Feature.Category.Config,
|
||||
type=Feature.Type.Choice,
|
||||
choices_getter="effect_list",
|
||||
)
|
||||
)
|
||||
|
||||
def _initialize_effects(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return built-in effects."""
|
||||
# Copy the effects so scene name updates do not update the underlying dict.
|
||||
effects = copy.deepcopy(
|
||||
{effect["id"]: effect for effect in self.data["rule_list"]}
|
||||
)
|
||||
for effect in effects.values():
|
||||
if not effect["scene_name"]:
|
||||
# If the name has not been edited scene_name will be an empty string
|
||||
effect["scene_name"] = self.AVAILABLE_BULB_EFFECTS[effect["id"]]
|
||||
else:
|
||||
# Otherwise it will be b64 encoded
|
||||
effect["scene_name"] = base64.b64decode(effect["scene_name"]).decode()
|
||||
self._scenes_names_to_id = {
|
||||
effect["scene_name"]: effect["id"] for effect in effects.values()
|
||||
}
|
||||
return effects
|
||||
|
||||
@property
|
||||
def effect_list(self) -> list[str] | None:
|
||||
"""Return built-in effects list.
|
||||
|
||||
Example:
|
||||
['Party', 'Relax', ...]
|
||||
"""
|
||||
effects = [self.LIGHT_EFFECTS_OFF]
|
||||
effects.extend(
|
||||
[effect["scene_name"] for effect in self._initialize_effects().values()]
|
||||
)
|
||||
return effects
|
||||
|
||||
@property
|
||||
def effect(self) -> str:
|
||||
"""Return effect name."""
|
||||
# get_dynamic_light_effect_rules also has an enable property and current_rule_id
|
||||
# property that could be used here as an alternative
|
||||
if self._device._info["dynamic_light_effect_enable"]:
|
||||
return self._initialize_effects()[
|
||||
self._device._info["dynamic_light_effect_id"]
|
||||
]["scene_name"]
|
||||
return self.LIGHT_EFFECTS_OFF
|
||||
|
||||
async def set_effect(
|
||||
self,
|
||||
effect: str,
|
||||
) -> None:
|
||||
"""Set an effect for the device.
|
||||
|
||||
The device doesn't store an active effect while not enabled so store locally.
|
||||
"""
|
||||
if effect != self.LIGHT_EFFECTS_OFF and effect not in self._scenes_names_to_id:
|
||||
raise ValueError(
|
||||
f"Cannot set light effect to {effect}, possible values "
|
||||
f"are: {self.LIGHT_EFFECTS_OFF} "
|
||||
f"{' '.join(self._scenes_names_to_id.keys())}"
|
||||
)
|
||||
enable = effect != self.LIGHT_EFFECTS_OFF
|
||||
params: dict[str, bool | str] = {"enable": enable}
|
||||
if enable:
|
||||
effect_id = self._scenes_names_to_id[effect]
|
||||
params["id"] = effect_id
|
||||
return await self.call("set_dynamic_light_effect_rule_enable", params)
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
return {self.QUERY_GETTER_NAME: {"start_index": 0}}
|
@@ -40,11 +40,6 @@ if TYPE_CHECKING:
|
||||
# same issue, homekit perhaps?
|
||||
WALL_SWITCH_PARENT_ONLY_MODULES = [DeviceModule, TimeModule, Firmware, CloudModule]
|
||||
|
||||
AVAILABLE_BULB_EFFECTS = {
|
||||
"L1": "Party",
|
||||
"L2": "Relax",
|
||||
}
|
||||
|
||||
|
||||
# Device must go last as the other interfaces also inherit Device
|
||||
# and python needs a consistent method resolution order.
|
||||
@@ -683,44 +678,6 @@ class SmartDevice(Bulb, Fan, Device):
|
||||
ColorTemperatureModule, self.modules["ColorTemperatureModule"]
|
||||
).valid_temperature_range
|
||||
|
||||
@property
|
||||
def has_effects(self) -> bool:
|
||||
"""Return True if the device supports effects."""
|
||||
return "dynamic_light_effect_enable" in self._info
|
||||
|
||||
@property
|
||||
def effect(self) -> dict:
|
||||
"""Return effect state.
|
||||
|
||||
This follows the format used by SmartLightStrip.
|
||||
|
||||
Example:
|
||||
{'brightness': 50,
|
||||
'custom': 0,
|
||||
'enable': 0,
|
||||
'id': '',
|
||||
'name': ''}
|
||||
"""
|
||||
# If no effect is active, dynamic_light_effect_id does not appear in info
|
||||
current_effect = self._info.get("dynamic_light_effect_id", "")
|
||||
data = {
|
||||
"brightness": self.brightness,
|
||||
"enable": current_effect != "",
|
||||
"id": current_effect,
|
||||
"name": AVAILABLE_BULB_EFFECTS.get(current_effect, ""),
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def effect_list(self) -> list[str] | None:
|
||||
"""Return built-in effects list.
|
||||
|
||||
Example:
|
||||
['Party', 'Relax', ...]
|
||||
"""
|
||||
return list(AVAILABLE_BULB_EFFECTS.keys()) if self.has_effects else None
|
||||
|
||||
@property
|
||||
def hsv(self) -> HSV:
|
||||
"""Return the current HSV state of the bulb.
|
||||
@@ -807,17 +764,12 @@ class SmartDevice(Bulb, Fan, Device):
|
||||
brightness
|
||||
)
|
||||
|
||||
async def set_effect(
|
||||
self,
|
||||
effect: str,
|
||||
*,
|
||||
brightness: int | None = None,
|
||||
transition: int | None = None,
|
||||
) -> None:
|
||||
"""Set an effect on the device."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def presets(self) -> list[BulbPreset]:
|
||||
"""Return a list of available bulb setting presets."""
|
||||
return []
|
||||
|
||||
@property
|
||||
def has_effects(self) -> bool:
|
||||
"""Return True if the device supports effects."""
|
||||
return "LightEffectModule" in self.modules
|
||||
|
Reference in New Issue
Block a user