Repository: zigpy/zha-device-handlers
Pull request #2265 (2023-03-09 03:24:09)

Add Tuya TS0501 fan controller quirk
Fixes https://github.com/zigpy/zha-device-handlers/issues/2264
(Confirmed working in that issue)

This changes the `fan_mode_sequence` to `Low_Med_High` on pairing, so the physical switch still cycles through all modes properly.

(Not sure what use the Tuya cluster has, but had to pick one `CustomCluster` with the Tuya ID, as the Tuya cluster ID is outside of the range designated for manufacturers (so tests checking for a valid cluster range wouldn't pass before).
Is this fine?)

coveralls: ## Pull Request Test Coverage Report for [Build 4371344438](https://coveralls.io/builds/57680150)

* **12** of **15**   **(80.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.007%**) to **83.805%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [zhaquirks/tuya/ts0501_fan_switch.py](https://coveralls.io/builds/57680150/source?filename=zhaquirks%2Ftuya%2Fts0501_fan_switch.py#L28) | 12 | 15 | 80.0%
<!-- | **Total:** | **12** | **15** | **80.0%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/57680150/badge)](https://coveralls.io/builds/57680150) |
| :-- | --: |
| Change from base [Build 4368969604](https://coveralls.io/builds/57671831): |  -0.007% |
| Covered Lines: | 6867 |
| Relevant Lines: | 8194 |

---
##### 💛  - [Coveralls](https://coveralls.io)

codecov-commenter: # [Codecov](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
Patch coverage: **`84.21`**% and project coverage change: **`+0.01`** :tada:
> Comparison is base [(`1e9d03b`)](https://codecov.io/gh/zigpy/zha-device-handlers/commit/1e9d03b72cb86bcf3925fc6d266f97572c644f4f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 83.78% compared to head [(`d90ad42`)](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 83.80%.

: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    #2265      +/-   ##
==========================================
+ Coverage   83.78%   83.80%   +0.01%     
==========================================
  Files         257      258       +1     
  Lines        8167     8194      +27     
==========================================
+ Hits         6843     6867      +24     
- Misses       1324     1327       +3     
```


| [Impacted Files](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) | Coverage Δ | |
|---|---|---|
| [zhaquirks/tuya/ts0501\_fan\_switch.py](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHMwNTAxX2Zhbl9zd2l0Y2gucHk=) | `80.00% <80.00%> (ø)` | |
| [zhaquirks/tuya/ts011f\_plug.py](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHMwMTFmX3BsdWcucHk=) | `100.00% <100.00%> (ø)` | |
| [zhaquirks/const.py](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL2NvbnN0LnB5) | `100.00% <0.00%> (ø)` | |
| [zhaquirks/\_\_init\_\_.py](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL19faW5pdF9fLnB5) | `87.43% <0.00%> (+0.59%)` | :arrow_up: |

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 at Codecov](https://codecov.io/gh/zigpy/zha-device-handlers/pull/2265?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/tuya/ts0501_fan_switch.py b/zhaquirks/tuya/ts0501_fan_switch.py
new file mode 100644
index 0000000..e5ccc68
--- /dev/null
+++ b/zhaquirks/tuya/ts0501_fan_switch.py
@@ -0,0 +1,79 @@
+"""Quirk for TS0501 fan switch."""
+
+from zigpy.profiles import zha
+from zigpy.quirks import CustomCluster, CustomDevice
+from zigpy.zcl.clusters.general import Basic, Groups, Identify, OnOff, Ota, Scenes, Time
+from zigpy.zcl.clusters.hvac import Fan
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.tuya import TUYA_CLUSTER_ID, TuyaManufCluster
+
+
+class FanCluster(CustomCluster, Fan):
+    """Custom fan cluster."""
+
+    attr_config = {
+        Fan.attributes_by_name["fan_mode_sequence"].id: Fan.FanModeSequence.Low_Med_High
+    }
+
+    async def bind(self):
+        """Bind fan cluster and write attributes."""
+        result = await super().bind()
+        await self.write_attributes(self.attr_config)
+        return result
+
+
+class TS0501FanSwitch(CustomDevice):
+    """TS0501 fan switch custom device implementation."""
+
+    signature = {
+        MODELS_INFO: [("_TZ3210_lzqq3u4r", "TS0501")],
+        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,
+                    OnOff.cluster_id,
+                    Fan.cluster_id,
+                    TUYA_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.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,
+                    OnOff.cluster_id,
+                    FanCluster,
+                    TuyaManufCluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
diff --git a/zhaquirks/xiaomi/aqara/water_acn001.py b/zhaquirks/xiaomi/aqara/water_acn001.py
new file mode 100644
index 0000000..93797a8
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/water_acn001.py
@@ -0,0 +1,62 @@
+"""Quirk for Aqara E1 water leak sensor lumi.flood.acn001."""
+
+from zigpy.profiles import zha
+from zigpy.zcl.clusters.general import Basic, Identify, 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 (
+    LUMI,
+    BasicCluster,
+    XiaomiAqaraE1Cluster,
+    XiaomiCustomDevice,
+    XiaomiPowerConfiguration,
+)
+
+XIAOMI_CLUSTER_ID = 0xFCC0
+
+
+class WaterE1(XiaomiCustomDevice):
+    """Aqara E1 water leak sensor quirk."""
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.flood.acn001")],
+        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,
+                    XIAOMI_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    XiaomiPowerConfiguration,
+                    Identify.cluster_id,
+                    IasZone.cluster_id,
+                    XiaomiAqaraE1Cluster,
+                ],
+                OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id],
+            },
+        },
+    }


Test patch:
diff --git a/tests/test_tuya.py b/tests/test_tuya.py
index 658ea41..bcbcb48 100644
--- a/tests/test_tuya.py
+++ b/tests/test_tuya.py
@@ -25,6 +25,7 @@ from zhaquirks.const import (
 from zhaquirks.tuya import Data, TuyaManufClusterAttributes, TuyaNewManufCluster
 import zhaquirks.tuya.ts0042
 import zhaquirks.tuya.ts0043
+import zhaquirks.tuya.ts0501_fan_switch
 import zhaquirks.tuya.ts0601_electric_heating
 import zhaquirks.tuya.ts0601_motion
 import zhaquirks.tuya.ts0601_siren
@@ -1502,3 +1503,27 @@ async def test_rh_multiplier(zigpy_device_from_quirk, quirk, quirk_sq):
         square_humidity.get("measured_value")
         == square_data.data.datapoints[0].data.payload * 100
     )  # no square_sensor.RH_MULTIPLIER attribute
+
+
+@mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock())
+@pytest.mark.parametrize(
+    "quirk",
+    (zhaquirks.tuya.ts0501_fan_switch.TS0501FanSwitch,),
+)
+async def test_fan_switch_writes_attributes(zigpy_device_from_quirk, quirk):
+    """Test that fan mode sequence attribute is written to the device when binding."""
+
+    device = zigpy_device_from_quirk(quirk)
+    fan_cluster = device.endpoints[1].fan
+
+    with mock.patch.object(fan_cluster.endpoint, "request", mock.AsyncMock()) as m1:
+        m1.return_value = (foundation.Status.SUCCESS, "done")
+
+        await fan_cluster.bind()
+
+        assert len(m1.mock_calls) == 1
+        assert m1.mock_calls[0][1] == (
+            514,
+            1,
+            b"\x00\x01\x02\x01\x000\x00",
+        )