From 3ab41f3db79ddac6bc029c3fc010d116d03b13a0 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sun, 18 Feb 2024 22:18:33 +0100 Subject: [PATCH 1/5] Add fan control module --- kasa/device_type.py | 1 + kasa/smart/modules/__init__.py | 2 + kasa/smart/modules/fanmodule.py | 65 +++++++++++++++++++++++++++++++++ kasa/smart/smartchilddevice.py | 2 + 4 files changed, 70 insertions(+) create mode 100644 kasa/smart/modules/fanmodule.py diff --git a/kasa/device_type.py b/kasa/device_type.py index 80a816443..e56d2c202 100755 --- a/kasa/device_type.py +++ b/kasa/device_type.py @@ -17,6 +17,7 @@ class DeviceType(Enum): LightStrip = "lightstrip" Sensor = "sensor" Hub = "hub" + Fan = "fan" Unknown = "unknown" @staticmethod diff --git a/kasa/smart/modules/__init__.py b/kasa/smart/modules/__init__.py index 9d1af1c82..80db14b9e 100644 --- a/kasa/smart/modules/__init__.py +++ b/kasa/smart/modules/__init__.py @@ -8,6 +8,7 @@ from .colortemp import ColorTemperatureModule from .devicemodule import DeviceModule from .energymodule import EnergyModule +from .fanmodule import FanModule from .firmware import Firmware from .humidity import HumiditySensor from .ledmodule import LedModule @@ -29,6 +30,7 @@ "AutoOffModule", "LedModule", "Brightness", + "FanModule", "Firmware", "CloudModule", "LightTransitionModule", diff --git a/kasa/smart/modules/fanmodule.py b/kasa/smart/modules/fanmodule.py new file mode 100644 index 000000000..57c978395 --- /dev/null +++ b/kasa/smart/modules/fanmodule.py @@ -0,0 +1,65 @@ +"""Implementation of fan_control module.""" +from typing import TYPE_CHECKING, Dict + +from ...feature import Feature, FeatureType +from ..smartmodule import SmartModule + +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + + +class FanModule(SmartModule): + """Implementation of fan_control module.""" + + REQUIRED_COMPONENT = "fan_control" + + def __init__(self, device: "SmartDevice", module: str): + super().__init__(device, module) + + self._add_feature( + Feature( + device, + "Fan speed level", + container=self, + attribute_getter="fan_speed_level", + attribute_setter="set_fan_speed_level", + icon="mdi:fan", + type=FeatureType.Number, + minimum_value=1, + maximum_value=4, + ) + ) + self._add_feature( + Feature( + device, + "Fan sleep mode", + container=self, + attribute_getter="sleep_mode", + attribute_setter="set_sleep_mode", + icon="mdi:sleep", + ) + ) + + def query(self) -> Dict: + """Query to execute during the update cycle.""" + return {} + + @property + def fan_speed_level(self) -> int: + """Return fan speed level.""" + return self.data["fan_speed_level"] + + async def set_fan_speed_level(self, level: int): + """Set fan speed level.""" + if level < 1 or level > 4: + raise ValueError("Invalid level, should be in range 1-4.") + return await self.call("set_device_info", {"fan_speed_level": level}) + + @property + def sleep_mode(self) -> bool: + """Return sleep mode status.""" + return self.data["fan_sleep_mode_on"] + + async def set_sleep_mode(self, on: bool): + """Set sleep mode.""" + return await self.call("set_device_info", {"fan_sleep_mode_on": on}) diff --git a/kasa/smart/smartchilddevice.py b/kasa/smart/smartchilddevice.py index 1ea517aa6..1a4de8007 100644 --- a/kasa/smart/smartchilddevice.py +++ b/kasa/smart/smartchilddevice.py @@ -48,6 +48,8 @@ def device_type(self) -> DeviceType: child_device_map = { "plug.powerstrip.sub-plug": DeviceType.Plug, "subg.trigger.temp-hmdt-sensor": DeviceType.Sensor, + "kasa.switch.outlet.sub-fan": DeviceType.Fan, + "kasa.switch.outlet.sub-dimmer": DeviceType.Dimmer, } dev_type = child_device_map.get(self.sys_info["category"]) if dev_type is None: From 7ef7a9b1f237d75994dbf1f57d86511c9743fd69 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 5 Mar 2024 15:08:22 +0100 Subject: [PATCH 2/5] Use sys_info for accessing the main sysinfo data, needed for child devices --- kasa/smart/smartdevice.py | 7 +++---- kasa/smart/smartmodule.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 3cbd12f97..f69a4a186 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -313,12 +313,11 @@ def internal_state(self) -> Any: return self._last_update def _update_internal_state(self, info): - """Update internal state. + """Update the internal info state. - This is used by the parent to push updates to its children + This is used by the parent to push updates to its children. """ - # TODO: cleanup the _last_update, _info mess. - self._last_update = self._info = info + self._info = info async def _query_helper( self, method: str, params: Optional[Dict] = None, child_ids=None diff --git a/kasa/smart/smartmodule.py b/kasa/smart/smartmodule.py index 01a27360f..c57c76df6 100644 --- a/kasa/smart/smartmodule.py +++ b/kasa/smart/smartmodule.py @@ -61,7 +61,7 @@ def data(self): q = self.query() if not q: - return dev.internal_state["get_device_info"] + return dev.sys_info q_keys = list(q.keys()) query_key = q_keys[0] From 7512b286a4d4796f10224df7ec495b565b28de22 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 15 Mar 2024 18:00:48 +0100 Subject: [PATCH 3/5] Fix tests --- kasa/tests/test_childdevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kasa/tests/test_childdevice.py b/kasa/tests/test_childdevice.py index 97d3fd376..64ad70fa1 100644 --- a/kasa/tests/test_childdevice.py +++ b/kasa/tests/test_childdevice.py @@ -32,8 +32,8 @@ async def test_childdevice_update(dev, dummy_protocol, mocker): await dev.update() - assert dev._last_update != first._last_update - assert child_list[0] == first._last_update + assert dev._info != first._info + assert child_list[0] == first._info @strip_smart From 8982855731e3b74b44334b053934447384bf0712 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 15 Mar 2024 19:00:40 +0100 Subject: [PATCH 4/5] Add tests which do not run until fixtures are in-place --- kasa/tests/smart/modules/test_fan.py | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 kasa/tests/smart/modules/test_fan.py diff --git a/kasa/tests/smart/modules/test_fan.py b/kasa/tests/smart/modules/test_fan.py new file mode 100644 index 000000000..7c7ad9d86 --- /dev/null +++ b/kasa/tests/smart/modules/test_fan.py @@ -0,0 +1,43 @@ +from pytest_mock import MockerFixture + +from kasa import SmartDevice +from kasa.smart.modules import FanModule +from kasa.tests.device_fixtures import parametrize + +fan = parametrize( + "has fan", component_filter="fan_control", protocol_filter={"SMART.CHILD"} +) + + +@fan +async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture): + """Test fan speed feature.""" + fan: FanModule = dev.modules["FanModule"] + level_feature = fan._module_features["fan_speed_level"] + assert level_feature.minimum_value <= level_feature.value <= level_feature.maximum_value + + call = mocker.spy(fan, "call") + await fan.set_fan_speed_level(3) + call.assert_called_with("set_device_info", {"fan_sleep_level": 3}) + + await dev.update() + + assert fan.fan_speed_level == 3 + assert level_feature.value == 3 + + +@fan +async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture): + """Test sleep mode feature.""" + fan: FanModule = dev.modules["FanModule"] + sleep_feature = fan._module_features["fan_sleep_mode"] + assert isinstance(sleep_feature.value, bool) + + call = mocker.spy(fan, "call") + await fan.set_sleep_mode(True) + call.assert_called_with("set_device_info", {"fan_sleep_mode_on": True}) + + await dev.update() + + assert fan.sleep_mode is True + assert sleep_feature.value is True From cf0467a675fa047198a1c20bafb60c9ba9f7881c Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 15 Mar 2024 19:48:55 +0100 Subject: [PATCH 5/5] Set type of fan sleep mode to switch --- kasa/smart/modules/fanmodule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kasa/smart/modules/fanmodule.py b/kasa/smart/modules/fanmodule.py index 57c978395..4734aa91c 100644 --- a/kasa/smart/modules/fanmodule.py +++ b/kasa/smart/modules/fanmodule.py @@ -37,6 +37,7 @@ def __init__(self, device: "SmartDevice", module: str): attribute_getter="sleep_mode", attribute_setter="set_sleep_mode", icon="mdi:sleep", + type=FeatureType.Switch ) )