Skip to content

Commit

Permalink
Support for on_off_gradually v2+ (#793)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rytilahti committed Feb 24, 2024
1 parent a73e2a9 commit cbf82c9
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 15 deletions.
14 changes: 14 additions & 0 deletions kasa/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class FeatureType(Enum):
BinarySensor = auto()
Switch = auto()
Button = auto()
Number = auto()


@dataclass
Expand All @@ -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."""
Expand All @@ -47,5 +54,12 @@ 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: # 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)
2 changes: 1 addition & 1 deletion kasa/smart/modules/devicemodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
152 changes: 138 additions & 14 deletions kasa/smart/modules/lighttransitionmodule.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 _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}"
)
)

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}},
)
5 changes: 5 additions & 0 deletions kasa/smart/smartmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit cbf82c9

Please sign in to comment.