2024-02-19 19:39:20 +00:00
|
|
|
"""Module for smooth light transitions."""
|
2024-04-16 18:21:20 +00:00
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-06-27 17:52:54 +00:00
|
|
|
from typing import TYPE_CHECKING, TypedDict
|
2024-02-19 19:39:20 +00:00
|
|
|
|
2024-02-24 01:16:43 +00:00
|
|
|
from ...exceptions import KasaException
|
2024-04-24 16:38:52 +00:00
|
|
|
from ...feature import Feature
|
2024-02-19 19:39:20 +00:00
|
|
|
from ..smartmodule import SmartModule
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
from ..smartdevice import SmartDevice
|
|
|
|
|
|
|
|
|
2024-06-27 17:52:54 +00:00
|
|
|
class _State(TypedDict):
|
|
|
|
duration: int
|
|
|
|
enable: bool
|
|
|
|
max_duration: int
|
|
|
|
|
|
|
|
|
2024-05-11 18:28:18 +00:00
|
|
|
class LightTransition(SmartModule):
|
2024-02-19 19:39:20 +00:00
|
|
|
"""Implementation of gradual on/off."""
|
|
|
|
|
|
|
|
REQUIRED_COMPONENT = "on_off_gradually"
|
|
|
|
QUERY_GETTER_NAME = "get_on_off_gradually_info"
|
2024-02-24 01:16:43 +00:00
|
|
|
MAXIMUM_DURATION = 60
|
2024-02-19 19:39:20 +00:00
|
|
|
|
2024-06-27 17:52:54 +00:00
|
|
|
# Key in sysinfo that indicates state can be retrieved from there.
|
|
|
|
# Usually only for child lights, i.e, ks240.
|
|
|
|
SYS_INFO_STATE_KEYS = (
|
|
|
|
"gradually_on_mode",
|
|
|
|
"gradually_off_mode",
|
|
|
|
"fade_on_time",
|
|
|
|
"fade_off_time",
|
|
|
|
)
|
|
|
|
|
|
|
|
_on_state: _State
|
|
|
|
_off_state: _State
|
|
|
|
_enabled: bool
|
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
def __init__(self, device: SmartDevice, module: str):
|
2024-02-19 19:39:20 +00:00
|
|
|
super().__init__(device, module)
|
2024-06-27 17:52:54 +00:00
|
|
|
self._state_in_sysinfo = all(
|
|
|
|
key in device.sys_info for key in self.SYS_INFO_STATE_KEYS
|
|
|
|
)
|
|
|
|
self._supports_on_and_off: bool = self.supported_version > 1
|
2024-02-24 01:16:43 +00:00
|
|
|
|
2024-06-27 17:52:54 +00:00
|
|
|
def _initialize_features(self):
|
|
|
|
"""Initialize features."""
|
2024-02-24 01:16:43 +00:00
|
|
|
icon = "mdi:transition"
|
2024-06-27 17:52:54 +00:00
|
|
|
if not self._supports_on_and_off:
|
2024-02-24 01:16:43 +00:00
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
|
|
|
device=self._device,
|
|
|
|
container=self,
|
2024-05-07 09:13:35 +00:00
|
|
|
id="smooth_transitions",
|
2024-02-24 01:16:43 +00:00
|
|
|
name="Smooth transitions",
|
|
|
|
icon=icon,
|
2024-06-27 17:52:54 +00:00
|
|
|
attribute_getter="enabled",
|
|
|
|
attribute_setter="set_enabled",
|
2024-04-24 16:38:52 +00:00
|
|
|
type=Feature.Type.Switch,
|
2024-02-24 01:16:43 +00:00
|
|
|
)
|
|
|
|
)
|
2024-06-27 17:52:54 +00:00
|
|
|
else:
|
2024-02-24 01:16:43 +00:00
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
|
|
|
self._device,
|
2024-05-07 09:13:35 +00:00
|
|
|
id="smooth_transition_on",
|
|
|
|
name="Smooth transition on",
|
2024-02-24 01:16:43 +00:00
|
|
|
container=self,
|
|
|
|
attribute_getter="turn_on_transition",
|
|
|
|
attribute_setter="set_turn_on_transition",
|
|
|
|
icon=icon,
|
2024-04-24 16:38:52 +00:00
|
|
|
type=Feature.Type.Number,
|
2024-06-27 17:52:54 +00:00
|
|
|
maximum_value=self._turn_on_transition_max,
|
2024-02-24 01:16:43 +00:00
|
|
|
)
|
2024-06-27 17:52:54 +00:00
|
|
|
)
|
2024-02-24 01:16:43 +00:00
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
|
|
|
self._device,
|
2024-05-07 09:13:35 +00:00
|
|
|
id="smooth_transition_off",
|
|
|
|
name="Smooth transition off",
|
2024-02-24 01:16:43 +00:00
|
|
|
container=self,
|
|
|
|
attribute_getter="turn_off_transition",
|
|
|
|
attribute_setter="set_turn_off_transition",
|
|
|
|
icon=icon,
|
2024-04-24 16:38:52 +00:00
|
|
|
type=Feature.Type.Number,
|
2024-06-27 17:52:54 +00:00
|
|
|
maximum_value=self._turn_off_transition_max,
|
2024-02-24 01:16:43 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2024-06-27 17:52:54 +00:00
|
|
|
def _post_update_hook(self) -> None:
|
|
|
|
"""Update the states."""
|
|
|
|
# Assumes any device with state in sysinfo supports on and off and
|
|
|
|
# has maximum values for both.
|
|
|
|
# v2 adds separate on & off states
|
|
|
|
# v3 adds max_duration except for ks240 which is v2 but supports it
|
|
|
|
if not self._supports_on_and_off:
|
|
|
|
self._enabled = self.data["enable"]
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._state_in_sysinfo:
|
|
|
|
on_max = self._device.sys_info.get(
|
|
|
|
"max_fade_on_time", self.MAXIMUM_DURATION
|
|
|
|
)
|
|
|
|
off_max = self._device.sys_info.get(
|
|
|
|
"max_fade_off_time", self.MAXIMUM_DURATION
|
|
|
|
)
|
|
|
|
on_enabled = bool(self._device.sys_info["gradually_on_mode"])
|
|
|
|
off_enabled = bool(self._device.sys_info["gradually_off_mode"])
|
|
|
|
on_duration = self._device.sys_info["fade_on_time"]
|
|
|
|
off_duration = self._device.sys_info["fade_off_time"]
|
|
|
|
elif (on_state := self.data.get("on_state")) and (
|
|
|
|
off_state := self.data.get("off_state")
|
|
|
|
):
|
|
|
|
on_max = on_state.get("max_duration", self.MAXIMUM_DURATION)
|
|
|
|
off_max = off_state.get("max_duration", self.MAXIMUM_DURATION)
|
|
|
|
on_enabled = on_state["enable"]
|
|
|
|
off_enabled = off_state["enable"]
|
|
|
|
on_duration = on_state["duration"]
|
|
|
|
off_duration = off_state["duration"]
|
|
|
|
else:
|
2024-02-24 01:16:43 +00:00
|
|
|
raise KasaException(
|
|
|
|
f"Unsupported for {self.REQUIRED_COMPONENT} v{self.supported_version}"
|
2024-02-19 19:39:20 +00:00
|
|
|
)
|
|
|
|
|
2024-06-27 17:52:54 +00:00
|
|
|
self._enabled = on_enabled or off_enabled
|
|
|
|
self._on_state = {
|
|
|
|
"duration": on_duration,
|
|
|
|
"enable": on_enabled,
|
|
|
|
"max_duration": on_max,
|
|
|
|
}
|
|
|
|
self._off_state = {
|
|
|
|
"duration": off_duration,
|
|
|
|
"enable": off_enabled,
|
|
|
|
"max_duration": off_max,
|
|
|
|
}
|
|
|
|
|
|
|
|
async def set_enabled(self, enable: bool):
|
2024-02-19 19:39:20 +00:00
|
|
|
"""Enable gradual on/off."""
|
2024-06-27 17:52:54 +00:00
|
|
|
if not self._supports_on_and_off:
|
|
|
|
return await self.call("set_on_off_gradually_info", {"enable": enable})
|
|
|
|
else:
|
|
|
|
on = await self.call(
|
|
|
|
"set_on_off_gradually_info", {"on_state": {"enable": enable}}
|
|
|
|
)
|
|
|
|
off = await self.call(
|
|
|
|
"set_on_off_gradually_info", {"off_state": {"enable": enable}}
|
|
|
|
)
|
|
|
|
return {**on, **off}
|
2024-02-19 19:39:20 +00:00
|
|
|
|
|
|
|
@property
|
2024-06-27 17:52:54 +00:00
|
|
|
def enabled(self) -> bool:
|
2024-02-19 19:39:20 +00:00
|
|
|
"""Return True if gradual on/off is enabled."""
|
2024-06-27 17:52:54 +00:00
|
|
|
return self._enabled
|
2024-02-19 19:39:20 +00:00
|
|
|
|
2024-02-24 01:16:43 +00:00
|
|
|
@property
|
|
|
|
def turn_on_transition(self) -> int:
|
|
|
|
"""Return transition time for turning the light on.
|
|
|
|
|
|
|
|
Available only from v2.
|
|
|
|
"""
|
2024-06-27 17:52:54 +00:00
|
|
|
return self._on_state["duration"] if self._on_state["enable"] else 0
|
2024-02-24 01:16:43 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def _turn_on_transition_max(self) -> int:
|
|
|
|
"""Maximum turn on duration."""
|
|
|
|
# v3 added max_duration, we default to 60 when it's not available
|
2024-06-27 17:52:54 +00:00
|
|
|
return self._on_state["max_duration"]
|
2024-02-24 01:16:43 +00:00
|
|
|
|
|
|
|
async def set_turn_on_transition(self, seconds: int):
|
|
|
|
"""Set turn on transition in seconds.
|
|
|
|
|
|
|
|
Setting to 0 turns the feature off.
|
|
|
|
"""
|
|
|
|
if seconds > self._turn_on_transition_max:
|
|
|
|
raise ValueError(
|
|
|
|
f"Value {seconds} out of range, max {self._turn_on_transition_max}"
|
|
|
|
)
|
|
|
|
|
|
|
|
if seconds <= 0:
|
|
|
|
return await self.call(
|
|
|
|
"set_on_off_gradually_info",
|
2024-06-27 17:52:54 +00:00
|
|
|
{"on_state": {"enable": False}},
|
2024-02-24 01:16:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return await self.call(
|
|
|
|
"set_on_off_gradually_info",
|
2024-06-27 17:52:54 +00:00
|
|
|
{"on_state": {"enable": True, "duration": seconds}},
|
2024-02-24 01:16:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def turn_off_transition(self) -> int:
|
|
|
|
"""Return transition time for turning the light off.
|
|
|
|
|
|
|
|
Available only from v2.
|
|
|
|
"""
|
2024-06-27 17:52:54 +00:00
|
|
|
return self._off_state["duration"] if self._off_state["enable"] else 0
|
2024-02-24 01:16:43 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def _turn_off_transition_max(self) -> int:
|
|
|
|
"""Maximum turn on duration."""
|
|
|
|
# v3 added max_duration, we default to 60 when it's not available
|
2024-06-27 17:52:54 +00:00
|
|
|
return self._off_state["max_duration"]
|
2024-02-24 01:16:43 +00:00
|
|
|
|
|
|
|
async def set_turn_off_transition(self, seconds: int):
|
|
|
|
"""Set turn on transition in seconds.
|
|
|
|
|
|
|
|
Setting to 0 turns the feature off.
|
|
|
|
"""
|
|
|
|
if seconds > self._turn_off_transition_max:
|
|
|
|
raise ValueError(
|
|
|
|
f"Value {seconds} out of range, max {self._turn_off_transition_max}"
|
|
|
|
)
|
|
|
|
|
|
|
|
if seconds <= 0:
|
|
|
|
return await self.call(
|
|
|
|
"set_on_off_gradually_info",
|
2024-06-27 17:52:54 +00:00
|
|
|
{"off_state": {"enable": False}},
|
2024-02-24 01:16:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return await self.call(
|
|
|
|
"set_on_off_gradually_info",
|
2024-06-27 17:52:54 +00:00
|
|
|
{"off_state": {"enable": True, "duration": seconds}},
|
2024-02-24 01:16:43 +00:00
|
|
|
)
|
2024-04-24 18:17:49 +00:00
|
|
|
|
|
|
|
def query(self) -> dict:
|
|
|
|
"""Query to execute during the update cycle."""
|
|
|
|
# Some devices have the required info in the device info.
|
2024-06-27 17:52:54 +00:00
|
|
|
if self._state_in_sysinfo:
|
2024-04-24 18:17:49 +00:00
|
|
|
return {}
|
|
|
|
else:
|
|
|
|
return {self.QUERY_GETTER_NAME: None}
|
2024-06-11 14:46:36 +00:00
|
|
|
|
|
|
|
async def _check_supported(self):
|
2024-06-25 18:52:25 +00:00
|
|
|
"""Additional check to see if the module is supported by the device."""
|
2024-06-27 17:52:54 +00:00
|
|
|
# For devices that report child components on the parent that are not
|
|
|
|
# actually supported by the parent.
|
2024-06-11 14:46:36 +00:00
|
|
|
return "brightness" in self._device.sys_info
|