From 9975bbf26a10ef865f7cbf50f064944f3c5fe5cf Mon Sep 17 00:00:00 2001 From: "Teemu R." Date: Thu, 31 Oct 2024 11:41:11 +0100 Subject: [PATCH] Expose PIR enabled setting for iot dimmers (#1174) This adds PIR enabled feature to iot dimmers, making it possible to enable and disable the motion detection. --- kasa/iot/modules/motion.py | 44 +++++++++++++++++---- kasa/tests/iot/modules/__init__.py | 0 kasa/tests/iot/modules/test_motion.py | 57 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 kasa/tests/iot/modules/__init__.py create mode 100644 kasa/tests/iot/modules/test_motion.py diff --git a/kasa/iot/modules/motion.py b/kasa/iot/modules/motion.py index fe59748e..db272e2f 100644 --- a/kasa/iot/modules/motion.py +++ b/kasa/iot/modules/motion.py @@ -2,11 +2,15 @@ from __future__ import annotations +import logging from enum import Enum from ...exceptions import KasaException +from ...feature import Feature from ..iotmodule import IotModule +_LOGGER = logging.getLogger(__name__) + class Range(Enum): """Range for motion detection.""" @@ -17,27 +21,51 @@ class Range(Enum): Custom = 3 -# TODO: use the config reply in tests -# {"enable":0,"version":"1.0","trigger_index":2,"cold_time":60000, -# "min_adc":0,"max_adc":4095,"array":[80,50,20,0],"err_code":0}}} - - class Motion(IotModule): """Implements the motion detection (PIR) module.""" + def _initialize_features(self): + """Initialize features after the initial update.""" + # Only add features if the device supports the module + if "get_config" not in self.data: + return + + if "enable" not in self.config: + _LOGGER.warning("%r initialized, but no enable in response") + return + + self._add_feature( + Feature( + device=self._device, + container=self, + id="pir_enabled", + name="PIR enabled", + icon="mdi:motion-sensor", + attribute_getter="enabled", + attribute_setter="set_enabled", + type=Feature.Type.Switch, + category=Feature.Category.Config, + ) + ) + def query(self): """Request PIR configuration.""" return self.query_for_command("get_config") + @property + def config(self) -> dict: + """Return current configuration.""" + return self.data["get_config"] + @property def range(self) -> Range: """Return motion detection range.""" - return Range(self.data["trigger_index"]) + return Range(self.config["trigger_index"]) @property def enabled(self) -> bool: """Return True if module is enabled.""" - return bool(self.data["enable"]) + return bool(self.config["enable"]) async def set_enabled(self, state: bool): """Enable/disable PIR.""" @@ -63,7 +91,7 @@ class Motion(IotModule): @property def inactivity_timeout(self) -> int: """Return inactivity timeout in milliseconds.""" - return self.data["cold_time"] + return self.config["cold_time"] async def set_inactivity_timeout(self, timeout: int): """Set inactivity timeout in milliseconds. diff --git a/kasa/tests/iot/modules/__init__.py b/kasa/tests/iot/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kasa/tests/iot/modules/test_motion.py b/kasa/tests/iot/modules/test_motion.py new file mode 100644 index 00000000..93236164 --- /dev/null +++ b/kasa/tests/iot/modules/test_motion.py @@ -0,0 +1,57 @@ +from pytest_mock import MockerFixture + +from kasa import Module +from kasa.iot import IotDimmer +from kasa.iot.modules.motion import Motion, Range +from kasa.tests.device_fixtures import dimmer_iot + + +@dimmer_iot +def test_motion_getters(dev: IotDimmer): + assert Module.IotMotion in dev.modules + motion: Motion = dev.modules[Module.IotMotion] + + assert motion.enabled == motion.config["enable"] + assert motion.inactivity_timeout == motion.config["cold_time"] + assert motion.range.value == motion.config["trigger_index"] + + +@dimmer_iot +async def test_motion_setters(dev: IotDimmer, mocker: MockerFixture): + motion: Motion = dev.modules[Module.IotMotion] + query_helper = mocker.patch("kasa.iot.IotDimmer._query_helper") + + await motion.set_enabled(True) + query_helper.assert_called_with("smartlife.iot.PIR", "set_enable", {"enable": True}) + + await motion.set_inactivity_timeout(10) + query_helper.assert_called_with( + "smartlife.iot.PIR", "set_cold_time", {"cold_time": 10} + ) + + +@dimmer_iot +async def test_motion_range(dev: IotDimmer, mocker: MockerFixture): + motion: Motion = dev.modules[Module.IotMotion] + query_helper = mocker.patch("kasa.iot.IotDimmer._query_helper") + + await motion.set_range(custom_range=123) + query_helper.assert_called_with( + "smartlife.iot.PIR", + "set_trigger_sens", + {"index": Range.Custom.value, "value": 123}, + ) + + await motion.set_range(range=Range.Far) + query_helper.assert_called_with( + "smartlife.iot.PIR", "set_trigger_sens", {"index": Range.Far.value} + ) + + +@dimmer_iot +def test_motion_feature(dev: IotDimmer): + assert Module.IotMotion in dev.modules + motion: Motion = dev.modules[Module.IotMotion] + + pir_enabled = dev.features["pir_enabled"] + assert motion.enabled == pir_enabled.value