Repository: zigpy/zha-device-handlers (python)

Issue:
[Device Support Request] for Aqara T1 door and window sensor
              hey, I got 2  Aqara T1 door and window sensor( new version that had been launched in the past month, and it seems it is not supported either. neither the status or the battery. Any thoughts on how to fix this?

Device is initialized <Device model='lumi.magnet.agl02' manuf='LUMI' nwk=0x91F1 ieee=54:ef:44:10:00:92:52:06 is_initialized=True>
Checking quirks for LUMI lumi.magnet.agl02 (54:ef:44:10:00:92:52:06)
Considering <class 'zhaquirks.xiaomi.aqara.opple_switch.XiaomiOpple2ButtonSwitchFace2'>
Fail because endpoint list mismatch: {1, 2, 242} {1}
Considering <class 'zhaquirks.xiaomi.aqara.opple_switch.XiaomiOpple2ButtonSwitchFace1'>
Fail because endpoint list mismatch: {1, 2, 41, 42, 242, 51, 21, 31} {1}
Considering <class 'zhaquirks.xbee.xbee_io.XBeeSensor'>
Fail because endpoint list mismatch: {232, 230} {1}
Considering <class 'zhaquirks.xbee.xbee3_io.XBee3Sensor'>
Fail because endpoint list mismatch: {232, 230} {1}
Considering <class 'zhaquirks.tuya.ts0201.MoesTemperatureHumidtySensorWithScreen'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.smartthings.tag_v4.SmartThingsTagV4'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.smartthings.multi.SmartthingsMultiPurposeSensor'>
Fail because input cluster mismatch on at least one endpoint
Considering <class 'zhaquirks.netvox.z308e3ed.Z308E3ED'>
Fail because input cluster mismatch on at least one endpoint
Considering <class 'zhaquirks.gledopto.soposhgu10.SoposhGU10'>
Fail because endpoint list mismatch: {11, 13} {1}

this is the spec page i found digging around 
https://home.miot-spec.com/spec?type=urn:miot-spec-v2:device:magnet-sensor:0000A016:lumi-agl02:1:0000C80D

_Originally posted by @vistvanv in https://github.com/zigpy/zha-device-handlers/issues/2582#issuecomment-1733241147_
            

Pull request:
Add Aqara T1 door sensor support
## Proposed change
<!--
  Explain your proposed change below.
-->
Adds support for the Aqara T1 door sensor.
This device seems to use the `OnOff` cluster instead of the `IasZone` cluster.
(Should we also change the `DeviceType` then. If yes, to what though? Doesn't really matter for functionality)

It's possible that the other cluster is used, because coordinators are sending Xiaomi manufacturer codes during pairing, so the sensor thinks it's paired to a Xiaomi hub and then uses the `OnOff` cluster instead of `IasZone` cluster. This is just a guess though.

So this quirk fixes the actual open/close sensor functionality and also adds battery measurement support through Xiaomi's attribute reports.

## Additional information
<!--
  Please include any additional information that is important to this PR.
  For example, if this PR is a potentially breaking change, mention that here.
  If this PR requires other PRs to be merged in HA Core or other projects, mention that.
  Lastly, if this PR fixes a specific issue, please include "Fixes #xxxx".
-->
See https://github.com/home-assistant/core/issues/100805
Fixes #2631


## Checklist
<!--
  Put an 'x' in all boxes that apply.
  Note: You do not need to tick all boxes before creating a PR.
-->

- [x] The changes are tested and work correctly
- [x] `pre-commit` checks pass / the code has been formatted using Black
- [x] Tests have been added to verify that the new code works


Patch:
diff --git a/zhaquirks/gledopto/glsd001.py b/zhaquirks/gledopto/glsd_dimmer.py
similarity index 88%
rename from zhaquirks/gledopto/glsd001.py
rename to zhaquirks/gledopto/glsd_dimmer.py
index 4f9d31c..824ff50 100644
--- a/zhaquirks/gledopto/glsd001.py
+++ b/zhaquirks/gledopto/glsd_dimmer.py
@@ -1,4 +1,4 @@
-"""Quirk for GLEDOPTO GL-SD-001."""
+"""Quirk for GLEDOPTO GL-SD-* dimmers."""
 
 from zigpy.profiles import zha
 from zigpy.quirks import CustomCluster, CustomDevice
@@ -30,11 +30,11 @@ class LevelControlNoReply(NoReplyMixin, CustomCluster, LevelControl):
     void_input_commands = {cmd.id for cmd in LevelControl.commands_by_name.values()}
 
 
-class GledoptoGlSd001(CustomDevice):
-    """Gledopto GL-SD-001 dimmer custom device implementation."""
+class GledoptoGlSdDimmer(CustomDevice):
+    """Gledopto GL-SD-001 & GL-SD-003P dimmer custom device implementation."""
 
     signature = {
-        MODELS_INFO: [("GLEDOPTO", "GL-SD-001")],
+        MODELS_INFO: [("GLEDOPTO", "GL-SD-001"), ("GLEDOPTO", "GL-SD-003P")],
         ENDPOINTS: {
             1: {
                 PROFILE_ID: zha.PROFILE_ID,
diff --git a/zhaquirks/xiaomi/aqara/magnet_agl02.py b/zhaquirks/xiaomi/aqara/magnet_agl02.py
new file mode 100644
index 0000000..2a63c40
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/magnet_agl02.py
@@ -0,0 +1,67 @@
+"""Quirk for Aqara T1 door sensor lumi.magnet.agl02."""
+
+from zigpy.profiles import zha
+from zigpy.zcl.clusters.general import Basic, Identify, OnOff, Ota, PowerConfiguration
+from zigpy.zcl.clusters.security import IasZone
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.xiaomi import (
+    BasicCluster,
+    XiaomiAqaraE1Cluster,
+    XiaomiCustomDevice,
+    XiaomiPowerConfiguration,
+)
+
+
+class MagnetT1(XiaomiCustomDevice):
+    """Aqara T1 door sensor quirk."""
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        self.battery_size = 11
+        super().__init__(*args, **kwargs)
+
+    signature = {
+        MODELS_INFO: [("LUMI", "lumi.magnet.agl02")],
+        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,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    XiaomiPowerConfiguration,
+                    Identify.cluster_id,
+                    XiaomiAqaraE1Cluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    OnOff.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }


Test patch:
diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py
index 624f19e..654ef9f 100644
--- a/tests/test_xiaomi.py
+++ b/tests/test_xiaomi.py
@@ -11,6 +11,7 @@ from zigpy.zcl import foundation
 from zigpy.zcl.clusters.general import (
     AnalogInput,
     DeviceTemperature,
+    OnOff,
     PowerConfiguration,
 )
 from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
@@ -45,6 +46,7 @@ from zhaquirks.xiaomi import (
     XIAOMI_AQARA_ATTRIBUTE_E1,
     XIAOMI_NODE_DESC,
     BasicCluster,
+    XiaomiAqaraE1Cluster,
     XiaomiCustomDevice,
     XiaomiQuickInitDevice,
     handle_quick_init,
@@ -65,6 +67,7 @@ from zhaquirks.xiaomi.aqara.feeder_acn001 import (
     AqaraFeederAcn001,
     OppleCluster,
 )
+import zhaquirks.xiaomi.aqara.magnet_agl02
 import zhaquirks.xiaomi.aqara.motion_ac02
 import zhaquirks.xiaomi.aqara.motion_aq2
 import zhaquirks.xiaomi.aqara.motion_aq2b
@@ -1283,3 +1286,79 @@ async def test_xiaomi_e1_roller_curtain_battery(zigpy_device_from_quirk, quirk):
     assert power_listener.attribute_updates[0][1] == 28.9
     assert power_listener.attribute_updates[1][0] == zcl_power_percent_id
     assert power_listener.attribute_updates[1][1] == 120
+
+
+@pytest.mark.parametrize(
+    "raw_report, expected_results",
+    (
+        [
+            "1C5F11670AF700412E0121B00C0328190421A8130521090006240D0000000008211E010A2100000C20016410016620036720016821A800",
+            [
+                32.5,  # battery voltage
+                200,  # battery percent * 2
+            ],
+        ],
+    ),
+)
+async def test_xiaomi_t1_door_sensor(
+    zigpy_device_from_quirk, raw_report, expected_results
+):
+    """Test Aqara T1 door sensor."""
+    raw_report = bytes.fromhex(raw_report)
+
+    device = zigpy_device_from_quirk(zhaquirks.xiaomi.aqara.magnet_agl02.MagnetT1)
+
+    on_off_cluster = device.endpoints[1].out_clusters[OnOff.cluster_id]
+    on_off_listener = ClusterListener(on_off_cluster)
+
+    # check open state
+    device.handle_message(
+        260,
+        on_off_cluster.cluster_id,
+        on_off_cluster.endpoint.endpoint_id,
+        on_off_cluster.endpoint.endpoint_id,
+        bytes.fromhex("185D0A00001001"),
+    )
+
+    assert len(on_off_listener.attribute_updates) == 1
+    assert on_off_listener.attribute_updates[0][0] == OnOff.AttributeDefs.on_off.id
+    assert on_off_listener.attribute_updates[0][1] == t.Bool.true
+
+    # check closed state
+    device.handle_message(
+        260,
+        on_off_cluster.cluster_id,
+        on_off_cluster.endpoint.endpoint_id,
+        on_off_cluster.endpoint.endpoint_id,
+        bytes.fromhex("18640A00001000"),
+    )
+
+    assert len(on_off_listener.attribute_updates) == 2
+    assert on_off_listener.attribute_updates[1][0] == OnOff.AttributeDefs.on_off.id
+    assert on_off_listener.attribute_updates[1][1] == t.Bool.false
+
+    opple_cluster = device.endpoints[1].in_clusters[XiaomiAqaraE1Cluster.cluster_id]
+    ClusterListener(opple_cluster)
+
+    power_cluster = device.endpoints[1].power
+    power_listener = ClusterListener(power_cluster)
+
+    zcl_power_voltage_id = PowerConfiguration.AttributeDefs.battery_voltage.id
+    zcl_power_percent_id = (
+        PowerConfiguration.AttributeDefs.battery_percentage_remaining.id
+    )
+
+    # check Xiaomi attribute report
+    device.handle_message(
+        260,
+        opple_cluster.cluster_id,
+        opple_cluster.endpoint.endpoint_id,
+        opple_cluster.endpoint.endpoint_id,
+        raw_report,
+    )
+
+    assert len(power_listener.attribute_updates) == 2
+    assert power_listener.attribute_updates[0][0] == zcl_power_voltage_id
+    assert power_listener.attribute_updates[0][1] == expected_results[0]
+    assert power_listener.attribute_updates[1][0] == zcl_power_percent_id
+    assert power_listener.attribute_updates[1][1] == expected_results[1]