Repository: zigpy/zha-device-handlers
Pull request #2289 (2023-03-21 21:02:41)

Add Tuya LH-961ZB motion sensor support
This quirk fixes the problem there the iaszone state is never reset.

codecov-commenter: ## [Codecov](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2289?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
Patch coverage: **`72.72`**% and project coverage change: **`-0.05`** :warning:
> Comparison is base [(`fa60b9d`)](https://codecov.io/gh/zigpy/zha-device-handlers/commit/fa60b9d23a3e94b5a164e6aad1f24f4134544daa?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 83.92% compared to head [(`d440a84`)](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2289?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 83.88%.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy)

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              dev    #2289      +/-   ##
==========================================
- Coverage   83.92%   83.88%   -0.05%     
==========================================
  Files         260      261       +1     
  Lines        8219     8252      +33     
==========================================
+ Hits         6898     6922      +24     
- Misses       1321     1330       +9     
```


| [Impacted Files](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2289?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) | Coverage Δ | |
|---|---|---|
| [zhaquirks/tuya/sm0202\_motion.py](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2289?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvc20wMjAyX21vdGlvbi5weQ==) | `72.72% <72.72%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2289?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).


Patch:
diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py
index 93dccc4..45dca42 100644
--- a/zhaquirks/ikea/__init__.py
+++ b/zhaquirks/ikea/__init__.py
@@ -4,7 +4,7 @@ import logging
 from zigpy.quirks import CustomCluster
 import zigpy.types as t
 from zigpy.zcl import foundation
-from zigpy.zcl.clusters.general import Scenes
+from zigpy.zcl.clusters.general import PowerConfiguration, Scenes
 from zigpy.zcl.clusters.lightlink import LightLink
 
 from zhaquirks import DoublingPowerConfigurationCluster
@@ -15,6 +15,14 @@ IKEA = "IKEA of Sweden"
 IKEA_CLUSTER_ID = 0xFC7C  # decimal = 64636
 WWAH_CLUSTER_ID = 0xFC57  # decimal = 64599 ('Works with all Hubs' cluster)
 
+# PowerConfiguration cluster attributes
+BATTERY_VOLTAGE = PowerConfiguration.attributes_by_name["battery_voltage"].id
+BATTERY_SIZE = PowerConfiguration.attributes_by_name["battery_size"].id
+BATTERY_QUANTITY = PowerConfiguration.attributes_by_name["battery_quantity"].id
+BATTERY_RATED_VOLTAGE = PowerConfiguration.attributes_by_name[
+    "battery_rated_voltage"
+].id
+
 
 class LightLinkCluster(CustomCluster, LightLink):
     """Ikea LightLink cluster."""
@@ -73,59 +81,68 @@ class ScenesCluster(CustomCluster, Scenes):
     )
 
 
-class PowerConfiguration2AAACluster(DoublingPowerConfigurationCluster):
-    """Updating Power attributes 2 AAA."""
-
-    BATTERY_SIZES = 0x0031
-    BATTERY_QUANTITY = 0x0033
-    BATTERY_RATED_VOLTAGE = 0x0034
+# ZCL compliant IKEA power configuration clusters:
+class PowerConfig2AAACluster(CustomCluster, PowerConfiguration):
+    """Updating power attributes: 2 AAA."""
 
     _CONSTANT_ATTRIBUTES = {
-        BATTERY_SIZES: 4,
+        BATTERY_SIZE: 4,
         BATTERY_QUANTITY: 2,
         BATTERY_RATED_VOLTAGE: 15,
     }
 
 
-class PowerConfiguration2CRCluster(DoublingPowerConfigurationCluster):
-    """Updating Power attributes 2 CR2032."""
-
-    BATTERY_SIZES = 0x0031
-    BATTERY_QUANTITY = 0x0033
-    BATTERY_RATED_VOLTAGE = 0x0034
+class PowerConfig2CRCluster(CustomCluster, PowerConfiguration):
+    """Updating power attributes: 2 CR2032."""
 
     _CONSTANT_ATTRIBUTES = {
-        BATTERY_SIZES: 10,
+        BATTERY_SIZE: 10,
         BATTERY_QUANTITY: 2,
         BATTERY_RATED_VOLTAGE: 30,
     }
 
 
-class PowerConfiguration1CRCluster(DoublingPowerConfigurationCluster):
-    """Updating Power attributes 1 CR2032."""
-
-    BATTERY_SIZES = 0x0031
-    BATTERY_QUANTITY = 0x0033
-    BATTERY_RATED_VOLTAGE = 0x0034
+class PowerConfig1CRCluster(CustomCluster, PowerConfiguration):
+    """Updating power attributes: 1 CR2032."""
 
     _CONSTANT_ATTRIBUTES = {
-        BATTERY_SIZES: 10,
+        BATTERY_SIZE: 10,
         BATTERY_QUANTITY: 1,
         BATTERY_RATED_VOLTAGE: 30,
     }
 
 
-class PowerConfiguration1CRXCluster(DoublingPowerConfigurationCluster):
-    """Updating Power attributes 1 CR2032 and Zero voltage."""
-
-    BATTERY_VOLTAGE = 0x0020
-    BATTERY_SIZES = 0x0031
-    BATTERY_QUANTITY = 0x0033
-    BATTERY_RATED_VOLTAGE = 0x0034
+class PowerConfig1CRXCluster(CustomCluster, PowerConfiguration):
+    """Updating power attributes: 1 CR2032 and zero voltage."""
 
     _CONSTANT_ATTRIBUTES = {
         BATTERY_VOLTAGE: 0,
-        BATTERY_SIZES: 10,
+        BATTERY_SIZE: 10,
         BATTERY_QUANTITY: 1,
         BATTERY_RATED_VOLTAGE: 30,
     }
+
+
+# doubling IKEA power configuration clusters:
+class DoublingPowerConfig2AAACluster(
+    DoublingPowerConfigurationCluster, PowerConfig2AAACluster
+):
+    """Doubling power configuration cluster. Updating power attributes: 2 AAA."""
+
+
+class DoublingPowerConfig2CRCluster(
+    DoublingPowerConfigurationCluster, PowerConfig2CRCluster
+):
+    """Doubling power configuration cluster. Updating power attributes: 2 CR2032."""
+
+
+class DoublingPowerConfig1CRCluster(
+    DoublingPowerConfigurationCluster, PowerConfig1CRCluster
+):
+    """Doubling power configuration cluster. Updating power attributes: 1 CR2032."""
+
+
+class DoublingPowerConfig1CRXCluster(
+    DoublingPowerConfigurationCluster, PowerConfig1CRXCluster
+):
+    """Doubling power configuration cluster. Updating power attributes: 1 CR2032 and zero voltage."""
diff --git a/zhaquirks/ikea/dimmer.py b/zhaquirks/ikea/dimmer.py
index 29ad66f..fa6f79f 100644
--- a/zhaquirks/ikea/dimmer.py
+++ b/zhaquirks/ikea/dimmer.py
@@ -30,7 +30,7 @@ from zhaquirks.const import (
     RIGHT,
     ROTATED,
 )
-from zhaquirks.ikea import IKEA, PowerConfiguration1CRXCluster
+from zhaquirks.ikea import IKEA, DoublingPowerConfig1CRXCluster
 
 
 class IkeaDimmer(CustomDevice):
@@ -72,7 +72,7 @@ class IkeaDimmer(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRXCluster,
+                    DoublingPowerConfig1CRXCluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLink.cluster_id,
diff --git a/zhaquirks/ikea/fivebtnremote.py b/zhaquirks/ikea/fivebtnremote.py
index 9532acc..befab21 100644
--- a/zhaquirks/ikea/fivebtnremote.py
+++ b/zhaquirks/ikea/fivebtnremote.py
@@ -51,8 +51,9 @@ from zhaquirks.ikea import (
     IKEA,
     IKEA_CLUSTER_ID,
     WWAH_CLUSTER_ID,
+    DoublingPowerConfig1CRCluster,
     LightLinkCluster,
-    PowerConfiguration1CRCluster,
+    PowerConfig1CRCluster,
     ScenesCluster,
 )
 
@@ -244,7 +245,7 @@ class IkeaTradfriRemote2(IkeaTradfriRemote1):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLinkCluster,
@@ -305,7 +306,7 @@ class IkeaTradfriRemote3(IkeaTradfriRemote1):
                 DEVICE_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     LightLinkCluster,
@@ -365,7 +366,7 @@ class IkeaTradfriRemote4(IkeaTradfriRemote1):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLinkCluster,
@@ -427,7 +428,7 @@ class IkeaTradfriRemote5(IkeaTradfriRemote1):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration.cluster_id,
+                    PowerConfig1CRCluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLinkCluster,
diff --git a/zhaquirks/ikea/fourbtnremote.py b/zhaquirks/ikea/fourbtnremote.py
index 5ea9d63..ee37f4b 100644
--- a/zhaquirks/ikea/fourbtnremote.py
+++ b/zhaquirks/ikea/fourbtnremote.py
@@ -45,7 +45,7 @@ from zhaquirks.ikea import (
     IKEA,
     IKEA_CLUSTER_ID,
     WWAH_CLUSTER_ID,
-    PowerConfiguration2AAACluster,
+    DoublingPowerConfig2AAACluster,
     ScenesCluster,
 )
 
@@ -89,7 +89,7 @@ class IkeaTradfriRemoteV1(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration2AAACluster,
+                    DoublingPowerConfig2AAACluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLink.cluster_id,
diff --git a/zhaquirks/ikea/motion.py b/zhaquirks/ikea/motion.py
index f53d7ec..2a03460 100644
--- a/zhaquirks/ikea/motion.py
+++ b/zhaquirks/ikea/motion.py
@@ -21,7 +21,7 @@ from zhaquirks.const import (
     OUTPUT_CLUSTERS,
     PROFILE_ID,
 )
-from zhaquirks.ikea import IKEA, LightLinkCluster, PowerConfiguration2CRCluster
+from zhaquirks.ikea import IKEA, DoublingPowerConfig2CRCluster, LightLinkCluster
 
 
 class IkeaTradfriMotion(CustomDevice):
@@ -63,7 +63,7 @@ class IkeaTradfriMotion(CustomDevice):
                 DEVICE_TYPE: zll.DeviceType.ON_OFF_SENSOR,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration2CRCluster,
+                    DoublingPowerConfig2CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     Diagnostic.cluster_id,
diff --git a/zhaquirks/ikea/motionzha.py b/zhaquirks/ikea/motionzha.py
index f07baad..b1bf1d1 100644
--- a/zhaquirks/ikea/motionzha.py
+++ b/zhaquirks/ikea/motionzha.py
@@ -27,8 +27,8 @@ from zhaquirks.ikea import (
     IKEA,
     IKEA_CLUSTER_ID,
     WWAH_CLUSTER_ID,
+    DoublingPowerConfig2CRCluster,
     LightLinkCluster,
-    PowerConfiguration2CRCluster,
 )
 
 
@@ -71,7 +71,7 @@ class IkeaTradfriMotionE1745_Var01(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_SENSOR,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration2CRCluster,
+                    DoublingPowerConfig2CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     Diagnostic.cluster_id,
@@ -130,7 +130,7 @@ class IkeaTradfriMotionE1745_Var02(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_SENSOR,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration2CRCluster,
+                    DoublingPowerConfig2CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     PollControl.cluster_id,
@@ -190,7 +190,7 @@ class IkeaTradfriMotionE1525_Var01(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_SENSOR,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration2CRCluster,
+                    DoublingPowerConfig2CRCluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLinkCluster,
diff --git a/zhaquirks/ikea/opencloseremote.py b/zhaquirks/ikea/opencloseremote.py
index aefdd69..ba2da52 100644
--- a/zhaquirks/ikea/opencloseremote.py
+++ b/zhaquirks/ikea/opencloseremote.py
@@ -35,7 +35,7 @@ from zhaquirks.const import (
     SHORT_PRESS,
     ZHA_SEND_EVENT,
 )
-from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, PowerConfiguration1CRCluster
+from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, DoublingPowerConfig1CRCluster
 
 COMMAND_CLOSE = "down_close"
 COMMAND_STOP_OPENING = "stop_opening"
@@ -120,7 +120,7 @@ class IkeaTradfriOpenCloseRemote(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     PollControl.cluster_id,
diff --git a/zhaquirks/ikea/shortcutbtn.py b/zhaquirks/ikea/shortcutbtn.py
index 2ca9370..f719735 100644
--- a/zhaquirks/ikea/shortcutbtn.py
+++ b/zhaquirks/ikea/shortcutbtn.py
@@ -40,8 +40,8 @@ from zhaquirks.const import (
 from zhaquirks.ikea import (
     IKEA,
     IKEA_CLUSTER_ID,
+    DoublingPowerConfig1CRCluster,
     LightLinkCluster,
-    PowerConfiguration1CRCluster,
 )
 
 
@@ -86,7 +86,7 @@ class IkeaTradfriShortcutBtn(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     PollControl.cluster_id,
@@ -163,7 +163,7 @@ class IkeaTradfriShortcutBtn2(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     PollControl.cluster_id,
diff --git a/zhaquirks/ikea/symfonisk.py b/zhaquirks/ikea/symfonisk.py
index cb82f7e..1469af1 100644
--- a/zhaquirks/ikea/symfonisk.py
+++ b/zhaquirks/ikea/symfonisk.py
@@ -38,7 +38,7 @@ from zhaquirks.const import (
     TRIPLE_PRESS,
     TURN_ON,
 )
-from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, PowerConfiguration1CRCluster
+from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, DoublingPowerConfig1CRCluster
 
 
 class IkeaSYMFONISK1(CustomDevice):
@@ -80,7 +80,7 @@ class IkeaSYMFONISK1(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLink.cluster_id,
@@ -176,7 +176,7 @@ class IkeaSYMFONISK2(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     PollControl.cluster_id,
                     LightLink.cluster_id,
diff --git a/zhaquirks/ikea/twobtnremote.py b/zhaquirks/ikea/twobtnremote.py
index c655cec..1d89260 100644
--- a/zhaquirks/ikea/twobtnremote.py
+++ b/zhaquirks/ikea/twobtnremote.py
@@ -43,8 +43,8 @@ from zhaquirks.const import (
 from zhaquirks.ikea import (
     IKEA,
     IKEA_CLUSTER_ID,
+    DoublingPowerConfig1CRCluster,
     LightLinkCluster,
-    PowerConfiguration1CRCluster,
 )
 
 
@@ -90,7 +90,7 @@ class IkeaTradfriRemote2Btn(CustomDevice):
                 DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    PowerConfiguration1CRCluster,
+                    DoublingPowerConfig1CRCluster,
                     Identify.cluster_id,
                     Alarms.cluster_id,
                     PollControl.cluster_id,
diff --git a/zhaquirks/tuya/sm0202_motion.py b/zhaquirks/tuya/sm0202_motion.py
new file mode 100644
index 0000000..044a329
--- /dev/null
+++ b/zhaquirks/tuya/sm0202_motion.py
@@ -0,0 +1,64 @@
+"""Device handler for Tuya LH-961ZB motion sensor."""
+from zigpy.profiles import zha
+from zigpy.quirks import CustomDevice
+from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration
+from zigpy.zcl.clusters.security import IasZone
+
+from zhaquirks import MotionWithReset
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+
+class MotionCluster(MotionWithReset):
+    """Motion cluster."""
+
+    reset_s: int = 60
+
+
+class SM0202Motion(CustomDevice):
+    """Quirk for LH-961ZB motion sensor."""
+
+    signature = {
+        # "endpoint": 1
+        # "profile_id": 260,
+        # "device_type": "0x0402",
+        # "in_clusters": ["0x0000","0x0001","0x0003", "0x0500", "0xeeff"],
+        # "out_clusters": ["0x0019"]
+        MODELS_INFO: [("_TYZB01_z2umiwvq", "SM0202")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    IasZone.cluster_id,
+                    0xEEFF,  # Unknown
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    MotionCluster,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+        },
+    }
diff --git a/zhaquirks/tuya/ts000x.py b/zhaquirks/tuya/ts000x.py
index f3a8546..8027b8d 100644
--- a/zhaquirks/tuya/ts000x.py
+++ b/zhaquirks/tuya/ts000x.py
@@ -322,6 +322,98 @@ class Switch_2G_Metering(EnchantedDevice, CustomDevice):
     }
 
 
+class Switch_2G_Var03(EnchantedDevice, CustomDevice):
+    """Tuya 2 gang (variation 03)."""
+
+    signature = {
+        MODEL: "TS0002",
+        ENDPOINTS: {
+            # <SimpleDescriptor endpoint=1 profile=260 device_type=256
+            # device_version=1
+            # input_clusters=[0, 3, 4, 5, 6, 57344, 57345]
+            # output_clusters=[10, 25]>
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    TuyaZBE000Cluster.cluster_id,
+                    TuyaZBExternalSwitchTypeCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            # <SimpleDescriptor endpoint=2 profile=260 device_type=256
+            # device_version=1
+            # input_clusters=[3, 4, 5, 6, 57344, 57345]
+            # output_clusters=[]>
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    TuyaZBE000Cluster.cluster_id,
+                    TuyaZBExternalSwitchTypeCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
+            # device_version=0
+            # input_clusters=[]
+            # output_clusters=[33]>
+            242: {
+                PROFILE_ID: 41440,
+                DEVICE_TYPE: 97,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
+                    TuyaZBE000Cluster,
+                    TuyaZBExternalSwitchTypeCluster,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
+                    TuyaZBE000Cluster,
+                    TuyaZBExternalSwitchTypeCluster,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: 41440,
+                DEVICE_TYPE: 97,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+
+
 class Switch_3G_GPP(EnchantedDevice, CustomDevice):
     """Tuya 3 gang switch module with restore power state support."""
 
diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py
index 27af824..d6c71d6 100644
--- a/zhaquirks/tuya/ts0601_dimmer.py
+++ b/zhaquirks/tuya/ts0601_dimmer.py
@@ -148,6 +148,7 @@ class TuyaSingleSwitchDimmerGP(TuyaDimmerSwitch):
         MODELS_INFO: [
             ("_TZE200_3p5ydos3", "TS0601"),
             ("_TZE200_ip2akl4w", "TS0601"),
+            ("_TZE200_vucankjx", "TS0601"),  # Loratap
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0100
@@ -207,6 +208,7 @@ class TuyaDoubleSwitchDimmerGP(TuyaDimmerSwitch):
     signature = {
         MODELS_INFO: [
             ("_TZE200_fjjbhx9d", "TS0601"),
+            ("_TZE200_gwkapsoq", "TS0601"),  # Loratap
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0100
diff --git a/zhaquirks/tuya/ts130f.py b/zhaquirks/tuya/ts130f.py
index eb0eef3..407bf0c 100644
--- a/zhaquirks/tuya/ts130f.py
+++ b/zhaquirks/tuya/ts130f.py
@@ -167,6 +167,67 @@ class TuyaZemismartTS130F(CustomDevice):
     }
 
 
+class TuyaTS130FTOGP(CustomDevice):
+    """Tuya Oxt smart curtain roller shutter."""
+
+    signature = {
+        MODEL: "TS130F",
+        ENDPOINTS: {
+            # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0202
+            # device_version=1
+            # input_clusters=[0x0000, 0x0004, 0x0005, 0x0102]
+            # output_clusters=[0x000a, 0x0019]>
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    WindowCovering.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
+            # input_clusters=[]
+            # output_clusters=[33]>
+            242: {
+                PROFILE_ID: 41440,
+                DEVICE_TYPE: 97,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaCoveringCluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+            242: {
+                PROFILE_ID: 41440,
+                DEVICE_TYPE: 97,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+
+
 class TuyaTS130FTI2(CustomDevice):
     """Tuya smart curtain roller shutter Time In."""
 


Test patch:
diff --git a/tests/test_tuya.py b/tests/test_tuya.py
index bcbcb48..25fc9f8 100644
--- a/tests/test_tuya.py
+++ b/tests/test_tuya.py
@@ -23,6 +23,7 @@ from zhaquirks.const import (
     ZONE_STATE,
 )
 from zhaquirks.tuya import Data, TuyaManufClusterAttributes, TuyaNewManufCluster
+import zhaquirks.tuya.sm0202_motion
 import zhaquirks.tuya.ts0042
 import zhaquirks.tuya.ts0043
 import zhaquirks.tuya.ts0501_fan_switch
@@ -1527,3 +1528,22 @@ async def test_fan_switch_writes_attributes(zigpy_device_from_quirk, quirk):
             1,
             b"\x00\x01\x02\x01\x000\x00",
         )
+
+
+async def test_sm0202_motion_sensor_signature(assert_signature_matches_quirk):
+    """Test LH992ZB motion sensor remote signature is matched to its quirk."""
+    signature = {
+        "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
+        "endpoints": {
+            "1": {
+                "profile_id": 260,
+                "device_type": "0x0402",
+                "in_clusters": ["0x0000", "0x0001", "0x0003", "0x0500", "0xeeff"],
+                "out_clusters": ["0x0019"],
+            }
+        },
+        "manufacturer": "_TYZB01_z2umiwvq",
+        "model": "SM0202",
+        "class": "zhaquirks.tuya.lh992zb.TuyaMotionSM0202",
+    }
+    assert_signature_matches_quirk(zhaquirks.tuya.sm0202_motion.SM0202Motion, signature)