python-kasa/kasa/smart/modules/lightstripeffect.py
Steven B 7fd5c213e6
Defer module updates for less volatile modules (#1052)
Addresses stability issues on older hw device versions

 - Handles module timeout errors better by querying modules individually on errors and disabling problematic modules like Firmware that go out to the internet to get updates.
- Addresses an issue with the Led module on P100 hardware version 1.0 which appears to have a memory leak and will cause the device to crash after approximately 500 calls.
- Delays updates of modules that do not have regular changes like LightPreset and LightEffect and enables them to be updated on the next update cycle only if required values have changed.
2024-07-11 16:21:59 +01:00

178 lines
5.5 KiB
Python

"""Module for light effects."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..effects import EFFECT_MAPPING, EFFECT_NAMES, SmartLightEffect
from ..smartmodule import Module, SmartModule, allow_update_after
if TYPE_CHECKING:
from ..smartdevice import SmartDevice
class LightStripEffect(SmartModule, SmartLightEffect):
"""Implementation of dynamic light effects."""
REQUIRED_COMPONENT = "light_strip_lighting_effect"
def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module)
effect_list = [self.LIGHT_EFFECTS_OFF]
effect_list.extend(EFFECT_NAMES)
self._effect_list = effect_list
self._effect_mapping = EFFECT_MAPPING
@property
def name(self) -> str:
"""Name of the module.
By default smart modules are keyed in the module mapping by class name.
The name is overriden here as this module implements the same common interface
as the bulb light_effect and the assumption is a device only supports one
or the other.
"""
return "LightEffect"
@property
def effect(self) -> str:
"""Return effect state.
Example:
{'brightness': 50,
'custom': 0,
'enable': 0,
'id': '',
'name': ''}
"""
eff = self.data["lighting_effect"]
name = eff["name"]
# When devices are unpaired effect name is softAP which is not in our list
if eff["enable"] and name in self._effect_list:
return name
return self.LIGHT_EFFECTS_OFF
@property
def is_active(self) -> bool:
"""Return if effect is active."""
eff = self.data["lighting_effect"]
# softAP has enable=1, but brightness 0 which fails on tests
return bool(eff["enable"]) and eff["name"] in self._effect_list
@property
def brightness(self) -> int:
"""Return effect brightness."""
eff = self.data["lighting_effect"]
return eff["brightness"]
async def set_brightness(self, brightness: int, *, transition: int | None = None):
"""Set effect brightness."""
if brightness <= 0:
return await self.set_effect(self.LIGHT_EFFECTS_OFF)
# Need to pass bAdjusted to keep the existing effect running
eff = {"brightness": brightness, "bAdjusted": True}
return await self.set_custom_effect(eff)
@property
def effect_list(self) -> list[str]:
"""Return built-in effects list.
Example:
['Aurora', 'Bubbling Cauldron', ...]
"""
return self._effect_list
@allow_update_after
async def set_effect(
self,
effect: str,
*,
brightness: int | None = None,
transition: int | None = None,
) -> None:
"""Set an effect on the device.
If brightness or transition is defined,
its value will be used instead of the effect-specific default.
See :meth:`effect_list` for available effects,
or use :meth:`set_custom_effect` for custom effects.
:param str effect: The effect to set
:param int brightness: The wanted brightness
:param int transition: The wanted transition time
"""
brightness_module = self._device.modules[Module.Brightness]
if effect == self.LIGHT_EFFECTS_OFF:
state = self._device.modules[Module.Light].state
await self._device.modules[Module.Light].set_state(state)
return
if effect not in self._effect_mapping:
raise ValueError(f"The effect {effect} is not a built in effect.")
else:
effect_dict = self._effect_mapping[effect]
# Use explicitly given brightness
if brightness is not None:
effect_dict["brightness"] = brightness
# Fall back to brightness reported by the brightness module
elif brightness_module.brightness:
effect_dict["brightness"] = brightness_module.brightness
if transition is not None:
effect_dict["transition"] = transition
await self.set_custom_effect(effect_dict)
@allow_update_after
async def set_custom_effect(
self,
effect_dict: dict,
) -> None:
"""Set a custom effect on the device.
:param str effect_dict: The custom effect dict to set
"""
return await self.call(
"set_lighting_effect",
effect_dict,
)
@property
def has_custom_effects(self) -> bool:
"""Return True if the device supports setting custom effects."""
return True
def query(self):
"""Return the base query."""
return {}
@property # type: ignore
def _deprecated_effect(self) -> dict:
"""Return effect state.
Example:
{'brightness': 50,
'custom': 0,
'enable': 0,
'id': '',
'name': ''}
"""
# LightEffectModule returns the current effect name
# so return the dict here for backwards compatibility
return self.data["lighting_effect"]
@property # type: ignore
def _deprecated_effect_list(self) -> list[str] | None:
"""Return built-in effects list.
Example:
['Aurora', 'Bubbling Cauldron', ...]
"""
# LightEffectModule returns effect names along with a LIGHT_EFFECTS_OFF value
# so return the original effect names here for backwards compatibility
return EFFECT_NAMES