Repository: zigpy/zha-device-handlers
Pull request #2991 (2024-02-19 10:25:30)

Add Tuya smoke detector TS0205 PST-YG500A
## Proposed change
<!--
  Explain your proposed change below.
-->
![tuya-smoke-detector-smart-zigbee](https://github.com/zigpy/zha-device-handlers/assets/22418250/13b49d57-ca86-4ab0-9d3e-c85690ab8c94)

![image](https://github.com/zigpy/zha-device-handlers/assets/22418250/f097a185-2904-48f8-88e5-02774ccc55cc)



## 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".
-->
Removed useless Opening sensor.
Changed motion sensor to smoke sensor.

## 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


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2991?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
Attention: `7 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`0df6f2b`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/0df6f2b6a7c3c895a73bedcc5846108c3050897b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 87.72% compared to head [(`b80878b`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2991?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 87.82%.
> Report is 2 commits behind head on dev.

| [Files](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2991?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) | Patch % | Lines |
|---|---|---|
| [zhaquirks/tuya/ts0205.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2991?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHMwMjA1LnB5) | 79.41% | [7 Missing :warning: ](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2991?src=pr&el=tree&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    #2991      +/-   ##
==========================================
+ Coverage   87.72%   87.82%   +0.09%     
==========================================
  Files         298      302       +4     
  Lines        9119     9246     +127     
==========================================
+ Hits         8000     8120     +120     
- Misses       1119     1126       +7     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2991?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

Fr33L4nc3r: @cdalexndr Seems the pipeline failed at pre-commit step. Since you fixed it in the past (https://github.com/zigpy/zha-device-handlers/pull/2991/commits/fa9848025ab0306627224b8b1adeb5617d9b7a37), any chance you could have a look again ?
It seems like there ain't much to have this PR approved (despite the change request still appearing in red, while it seemed addressed with https://github.com/zigpy/zha-device-handlers/pull/2991/commits/dcd03eb1035c7cd4ab2e283ba8c88b5b5464ff6b).

Looking forward to see this device recognized correctly and automatically in zha thanks to amazing community work like yours.

Patch:
diff --git a/zhaquirks/tuya/ts0205.py b/zhaquirks/tuya/ts0205.py
new file mode 100644
index 0000000..73054dd
--- /dev/null
+++ b/zhaquirks/tuya/ts0205.py
@@ -0,0 +1,111 @@
+"""Tuya TS0205 smoke detector."""
+
+from zigpy.profiles import zha
+from zigpy.quirks import CustomDevice
+import zigpy.types as t
+from zigpy.zcl.clusters.general import (
+    Basic,
+    Groups,
+    Identify,
+    OnOff,
+    Ota,
+    PowerConfiguration,
+    Scenes,
+    Time,
+)
+from zigpy.zcl.clusters.lightlink import LightLink
+from zigpy.zcl.clusters.security import IasZone
+from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef
+
+from zhaquirks import LocalDataCluster
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.tuya import TuyaManufClusterAttributes
+
+
+class TuyaSmokeDetectorCluster(TuyaManufClusterAttributes):
+    """Manufacturer Specific Cluster of the TS0205 smoke detector."""
+
+    class AttributeDefs(BaseAttributeDefs):
+        """Attribute definitions."""
+
+        smoke_detected = ZCLAttributeDef(
+            id=0x0401,  # [0]/[1] [Detected]/[Clear]
+            type=t.uint8_t,
+            is_manufacturer_specific=True,
+        )
+
+    def _update_attribute(self, attrid, value):
+        super()._update_attribute(attrid, value)
+        if attrid == self.AttributeDefs.smoke_detected.id:
+            if value == 0:
+                self.endpoint.ias_zone.update_attribute(
+                    IasZone.AttributeDefs.zone_status.id, IasZone.ZoneStatus.Alarm_1
+                )
+            else:
+                self.endpoint.ias_zone.update_attribute(
+                    IasZone.AttributeDefs.zone_status.id, 0
+                )
+
+
+class TuyaIasZone(LocalDataCluster, IasZone):
+    """IAS Zone."""
+
+    _CONSTANT_ATTRIBUTES = {
+        IasZone.AttributeDefs.zone_type.id: IasZone.ZoneType.Fire_Sensor
+    }
+
+
+class TuyaSmokeDetectorTS0205(CustomDevice):
+    """Tuya smoke sensor PST-YG500A."""
+
+    signature = {
+        MODELS_INFO: [("_TZ3210_up3pngle", "TS0205")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    IasZone.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                    LightLink.cluster_id,
+                ],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaIasZone,
+                    TuyaSmokeDetectorCluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Ota.cluster_id,
+                ],
+            }
+        }
+    }


Test patch:
diff --git a/tests/test_tuya_smoke.py b/tests/test_tuya_smoke.py
new file mode 100644
index 0000000..1f0540e
--- /dev/null
+++ b/tests/test_tuya_smoke.py
@@ -0,0 +1,68 @@
+"""Tests for Tuya Smoke Detector."""
+
+import pytest
+from zigpy.zcl.clusters.security import IasZone
+
+from tests.common import ClusterListener
+import zhaquirks.tuya.ts0205
+
+zhaquirks.setup()
+
+
+def test_ts0205_signature(assert_signature_matches_quirk):
+    """Test signature."""
+
+    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=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, 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": 0x0104,
+                "device_type": "0x0402",
+                "in_clusters": ["0x0000", "0x0001", "0x0004", "0x0005", "0x0500"],
+                "out_clusters": [
+                    "0x0003",
+                    "0x0004",
+                    "0x0006",
+                    "0x000a",
+                    "0x0019",
+                    "0x1000",
+                ],
+            }
+        },
+        "manufacturer": "_TZ3210_up3pngle",
+        "model": "TS0205",
+        "class": "ts0205.TuyaSmokeDetectorTS0205",
+    }
+    assert_signature_matches_quirk(
+        zhaquirks.tuya.ts0205.TuyaSmokeDetectorTS0205, signature
+    )
+
+
+@pytest.mark.parametrize("quirk", (zhaquirks.tuya.ts0205.TuyaSmokeDetectorTS0205,))
+async def test_tuya_smoke_sensor_attribute_update(zigpy_device_from_quirk, quirk):
+    """Test update_attribute on Tuya smoke sensor."""
+
+    device = zigpy_device_from_quirk(quirk)
+
+    opple_cluster = device.endpoints[1].tuya_manufacturer
+    opple_listener = ClusterListener(opple_cluster)
+
+    ias_cluster = device.endpoints[1].ias_zone
+    ias_listener = ClusterListener(ias_cluster)
+
+    zone_status_id = IasZone.AttributeDefs.zone_status.id
+
+    # check that updating smoke attribute also updates zone status on the Ias Zone cluster
+
+    # turn on smoke alarm
+    opple_cluster.update_attribute(0x0401, 1)
+    assert len(opple_listener.attribute_updates) == 1
+    assert len(ias_listener.attribute_updates) == 1
+    assert ias_listener.attribute_updates[0][0] == zone_status_id
+    assert ias_listener.attribute_updates[0][1] == 0
+    # turn off smoke alarm
+    opple_cluster.update_attribute(0x0401, 0)
+    assert len(opple_listener.attribute_updates) == 2
+    assert len(ias_listener.attribute_updates) == 2
+    assert ias_listener.attribute_updates[1][0] == zone_status_id
+    assert ias_listener.attribute_updates[1][1] == IasZone.ZoneStatus.Alarm_1