From cb972c3a30b65720563961427cf51ea1b53dfc6a Mon Sep 17 00:00:00 2001
From: Steven B <51370195+sdb9696@users.noreply.github.com>
Date: Tue, 7 Jan 2025 12:53:10 +0000
Subject: [PATCH] Add double click module for smart buttons

---
 kasa/smart/modules/__init__.py          |  2 ++
 kasa/smart/modules/doubleclick.py       | 42 ++++++++++++++++++++++++
 kasa/smart/smartdevice.py               |  4 +--
 kasa/smart/smartmodule.py               |  7 ++--
 tests/fakeprotocol_smart.py             |  6 ++++
 tests/smart/modules/test_doubleclick.py | 43 +++++++++++++++++++++++++
 6 files changed, 100 insertions(+), 4 deletions(-)
 create mode 100644 kasa/smart/modules/doubleclick.py
 create mode 100644 tests/smart/modules/test_doubleclick.py

diff --git a/kasa/smart/modules/__init__.py b/kasa/smart/modules/__init__.py
index ae9fb68f..1d397702 100644
--- a/kasa/smart/modules/__init__.py
+++ b/kasa/smart/modules/__init__.py
@@ -12,6 +12,7 @@ from .color import Color
 from .colortemperature import ColorTemperature
 from .contactsensor import ContactSensor
 from .devicemodule import DeviceModule
+from .doubleclick import DoubleClick
 from .energy import Energy
 from .fan import Fan
 from .firmware import Firmware
@@ -42,6 +43,7 @@ __all__ = [
     "DeviceModule",
     "ChildDevice",
     "BatterySensor",
+    "DoubleClick",
     "HumiditySensor",
     "TemperatureSensor",
     "TemperatureControl",
diff --git a/kasa/smart/modules/doubleclick.py b/kasa/smart/modules/doubleclick.py
new file mode 100644
index 00000000..b2267954
--- /dev/null
+++ b/kasa/smart/modules/doubleclick.py
@@ -0,0 +1,42 @@
+"""Module for double click enable."""
+
+from __future__ import annotations
+
+from ...feature import Feature
+from ..smartmodule import SmartModule, allow_update_after
+
+
+class DoubleClick(SmartModule):
+    """Implementation of double click module."""
+
+    REQUIRED_COMPONENT = "double_click"
+    QUERY_GETTER_NAME = "get_double_click_info"
+
+    def _initialize_features(self) -> None:
+        """Initialize features after the initial update."""
+        self._add_feature(
+            Feature(
+                self._device,
+                id="double_click",
+                name="Double click",
+                container=self,
+                attribute_getter="enabled",
+                attribute_setter="set_enabled",
+                type=Feature.Type.Switch,
+                category=Feature.Category.Config,
+            )
+        )
+
+    def query(self) -> dict:
+        """Query to execute during the update cycle."""
+        return {self.QUERY_GETTER_NAME: {}}
+
+    @property
+    def enabled(self) -> bool:
+        """Return current double click enabled status."""
+        return self.data["enable"]
+
+    @allow_update_after
+    async def set_enabled(self, enable: bool) -> dict:
+        """Set double click enable."""
+        return await self.call("set_double_click_info", {"enable": enable})
diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py
index 5fd22115..1c4925be 100644
--- a/kasa/smart/smartdevice.py
+++ b/kasa/smart/smartdevice.py
@@ -183,7 +183,7 @@ class SmartDevice(Device):
         """Update the internal device info."""
         self._info = self._try_get_response(info_resp, "get_device_info")
 
-    async def update(self, update_children: bool = False) -> None:
+    async def update(self, update_children: bool = True) -> None:
         """Update the device."""
         if self.credentials is None and self.credentials_hash is None:
             raise AuthenticationError("Tapo plug requires authentication.")
@@ -207,7 +207,7 @@ class SmartDevice(Device):
         # devices will always update children to prevent errors on module access.
         # This needs to go after updating the internal state of the children so that
         # child modules have access to their sysinfo.
-        if update_children or self.device_type != DeviceType.Hub:
+        if first_update or update_children or self.device_type != DeviceType.Hub:
             for child in self._children.values():
                 if TYPE_CHECKING:
                     assert isinstance(child, SmartChildDevice)
diff --git a/kasa/smart/smartmodule.py b/kasa/smart/smartmodule.py
index a5666f63..fd937ccb 100644
--- a/kasa/smart/smartmodule.py
+++ b/kasa/smart/smartmodule.py
@@ -4,12 +4,13 @@ from __future__ import annotations
 
 import logging
 from collections.abc import Awaitable, Callable, Coroutine
-from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar
+from typing import TYPE_CHECKING, Any, Concatenate, Final, ParamSpec, TypeVar
 
 from ..exceptions import DeviceError, KasaException, SmartErrorCode
-from ..module import Module
+from ..module import Module, ModuleName
 
 if TYPE_CHECKING:
+    from . import modules
     from .smartdevice import SmartDevice
 
 _LOGGER = logging.getLogger(__name__)
@@ -51,6 +52,8 @@ def raise_if_update_error(func: Callable[[_T], _R]) -> Callable[[_T], _R]:
 class SmartModule(Module):
     """Base class for SMART modules."""
 
+    SmartDoubleClick: Final[ModuleName[modules.DoubleClick]] = ModuleName("DoubleClick")
+
     NAME: str
     #: Module is initialized, if the given component is available
     REQUIRED_COMPONENT: str | None = None
diff --git a/tests/fakeprotocol_smart.py b/tests/fakeprotocol_smart.py
index c0222b99..aa17f1b2 100644
--- a/tests/fakeprotocol_smart.py
+++ b/tests/fakeprotocol_smart.py
@@ -167,6 +167,12 @@ class FakeSmartTransport(BaseTransport):
                 "setup_payload": "00:0000000-0000.00.000",
             },
         ),
+        "get_double_click_info": (
+            "double_click",
+            {
+                "enable": False,
+            },
+        ),
     }
 
     async def send(self, request: str):
diff --git a/tests/smart/modules/test_doubleclick.py b/tests/smart/modules/test_doubleclick.py
new file mode 100644
index 00000000..f0cd1c22
--- /dev/null
+++ b/tests/smart/modules/test_doubleclick.py
@@ -0,0 +1,43 @@
+"""Tests for smart double click module."""
+
+from __future__ import annotations
+
+from kasa import Device
+from kasa.smartcam.smartcammodule import SmartModule
+
+from ...device_fixtures import parametrize
+
+doubleclick = parametrize(
+    "has double click", component_filter="double_click", protocol_filter={"SMART.CHILD"}
+)
+
+
+@doubleclick
+async def test_doubleclick(dev: Device):
+    """Test device double click."""
+    doubleclick = dev.modules.get(SmartModule.SmartDoubleClick)
+    assert doubleclick
+
+    dc_feat = dev.features.get("double_click")
+    assert dc_feat
+
+    original_enabled = doubleclick.enabled
+
+    try:
+        await doubleclick.set_enabled(not original_enabled)
+        await dev.update()
+        assert doubleclick.enabled is not original_enabled
+        assert dc_feat.value is not original_enabled
+
+        await doubleclick.set_enabled(original_enabled)
+        await dev.update()
+        assert doubleclick.enabled is original_enabled
+        assert dc_feat.value is original_enabled
+
+        await dc_feat.set_value(not original_enabled)
+        await dev.update()
+        assert doubleclick.enabled is not original_enabled
+        assert dc_feat.value is not original_enabled
+
+    finally:
+        await doubleclick.set_enabled(original_enabled)