Support for on_off_gradually v2+ (#793)

Previously, only v1 of on_off_gradually is supported, and the newer versions are not backwards compatible.
This PR adds support for the newer versions of the component, and implements `number` type for `Feature` to expose the transition time selection.
This also adds a new `supported_version` property to the main module API.
This commit is contained in:
Teemu R 2024-02-24 02:16:43 +01:00 committed by GitHub
parent a73e2a9ede
commit cbf82c9498
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 159 additions and 16 deletions

View File

@ -14,6 +14,7 @@ class FeatureType(Enum):
BinarySensor = auto()
Switch = auto()
Button = auto()
Number = auto()
@dataclass
@ -35,6 +36,12 @@ class Feature:
#: Type of the feature
type: FeatureType = FeatureType.Sensor
# Number-specific attributes
#: Minimum value
minimum_value: int = 0
#: Maximum value
maximum_value: int = 2**16 # Arbitrary max
@property
def value(self):
"""Return the current value."""
@ -47,5 +54,12 @@ class Feature:
"""Set the value."""
if self.attribute_setter is None:
raise ValueError("Tried to set read-only feature.")
if self.type == FeatureType.Number: # noqa: SIM102
if value < self.minimum_value or value > self.maximum_value:
raise ValueError(
f"Value {value} out of range "
f"[{self.minimum_value}, {self.maximum_value}]"
)
container = self.container if self.container is not None else self.device
return await getattr(container, self.attribute_setter)(value)

View File

@ -15,7 +15,7 @@ class DeviceModule(SmartModule):
"get_device_info": None,
}
# Device usage is not available on older firmware versions
if self._device._components[self.REQUIRED_COMPONENT] >= 2:
if self.supported_version >= 2:
query["get_device_usage"] = None
return query

View File

@ -1,6 +1,7 @@
"""Module for smooth light transitions."""
from typing import TYPE_CHECKING
from ...exceptions import KasaException
from ...feature import Feature, FeatureType
from ..smartmodule import SmartModule
@ -13,29 +14,152 @@ class LightTransitionModule(SmartModule):
REQUIRED_COMPONENT = "on_off_gradually"
QUERY_GETTER_NAME = "get_on_off_gradually_info"
MAXIMUM_DURATION = 60
def __init__(self, device: "SmartDevice", module: str):
super().__init__(device, module)
self._add_feature(
Feature(
device=device,
container=self,
name="Smooth transitions",
icon="mdi:transition",
attribute_getter="enabled",
attribute_setter="set_enabled",
type=FeatureType.Switch,
)
)
self._create_features()
def set_enabled(self, enable: bool):
def _create_features(self):
"""Create features based on the available version."""
icon = "mdi:transition"
if self.supported_version == 1:
self._add_feature(
Feature(
device=self._device,
container=self,
name="Smooth transitions",
icon=icon,
attribute_getter="enabled_v1",
attribute_setter="set_enabled_v1",
type=FeatureType.Switch,
)
)
elif self.supported_version >= 2:
# v2 adds separate on & off states
# v3 adds max_duration
# TODO: note, hardcoding the maximums for now as the features get
# initialized before the first update.
self._add_feature(
Feature(
self._device,
"Smooth transition on",
container=self,
attribute_getter="turn_on_transition",
attribute_setter="set_turn_on_transition",
icon=icon,
type=FeatureType.Number,
maximum_value=self.MAXIMUM_DURATION,
)
) # self._turn_on_transition_max
self._add_feature(
Feature(
self._device,
"Smooth transition off",
container=self,
attribute_getter="turn_off_transition",
attribute_setter="set_turn_off_transition",
icon=icon,
type=FeatureType.Number,
maximum_value=self.MAXIMUM_DURATION,
)
) # self._turn_off_transition_max
@property
def _turn_on(self):
"""Internal getter for turn on settings."""
if "on_state" not in self.data:
raise KasaException(
f"Unsupported for {self.REQUIRED_COMPONENT} v{self.supported_version}"
)
return self.data["on_state"]
@property
def _turn_off(self):
"""Internal getter for turn off settings."""
if "off_state" not in self.data:
raise KasaException(
f"Unsupported for {self.REQUIRED_COMPONENT} v{self.supported_version}"
)
return self.data["off_state"]
def set_enabled_v1(self, enable: bool):
"""Enable gradual on/off."""
return self.call("set_on_off_gradually_info", {"enable": enable})
@property
def enabled(self) -> bool:
def enabled_v1(self) -> bool:
"""Return True if gradual on/off is enabled."""
return bool(self.data["enable"])
def __cli_output__(self):
return f"Gradual on/off enabled: {self.enabled}"
@property
def turn_on_transition(self) -> int:
"""Return transition time for turning the light on.
Available only from v2.
"""
return self._turn_on["duration"]
@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
return self._turn_on.get("max_duration", 60)
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",
{"on_state": {**self._turn_on, "enable": False}},
)
return await self.call(
"set_on_off_gradually_info",
{"on_state": {**self._turn_on, "duration": seconds}},
)
@property
def turn_off_transition(self) -> int:
"""Return transition time for turning the light off.
Available only from v2.
"""
return self._turn_off["duration"]
@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
return self._turn_off.get("max_duration", 60)
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",
{"off_state": {**self._turn_off, "enable": False}},
)
return await self.call(
"set_on_off_gradually_info",
{"off_state": {**self._turn_on, "duration": seconds}},
)

View File

@ -80,3 +80,8 @@ class SmartModule(Module):
return next(iter(filtered_data.values()))
return filtered_data
@property
def supported_version(self) -> int:
"""Return version supported by the device."""
return self._device._components[self.REQUIRED_COMPONENT]