From 8c7b1b4a684c34a47e079500d30d27d8d287579a Mon Sep 17 00:00:00 2001
From: Teemu R <tpr@iki.fi>
Date: Sat, 29 Jan 2022 17:53:18 +0100
Subject: [PATCH] Implement motion & ambient light sensor modules for dimmers
 (#278)

---
 kasa/modules/__init__.py     |  2 ++
 kasa/modules/ambientlight.py | 47 +++++++++++++++++++++++++++
 kasa/modules/motion.py       | 62 ++++++++++++++++++++++++++++++++++++
 kasa/smartdimmer.py          |  5 +++
 4 files changed, 116 insertions(+)
 create mode 100644 kasa/modules/ambientlight.py
 create mode 100644 kasa/modules/motion.py

diff --git a/kasa/modules/__init__.py b/kasa/modules/__init__.py
index dd9d1072..e5cb83d6 100644
--- a/kasa/modules/__init__.py
+++ b/kasa/modules/__init__.py
@@ -1,9 +1,11 @@
 # flake8: noqa
+from .ambientlight import AmbientLight
 from .antitheft import Antitheft
 from .cloud import Cloud
 from .countdown import Countdown
 from .emeter import Emeter
 from .module import Module
+from .motion import Motion
 from .rulemodule import Rule, RuleModule
 from .schedule import Schedule
 from .time import Time
diff --git a/kasa/modules/ambientlight.py b/kasa/modules/ambientlight.py
new file mode 100644
index 00000000..963c73a3
--- /dev/null
+++ b/kasa/modules/ambientlight.py
@@ -0,0 +1,47 @@
+"""Implementation of the ambient light (LAS) module found in some dimmers."""
+from .module import Module
+
+# TODO create tests and use the config reply there
+# [{"hw_id":0,"enable":0,"dark_index":1,"min_adc":0,"max_adc":2450,
+# "level_array":[{"name":"cloudy","adc":490,"value":20},
+# {"name":"overcast","adc":294,"value":12},
+# {"name":"dawn","adc":222,"value":9},
+# {"name":"twilight","adc":222,"value":9},
+# {"name":"total darkness","adc":111,"value":4},
+# {"name":"custom","adc":2400,"value":97}]}]
+
+
+class AmbientLight(Module):
+    """Implements ambient light controls for the motion sensor."""
+
+    def query(self):
+        """Request configuration."""
+        return self.query_for_command("get_config")
+
+    @property
+    def presets(self) -> dict:
+        """Return device-defined presets for brightness setting."""
+        return self.data["level_array"]
+
+    @property
+    def enabled(self) -> bool:
+        """Return True if the module is enabled."""
+        return bool(self.data["enable"])
+
+    async def set_enabled(self, state: bool):
+        """Enable/disable LAS."""
+        return await self.call("set_enable", {"enable": int(state)})
+
+    async def current_brightness(self) -> int:
+        """Return current brightness.
+
+        Return value units.
+        """
+        return await self.call("get_current_brt")
+
+    async def set_brightness_limit(self, value: int):
+        """Set the limit when the motion sensor is inactive.
+
+        See `presets` for preset values. Custom values are also likely allowed.
+        """
+        return await self.call("set_brt_level", {"index": 0, "value": value})
diff --git a/kasa/modules/motion.py b/kasa/modules/motion.py
new file mode 100644
index 00000000..d839ca98
--- /dev/null
+++ b/kasa/modules/motion.py
@@ -0,0 +1,62 @@
+"""Implementation of the motion detection (PIR) module found in some dimmers."""
+from enum import Enum
+from typing import Optional
+
+from kasa.smartdevice import SmartDeviceException
+
+from .module import Module
+
+
+class Range(Enum):
+    """Range for motion detection."""
+
+    Far = 0
+    Mid = 1
+    Near = 2
+    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(Module):
+    """Implements the motion detection (PIR) module."""
+
+    def query(self):
+        """Request PIR configuration."""
+        return self.query_for_command("get_config")
+
+    @property
+    def range(self) -> Range:
+        """Return motion detection range."""
+        return Range(self.data["trigger_index"])
+
+    @property
+    def enabled(self) -> bool:
+        """Return True if module is enabled."""
+        return bool(self.data["enable"])
+
+    async def set_enabled(self, state: bool):
+        """Enable/disable PIR."""
+        return await self.call("set_enable", {"enable": int(state)})
+
+    async def set_range(
+        self, *, range: Optional[Range] = None, custom_range: Optional[int] = None
+    ):
+        """Set the range for the sensor.
+
+        :param range: for using standard ranges
+        :param custom_range: range in decimeters, overrides the range parameter
+        """
+        if custom_range is not None:
+            payload = {"index": Range.Custom.value, "value": custom_range}
+        elif range is not None:
+            payload = {"index": range.value}
+        else:
+            raise SmartDeviceException(
+                "Either range or custom_range need to be defined"
+            )
+
+        return await self.call("set_trigger_sens", payload)
diff --git a/kasa/smartdimmer.py b/kasa/smartdimmer.py
index 8e5cb152..5c06b8b9 100644
--- a/kasa/smartdimmer.py
+++ b/kasa/smartdimmer.py
@@ -1,6 +1,7 @@
 """Module for dimmers (currently only HS220)."""
 from typing import Any, Dict
 
+from kasa.modules import AmbientLight, Motion
 from kasa.smartdevice import DeviceType, SmartDeviceException, requires_update
 from kasa.smartplug import SmartPlug
 
@@ -40,6 +41,10 @@ class SmartDimmer(SmartPlug):
     def __init__(self, host: str) -> None:
         super().__init__(host)
         self._device_type = DeviceType.Dimmer
+        # TODO: need to be verified if it's okay to call these on HS220 w/o these
+        # TODO: need to be figured out what's the best approach to detect support for these
+        self.add_module("motion", Motion(self, "smartlife.iot.PIR"))
+        self.add_module("ambient", AmbientLight(self, "smartlife.iot.LAS"))
 
     @property  # type: ignore
     @requires_update