Repository: zigpy/zha-device-handlers
Pull request #3724 (2025-01-18 15:54:58)

Fix Tuya smoke detector `_TZ3210_up3pngle`
## Proposed change
<!--
  Explain your proposed change below.
-->

When https://github.com/zigpy/zha-device-handlers/pull/3596 updated Tuya smoke detector quirks to v2, it broke _TZ3210_up3pngle. This fixes that.

## 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".
-->


## 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/3724?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
Attention: Patch coverage is `83.33333%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 89.86%. Comparing base [(`e61ecbb`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/e61ecbb4f64291c663fd19d23f95f7db5d33e1f0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`97ed877`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/97ed8778f32e90c74fbfcb0292b12b47e7e15f91?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

| [Files with missing lines](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3724?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) | Patch % | Lines |
|---|---|---|
| [zhaquirks/tuya/tuya\_smoke.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3724?src=pr&el=tree&filepath=zhaquirks%2Ftuya%2Ftuya_smoke.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHV5YV9zbW9rZS5weQ==) | 83.33% | [3 Missing :warning: ](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3724?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    #3724      +/-   ##
==========================================
- Coverage   89.87%   89.86%   -0.02%     
==========================================
  Files         320      320              
  Lines       10381    10399      +18     
==========================================
+ Hits         9330     9345      +15     
- Misses       1051     1054       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3724?dropdown=coverage&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).


Patch:
diff --git a/zhaquirks/tuya/tuya_smoke.py b/zhaquirks/tuya/tuya_smoke.py
index 0496743..01ce2ee 100644
--- a/zhaquirks/tuya/tuya_smoke.py
+++ b/zhaquirks/tuya/tuya_smoke.py
@@ -1,7 +1,56 @@
 """Smoke Sensor."""
 
+from zigpy.quirks.v2 import QuirkBuilder
+import zigpy.types as t
+from zigpy.zcl.clusters.general import OnOff, 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.tuya import TuyaManufClusterAttributes
 from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkBuilder
 
+
+class TuyaIasZone(LocalDataCluster, IasZone):
+    """IAS Zone."""
+
+    _CONSTANT_ATTRIBUTES = {
+        IasZone.AttributeDefs.zone_type.id: IasZone.ZoneType.Fire_Sensor
+    }
+
+
+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:
+            self.endpoint.ias_zone.update_attribute(
+                IasZone.AttributeDefs.zone_status.id,
+                IasZone.ZoneStatus.Alarm_1 if value == 0 else 0,
+            )
+
+
+(
+    QuirkBuilder("_TZ3210_up3pngle", "TS0205")
+    .removes(LightLink.cluster_id)
+    .removes(OnOff.cluster_id)
+    .removes(Time.cluster_id)
+    .replaces(TuyaIasZone)
+    .replaces(TuyaSmokeDetectorCluster)
+    .add_to_registry()
+)
+
 (
     TuyaQuirkBuilder("_TZE200_aycxwiau", "TS0601")
     .applies_to("_TZE200_dq1mfjug", "TS0601")
@@ -12,7 +61,6 @@ from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkB
     .applies_to("_TZE200_vzekyi4c", "TS0601")
     .applies_to("_TZE204_vawy74yh", "TS0601")
     .applies_to("_TZE284_0zaf1cr8", "TS0601")
-    .applies_to("_TZ3210_up3pngle", "TS0205")
     .tuya_smoke(dp_id=1)
     .skip_configuration()
     .add_to_registry()


Test patch:
diff --git a/tests/test_tuya_smoke.py b/tests/test_tuya_smoke.py
index d0eb952..1f439cc 100644
--- a/tests/test_tuya_smoke.py
+++ b/tests/test_tuya_smoke.py
@@ -31,7 +31,6 @@ zhaquirks.setup()
             ),
         ),
         ("_TZE284_0zaf1cr8", "TS0601", []),
-        ("_TZ3210_up3pngle", "TS0205", []),
     ],
 )
 async def test_handle_get_data(zigpy_device_from_v2_quirk, model, manuf, battery_test):
@@ -80,3 +79,32 @@ async def test_handle_get_data(zigpy_device_from_v2_quirk, model, manuf, battery
 
         assert len(power_listener.attribute_updates) == 1
         assert power_listener.attribute_updates[0][1] == state
+
+
+async def test_tuya_smoke_sensor_attribute_update(zigpy_device_from_v2_quirk):
+    """Test update_attribute on Tuya smoke sensor."""
+
+    device = zigpy_device_from_v2_quirk("_TZ3210_up3pngle", "TS0205")
+
+    tuya_cluster = device.endpoints[1].tuya_manufacturer
+    tuya_listener = ClusterListener(tuya_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
+    tuya_cluster.update_attribute(0x0401, 1)
+    assert len(tuya_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
+    tuya_cluster.update_attribute(0x0401, 0)
+    assert len(tuya_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