diff --git a/tests/test_sonoff.py b/tests/test_sonoff.py new file mode 100644 index 0000000000..17b340b3f7 --- /dev/null +++ b/tests/test_sonoff.py @@ -0,0 +1,43 @@ +"""Tests for Sonoff quirks.""" + +from unittest import mock + +import pytest + +import zhaquirks +import zhaquirks.sonoff.swv + +zhaquirks.setup() + + +def test_sonoff_swv(assert_signature_matches_quirk): + """Test new 'Sonoff SWV' signature is matched to its quirk.""" + + signature = { + "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4742, maximum_buffer_size=82, maximum_incoming_transfer_size=255, server_mask=11264, maximum_outgoing_transfer_size=255, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=True, *is_mains_powered=False, *is_receiver_on_when_idle=True, *is_router=False, *is_security_capable=False)", + "endpoints": { + "1": { + "profile_id": 0x0104, + "device_type": "0x0002", + "in_clusters": [ + "0x0000", + "0x0001", + "0x0003", + "0x0006", + "0x0020", + "0x0404", + "0x0b05", + "0xfc11", + "0xfc57", + ], + "out_clusters": ["0x000a", "0x0019"], + } + }, + "manufacturer": "SONOFF", + "model": "SWV", + "class": "sonoff.swv.SonoffSmartWaterValveSWV", + } + + assert_signature_matches_quirk( + zhaquirks.sonoff.swv.SonoffSmartWaterValveSWV, signature + ) diff --git a/zhaquirks/sonoff/swv.py b/zhaquirks/sonoff/swv.py new file mode 100644 index 0000000000..f9637c7e07 --- /dev/null +++ b/zhaquirks/sonoff/swv.py @@ -0,0 +1,112 @@ +"""Sonoff SWV - Zigbee smart water valve.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import ( + Basic, + PowerConfiguration, + Identify, + OnOff, + PollControl, + Ota, + Time, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.measurement import FlowMeasurement + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +SONOFF_CLUSTER_FC11_ID = 0xFC11 +SONOFF_CLUSTER_FC57_ID = 0xFC57 +ATTR_SONOFF_VALVE_STATUS = 0x500C + + +class ValveStatusBitmap(t.bitmap8): + """Valve status value enum.""" + + Water_Shortage = 0x1 + Water_Leakage = 0x2 + + +class SonoffFC11Cluster(CustomCluster): + """Sonoff manufacture specific cluster that provides valve status.""" + + @property + def _is_manuf_specific(self) -> bool: + """Override manufacturer specific property. + + valve_status return UNSUPPORTED_ATTRIBUTE if manufacturer_specific is set on report command. + We therefore need to treat cluster as if it doesn't have ID within manufacturer specific range + and explicitly set if each attribute is manufacturer_specific or not instead in attribute list. + """ + return False + + cluster_id = SONOFF_CLUSTER_FC11_ID + ep_attribute = "sonoff_manufacturer" + attributes = {ATTR_SONOFF_VALVE_STATUS: ("valve_status", ValveStatusBitmap, False)} + + +class SonoffSmartWaterValveSWV(CustomDevice): + """Sonoff smart water valve - model SWV.""" + + signature = { + # + MODELS_INFO: [ + ("SONOFF", "SWV"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_OUTPUT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + PollControl.cluster_id, + FlowMeasurement.cluster_id, + Diagnostic.cluster_id, + SONOFF_CLUSTER_FC11_ID, + SONOFF_CLUSTER_FC57_ID, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_OUTPUT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + PollControl.cluster_id, + FlowMeasurement.cluster_id, + Diagnostic.cluster_id, + SonoffFC11Cluster, + SONOFF_CLUSTER_FC57_ID, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + }, + }