Skip to content

Support for on_off_gradually v2+ #793

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]