2024-05-19 10:20:18 +00:00
|
|
|
"""Module for light effects."""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-07-04 15:22:47 +00:00
|
|
|
import logging
|
2024-06-19 18:24:12 +00:00
|
|
|
from collections.abc import Sequence
|
2024-05-19 10:20:18 +00:00
|
|
|
from dataclasses import asdict
|
2024-06-19 18:24:12 +00:00
|
|
|
from typing import TYPE_CHECKING
|
2024-05-19 10:20:18 +00:00
|
|
|
|
|
|
|
from ...interfaces import LightPreset as LightPresetInterface
|
|
|
|
from ...interfaces import LightState
|
2024-07-11 15:21:59 +00:00
|
|
|
from ..smartmodule import SmartModule, allow_update_after
|
2024-05-19 10:20:18 +00:00
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
from ..smartdevice import SmartDevice
|
|
|
|
|
2024-07-04 15:22:47 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2024-05-19 10:20:18 +00:00
|
|
|
|
|
|
|
class LightPreset(SmartModule, LightPresetInterface):
|
|
|
|
"""Implementation of light presets."""
|
|
|
|
|
|
|
|
REQUIRED_COMPONENT = "preset"
|
|
|
|
QUERY_GETTER_NAME = "get_preset_rules"
|
2024-07-11 15:21:59 +00:00
|
|
|
MINIMUM_UPDATE_INTERVAL_SECS = 60
|
2024-05-19 10:20:18 +00:00
|
|
|
|
|
|
|
SYS_INFO_STATE_KEY = "preset_state"
|
|
|
|
|
|
|
|
_presets: dict[str, LightState]
|
|
|
|
_preset_list: list[str]
|
|
|
|
|
2024-11-10 18:55:13 +00:00
|
|
|
def __init__(self, device: SmartDevice, module: str) -> None:
|
2024-05-19 10:20:18 +00:00
|
|
|
super().__init__(device, module)
|
|
|
|
self._state_in_sysinfo = self.SYS_INFO_STATE_KEY in device.sys_info
|
|
|
|
self._brightness_only: bool = False
|
|
|
|
|
2024-11-10 18:55:13 +00:00
|
|
|
async def _post_update_hook(self) -> None:
|
2024-05-19 10:20:18 +00:00
|
|
|
"""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:
|
2024-07-04 15:22:47 +00:00
|
|
|
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
|
2024-05-19 10:20:18 +00:00
|
|
|
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,
|
2024-11-10 18:55:13 +00:00
|
|
|
) -> dict:
|
2024-05-19 10:20:18 +00:00
|
|
|
"""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}")
|
2024-11-10 18:55:13 +00:00
|
|
|
return await self._device.modules[SmartModule.Light].set_state(preset)
|
2024-05-19 10:20:18 +00:00
|
|
|
|
2024-07-11 15:21:59 +00:00
|
|
|
@allow_update_after
|
2024-05-19 10:20:18 +00:00
|
|
|
async def save_preset(
|
|
|
|
self,
|
|
|
|
preset_name: str,
|
|
|
|
preset_state: LightState,
|
2024-11-10 18:55:13 +00:00
|
|
|
) -> dict:
|
2024-05-19 10:20:18 +00:00
|
|
|
"""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
|
2024-11-10 18:55:13 +00:00
|
|
|
return await self.call("set_preset_rules", {"brightness": bright_list})
|
2024-05-19 10:20:18 +00:00
|
|
|
else:
|
|
|
|
state_params = asdict(preset_state)
|
|
|
|
new_info = {k: v for k, v in state_params.items() if v is not None}
|
2024-11-10 18:55:13 +00:00
|
|
|
return await self.call(
|
|
|
|
"edit_preset_rules", {"index": index, "state": new_info}
|
|
|
|
)
|
2024-05-19 10:20:18 +00:00
|
|
|
|
|
|
|
@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 {}
|
2024-07-23 18:02:20 +00:00
|
|
|
if self.supported_version < 3:
|
|
|
|
return {self.QUERY_GETTER_NAME: None}
|
|
|
|
|
2024-07-04 07:02:50 +00:00
|
|
|
return {self.QUERY_GETTER_NAME: {"start_index": 0}}
|
2024-06-10 13:13:46 +00:00
|
|
|
|
2024-11-10 18:55:13 +00:00
|
|
|
async def _check_supported(self) -> bool:
|
2024-06-10 13:13:46 +00:00
|
|
|
"""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
|