From 3e038496cb2f04f8e5a1d1b5024b2598bd01f20f Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 16 Feb 2024 18:24:06 +0100 Subject: [PATCH 1/3] Auto auto-off module for smartdevice --- kasa/smart/modules/__init__.py | 3 ++- kasa/smart/modules/autooffmodule.py | 36 +++++++++++++++++++++++++++++ kasa/smart/smartdevice.py | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 kasa/smart/modules/autooffmodule.py diff --git a/kasa/smart/modules/__init__.py b/kasa/smart/modules/__init__.py index 564363222..932b9f0e0 100644 --- a/kasa/smart/modules/__init__.py +++ b/kasa/smart/modules/__init__.py @@ -2,6 +2,7 @@ from .childdevicemodule import ChildDeviceModule from .devicemodule import DeviceModule from .energymodule import EnergyModule +from .autooffmodule import AutoOffModule from .timemodule import TimeModule -__all__ = ["TimeModule", "EnergyModule", "DeviceModule", "ChildDeviceModule"] +__all__ = ["TimeModule", "EnergyModule", "DeviceModule", "ChildDeviceModule", "AutoOffModule"] \ No newline at end of file diff --git a/kasa/smart/modules/autooffmodule.py b/kasa/smart/modules/autooffmodule.py new file mode 100644 index 000000000..669b783eb --- /dev/null +++ b/kasa/smart/modules/autooffmodule.py @@ -0,0 +1,36 @@ +"""Implementation of auto off module.""" +from typing import Dict + +from ..smartmodule import SmartModule + + +class AutoOffModule(SmartModule): + """Implementation of auto off module.""" + + REQUIRED_COMPONENT = "auto_off" + QUERY_GETTER_NAME = "get_auto_off_config" + + def query(self) -> Dict: + """Query to execute during the update cycle.""" + return {self.QUERY_GETTER_NAME: {"start_index": 0}} + + @property + def enabled(self) -> bool: + """Return True if enabled.""" + return self.data["enable"] + + def set_enabled(self, enable: bool): + """Enable/disable auto off.""" + return self.call("set_auto_off_config", {"enable": enable}) + + @property + def delay(self) -> int: + """Return time until auto off.""" + return self.data["delay_min"] + + def set_delay(self, delay: int): + """Set time until auto off.""" + return self.call("set_auto_off_config", {"delay_min": delay}) + + def __cli_output__(self): + return f"Auto off enabled: {self.enabled} (delay: {self.delay}min)" diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index f5e41dc1b..df6c295c6 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -17,6 +17,7 @@ DeviceModule, EnergyModule, TimeModule, + AutoOffModule, ) from .smartmodule import SmartModule From 17092b0c8dc113859c8361071ff4cfbc562dfa4c Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 19 Feb 2024 00:38:54 +0100 Subject: [PATCH 2/3] Expose features --- kasa/feature.py | 3 +- kasa/smart/modules/__init__.py | 10 ++++- kasa/smart/modules/autooffmodule.py | 58 ++++++++++++++++++++++++++--- kasa/smart/smartdevice.py | 2 +- 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/kasa/feature.py b/kasa/feature.py index c0c14b06c..420fd8485 100644 --- a/kasa/feature.py +++ b/kasa/feature.py @@ -47,4 +47,5 @@ async def set_value(self, value): """Set the value.""" if self.attribute_setter is None: raise ValueError("Tried to set read-only feature.") - return await getattr(self.device, self.attribute_setter)(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/__init__.py b/kasa/smart/modules/__init__.py index 932b9f0e0..f4afcb91a 100644 --- a/kasa/smart/modules/__init__.py +++ b/kasa/smart/modules/__init__.py @@ -1,8 +1,14 @@ """Modules for SMART devices.""" +from .autooffmodule import AutoOffModule from .childdevicemodule import ChildDeviceModule from .devicemodule import DeviceModule from .energymodule import EnergyModule -from .autooffmodule import AutoOffModule from .timemodule import TimeModule -__all__ = ["TimeModule", "EnergyModule", "DeviceModule", "ChildDeviceModule", "AutoOffModule"] \ No newline at end of file +__all__ = [ + "TimeModule", + "EnergyModule", + "DeviceModule", + "ChildDeviceModule", + "AutoOffModule", +] diff --git a/kasa/smart/modules/autooffmodule.py b/kasa/smart/modules/autooffmodule.py index 669b783eb..b1993deba 100644 --- a/kasa/smart/modules/autooffmodule.py +++ b/kasa/smart/modules/autooffmodule.py @@ -1,8 +1,13 @@ """Implementation of auto off module.""" -from typing import Dict +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Dict, Optional +from ...feature import Feature from ..smartmodule import SmartModule +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + class AutoOffModule(SmartModule): """Implementation of auto off module.""" @@ -10,6 +15,32 @@ class AutoOffModule(SmartModule): REQUIRED_COMPONENT = "auto_off" QUERY_GETTER_NAME = "get_auto_off_config" + def __init__(self, device: "SmartDevice", module: str): + super().__init__(device, module) + self._add_feature( + Feature( + device, + "Auto off enabled", + container=self, + attribute_getter="enabled", + attribute_setter="set_enabled", + ) + ) + self._add_feature( + Feature( + device, + "Auto off minutes", + container=self, + attribute_getter="delay", + attribute_setter="set_delay", + ) + ) + self._add_feature( + Feature( + device, "Auto off at", container=self, attribute_getter="auto_off_at" + ) + ) + def query(self) -> Dict: """Query to execute during the update cycle.""" return {self.QUERY_GETTER_NAME: {"start_index": 0}} @@ -21,7 +52,10 @@ def enabled(self) -> bool: def set_enabled(self, enable: bool): """Enable/disable auto off.""" - return self.call("set_auto_off_config", {"enable": enable}) + return self.call( + "set_auto_off_config", + {"enable": enable, "delay_min": self.data["delay_min"]}, + ) @property def delay(self) -> int: @@ -30,7 +64,21 @@ def delay(self) -> int: def set_delay(self, delay: int): """Set time until auto off.""" - return self.call("set_auto_off_config", {"delay_min": delay}) + return self.call( + "set_auto_off_config", {"delay_min": delay, "enable": self.data["enable"]} + ) + + @property + def is_timer_active(self) -> bool: + """Return True is auto-off timer is active.""" + return self._device.sys_info["auto_off_status"] == "on" + + @property + def auto_off_at(self) -> Optional[datetime]: + """Return when the device will be turned off automatically.""" + if not self.is_timer_active: + return None + + sysinfo = self._device.sys_info - def __cli_output__(self): - return f"Auto off enabled: {self.enabled} (delay: {self.delay}min)" + return self._device.time + timedelta(seconds=sysinfo["auto_off_remain_time"]) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index df6c295c6..5b5eb3028 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -13,11 +13,11 @@ from ..feature import Feature, FeatureType from ..smartprotocol import SmartProtocol from .modules import ( # noqa: F401 + AutoOffModule, ChildDeviceModule, DeviceModule, EnergyModule, TimeModule, - AutoOffModule, ) from .smartmodule import SmartModule From b309e0f966cd8893b414a8354e70e54a172b1366 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 19 Feb 2024 18:57:54 +0100 Subject: [PATCH 3/3] Fix tests --- kasa/tests/fakeprotocol_smart.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kasa/tests/fakeprotocol_smart.py b/kasa/tests/fakeprotocol_smart.py index bbadec0af..b5316a612 100644 --- a/kasa/tests/fakeprotocol_smart.py +++ b/kasa/tests/fakeprotocol_smart.py @@ -46,6 +46,7 @@ def credentials_hash(self): FIXTURE_MISSING_MAP = { "get_wireless_scan_info": ("wireless", {"ap_list": [], "wep_supported": False}), + "get_auto_off_config": ("auto_off", {'delay_min': 10, 'enable': False}), } async def send(self, request: str):