python-kasa/kasa/smart/modules/lightpreset.py
2024-11-10 18:55:13 +00:00

172 lines
6.4 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) -> None:
super().__init__(device, module)
self._state_in_sysinfo = self.SYS_INFO_STATE_KEY in device.sys_info
self._brightness_only: bool = False
async def _post_update_hook(self) -> None:
"""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,
) -> dict:
"""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}")
return await self._device.modules[SmartModule.Light].set_state(preset)
@allow_update_after
async def save_preset(
self,
preset_name: str,
preset_state: LightState,
) -> dict:
"""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
return 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}
return 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 {}
if self.supported_version < 3:
return {self.QUERY_GETTER_NAME: None}
return {self.QUERY_GETTER_NAME: {"start_index": 0}}
async def _check_supported(self) -> bool:
"""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