diff --git a/kasa/device_type.py b/kasa/device_type.py index b6214c17..34f0bd89 100755 --- a/kasa/device_type.py +++ b/kasa/device_type.py @@ -16,6 +16,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 1a45bf1f..e2da5b69 100644 --- a/kasa/smart/modules/__init__.py +++ b/kasa/smart/modules/__init__.py @@ -9,6 +9,7 @@ from .cloudmodule import CloudModule 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 @@ -30,6 +31,7 @@ __all__ = [ "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 00000000..4734aa91 --- /dev/null +++ b/kasa/smart/modules/fanmodule.py @@ -0,0 +1,66 @@ +"""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", + type=FeatureType.Switch + ) + ) + + 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 e8d8c208..6289dbc0 100644 --- a/kasa/smart/smartchilddevice.py +++ b/kasa/smart/smartchilddevice.py @@ -49,6 +49,8 @@ class SmartChildDevice(SmartDevice): 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: diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 909341a1..331cf66e 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -314,12 +314,11 @@ class SmartDevice(Device): 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 4756a424..20580975 100644 --- a/kasa/smart/smartmodule.py +++ b/kasa/smart/smartmodule.py @@ -62,7 +62,7 @@ class SmartModule(Module): 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] diff --git a/kasa/tests/smart/modules/test_fan.py b/kasa/tests/smart/modules/test_fan.py new file mode 100644 index 00000000..7c7ad9d8 --- /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 diff --git a/kasa/tests/test_childdevice.py b/kasa/tests/test_childdevice.py index 97d3fd37..64ad70fa 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