mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 11:43:34 +00:00
7fd5c213e6
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.
167 lines
6.3 KiB
Python
167 lines
6.3 KiB
Python
"""Module for light effects."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Sequence
|
|
from dataclasses import asdict
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ...interfaces import LightPreset as LightPresetInterface
|
|
from ...interfaces import LightState
|
|
from ..smartmodule import SmartModule, allow_update_after
|
|
|
|
if TYPE_CHECKING:
|
|
from ..smartdevice import SmartDevice
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class LightPreset(SmartModule, LightPresetInterface):
|
|
"""Implementation of light presets."""
|
|
|
|
REQUIRED_COMPONENT = "preset"
|
|
QUERY_GETTER_NAME = "get_preset_rules"
|
|
MINIMUM_UPDATE_INTERVAL_SECS = 60
|
|
|
|
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:
|
|
if "brightness" not in preset_state:
|
|
# Some devices can store effects as a preset. These will be ignored
|
|
# and handled in the effects module
|
|
if "lighting_effect" not in preset_state:
|
|
_LOGGER.info(
|
|
"Unexpected keys %s in preset", list(preset_state.keys())
|
|
)
|
|
continue
|
|
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)
|
|
|
|
@allow_update_after
|
|
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: {"start_index": 0}}
|
|
|
|
async def _check_supported(self):
|
|
"""Additional check to see if the module is supported by the device.
|
|
|
|
Parent devices that report components of children such as ks240 will not have
|
|
the brightness value is sysinfo.
|
|
"""
|
|
# Look in _device.sys_info here because self.data is either sys_info or
|
|
# get_preset_rules depending on whether it's a child device or not.
|
|
return "brightness" in self._device.sys_info
|