From 9eb09b76f70cbd96e932b12958a39d9f7c778e5b Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 23 Feb 2024 15:36:44 +0100 Subject: [PATCH 1/2] Support for on_off_gradually v2+ --- kasa/feature.py | 13 ++ kasa/smart/modules/devicemodule.py | 2 +- kasa/smart/modules/lighttransitionmodule.py | 151 ++++++++++++++++++-- kasa/smart/smartmodule.py | 5 + 4 files changed, 156 insertions(+), 15 deletions(-) diff --git a/kasa/feature.py b/kasa/feature.py index 420fd8485..c86b775b1 100644 --- a/kasa/feature.py +++ b/kasa/feature.py @@ -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,11 @@ async def set_value(self, value): """Set the value.""" if self.attribute_setter is None: raise ValueError("Tried to set read-only feature.") + if self.type == FeatureType.Number: + if value < self.minimum_value or value > self.maximum_value: + raise ValueError( + f"Value {value} out of range [{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) diff --git a/kasa/smart/modules/devicemodule.py b/kasa/smart/modules/devicemodule.py index 80e7287f0..e36c09fed 100644 --- a/kasa/smart/modules/devicemodule.py +++ b/kasa/smart/modules/devicemodule.py @@ -15,7 +15,7 @@ def query(self) -> Dict: "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 diff --git a/kasa/smart/modules/lighttransitionmodule.py b/kasa/smart/modules/lighttransitionmodule.py index ef8739bcf..23377edbc 100644 --- a/kasa/smart/modules/lighttransitionmodule.py +++ b/kasa/smart/modules/lighttransitionmodule.py @@ -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,151 @@ 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 _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 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} version {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} version {self.supported_version}" ) - ) - def set_enabled(self, enable: bool): + 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}}, + ) diff --git a/kasa/smart/smartmodule.py b/kasa/smart/smartmodule.py index b557f4934..e34f2260a 100644 --- a/kasa/smart/smartmodule.py +++ b/kasa/smart/smartmodule.py @@ -80,3 +80,8 @@ def data(self): 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] From e9b6189d88bc1e5e4caa3786947358749831fb9d Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 23 Feb 2024 16:07:04 +0100 Subject: [PATCH 2/2] Fix linting --- kasa/feature.py | 5 +++-- kasa/smart/modules/lighttransitionmodule.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/kasa/feature.py b/kasa/feature.py index c86b775b1..df28c952c 100644 --- a/kasa/feature.py +++ b/kasa/feature.py @@ -54,10 +54,11 @@ async def set_value(self, value): """Set the value.""" if self.attribute_setter is None: raise ValueError("Tried to set read-only feature.") - if self.type == FeatureType.Number: + 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 [{self.minimum_value}, {self.maximum_value}]" + 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 diff --git a/kasa/smart/modules/lighttransitionmodule.py b/kasa/smart/modules/lighttransitionmodule.py index 23377edbc..f98f21ca8 100644 --- a/kasa/smart/modules/lighttransitionmodule.py +++ b/kasa/smart/modules/lighttransitionmodule.py @@ -38,7 +38,8 @@ def _create_features(self): elif self.supported_version >= 2: # v2 adds separate on & off states # v3 adds max_duration - # TODO: note, hardcoding the maximums as the features get initialized before the first update.. + # TODO: note, hardcoding the maximums for now as the features get + # initialized before the first update. self._add_feature( Feature( self._device, @@ -69,7 +70,7 @@ 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} version {self.supported_version}" + f"Unsupported for {self.REQUIRED_COMPONENT} v{self.supported_version}" ) return self.data["on_state"] @@ -79,7 +80,7 @@ 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} version {self.supported_version}" + f"Unsupported for {self.REQUIRED_COMPONENT} v{self.supported_version}" ) return self.data["off_state"]