diff --git a/README.md b/README.md index 3d5070dece..fc1b49f998 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ZHA Device Handlers are custom quirks implementations for [Zigpy](https://github ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices. Zigbee devices that deviate from or do not fully conform to the standard specifications set by the Zigbee Alliance may require the development of custom ZHA Device Handlers (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. -Custom quirks implementations for zigpy implemented as ZHA Device Handlers are a similar concept to that of [Hub-connected Device Handlers for the SmartThings Classics platform](https://docs.smartthings.com/en/latest/device-type-developers-guide/) as well that of [Zigbee-Herdsman Converters / Zigbee-Shepherd Converters as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html), meaning they are virtual representation of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. See [Device Specifics](#Device-Specifics) for details. +Custom quirks implementations for zigpy implemented as ZHA Device Handlers are a similar concept to that of [Hub-connected Device Handlers for the SmartThings Classics platform](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/) as well that of [Zigbee-Herdsman Converters (formerly Zigbee-Shepherd Converters) as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html), meaning they are virtual representation of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. See [Device Specifics](#Device-Specifics) for details. # How to contribute @@ -17,15 +17,33 @@ ZHA device handlers and it's provided Quirks allow Zigpy, ZHA and Home Assistant ## What are these specifications -[Zigbee PRO 2017 (R22) Protocol Specification](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) - -[Zigbee Cluster Library (R8)](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf) - -[Zigbee Base Device Behavior Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/zip/zigbee-base-device-behavior-bdb-v1-0.zip) - -[Zigbee Lighting & Occupancy Device Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-15-0014-05-0plo-Lighting-OccupancyDevice-Specification-V1.0.pdf) - -[Zigbee Primer](https://docs.smartthings.com/en/latest/device-type-developers-guide/zigbee-primer.html) +Reference official Zigbee specification documentation from Connectivity Standards Alliance (a.k.a. "CSA-IOT", formerly "Zigbee Alliance"): + +- Zigbee Protocol Specification (also known as "Zigbee Pro" specifications) + - [Zigbee Protocol Specification 2023 (also known as "Zigbee PRO 2023" or just Zigbee R23)](https://csa-iot.org/wp-content/uploads/2023/04/05-3474-23-csg-zigbee-specification-compressed.pdf) + - [Zigbee Protocol Specification 2017 (also known as "Zigbee PRO 2017" or just Zigbee R22)](https://csa-iot.org/wp-content/uploads/2022/01/docs-05-3474-22-0csg-zigbee-specification-1.pdf) + - [Zigbee Protocol Specification 2015 (also known as "Zigbee PRO 2015" or just Zigbee R21)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) +- Zigbee Cluster Library Specification + - [Zigbee Cluster Library Specification R8 (Revision 8)](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf) + - [Zigbee Cluster Library Specification R7 (Revision 7)](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/Zigbee%20Cluster%20Library%20Specification%20v7.pdf) + - [Zigbee Cluster Library Specification R6 (Revision 6)](https://zigbeealliance.org/wp-content/uploads/2019/12/07-5123-06-zigbee-cluster-library-specification.pdf) +- Zigbee Device Specifications + - [Zigbee Base Device Behavior Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/12/docs-13-0402-13-00zi-Base-Device-Behavior-Specification-2-1.pdf) + - [Zigbee Lighting & Occupancy Device Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-15-0014-05-0plo-Lighting-OccupancyDevice-Specification-V1.0.pdf) +- ZigBee Green Power (ZGP "GreenPower" Profile) specifications + - [Zigbee PRO Green Power feature specification Basic functionality set (v 1.1.1)](https://csa-iot.org/wp-content/uploads/2022/01/docs-14-0563-18-batt-Green-Power-Basic-specification-v1.1.1.pdf) + - [Zigbee PRO Green Power feature Specification 1.0a (Revision 26)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-09-5499-26-batt-zigbee-green-power-specification.pdf) +- ZigBee Smart Energy (ZSE / Zigbee SE "Smart Energy" Profile) specifications + - Zigbee Smart Energy Standard 1.4 + - [ZigBee Smart Energy Standard (v1.2a)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-07-5356-19-0zse-zigbee-smart-energy-profile-specification.pdf) + +In additional you can also reference third-party and manufacturer specific documentation: + +- [Tuya - Zigbee Connection Standard (Tuya Smart Documentation)](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/Zigbee%20Connection%20Standard_Tuya%20Smart_Documentation.pdf) + - [Zigbee2MQTT guide on understanding the custom 'manuSpecificTuya' cluster that TuYa devices uses](https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html) +- [Samsung SmartThings -Device Handlers](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/) + - [Samsung SmartThings - Zigbee Primer](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/zigbee-primer.html) + - [Samsung SmartThings - Building ZigBee Device Handlers](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/building-zigbee-device-handlers.html) ## What is a device in human terms diff --git a/setup.py b/setup.py index 5b24b56059..f21605f6e4 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.99" +VERSION = "0.0.100" setup( diff --git a/tests/test_develco.py b/tests/test_develco.py index b584297ef6..6c58846890 100644 --- a/tests/test_develco.py +++ b/tests/test_develco.py @@ -1,6 +1,31 @@ """Tests for Develco/Frient A/S quirks.""" +import pytest +from zigpy.zcl.clusters.general import DeviceTemperature import zhaquirks.develco.motion +import zhaquirks.develco.power_plug + +from tests.common import ClusterListener + +zhaquirks.setup() + + +@pytest.mark.parametrize("quirk", (zhaquirks.develco.power_plug.SPLZB131,)) +async def test_develco_plug_device_temp_multiplier(zigpy_device_from_quirk, quirk): + """Test device temperature multiplication.""" + + device = zigpy_device_from_quirk(quirk) + + dev_temp_cluster = device.endpoints[2].device_temperature + dev_temp_listener = ClusterListener(dev_temp_cluster) + + dev_temp_attr_id = DeviceTemperature.attributes_by_name["current_temperature"].id + + # turn off heating + dev_temp_cluster._update_attribute(dev_temp_attr_id, 25) + assert len(dev_temp_listener.attribute_updates) == 1 + assert dev_temp_listener.attribute_updates[0][0] == dev_temp_attr_id + assert dev_temp_listener.attribute_updates[0][1] == 2500 # multiplied by 100 def test_motion_signature(assert_signature_matches_quirk): diff --git a/tests/test_ikea.py b/tests/test_ikea.py index 37126d9a46..397ef523a4 100644 --- a/tests/test_ikea.py +++ b/tests/test_ikea.py @@ -1,7 +1,15 @@ """Tests for Ikea Starkvind quirks.""" +from unittest import mock + +from zigpy.zcl import foundation +from zigpy.zcl.clusters.measurement import PM25 + +import zhaquirks import zhaquirks.ikea.starkvind +zhaquirks.setup() + def test_ikea_starkvind(assert_signature_matches_quirk): """Test new 'STARKVIND Air purifier table' signature is matched to its quirk.""" @@ -72,3 +80,45 @@ def test_ikea_starkvind_v2(assert_signature_matches_quirk): } assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND_v2, signature) + + +async def test_pm25_cluster_read(zigpy_device_from_quirk): + """Test reading from PM25 cluster""" + + starkvind_device = zigpy_device_from_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND) + assert starkvind_device.model == "STARKVIND Air purifier" + + pm25_cluster = starkvind_device.endpoints[1].in_clusters[PM25.cluster_id] + ikea_cluster = starkvind_device.endpoints[1].in_clusters[ + zhaquirks.ikea.starkvind.IkeaAirpurifier.cluster_id + ] + + # Mock the read attribute to on the IkeaAirpurifier cluster + # to always return 6 for anything. + def mock_read(attributes, manufacturer=None): + records = [ + foundation.ReadAttributeRecord( + attr, foundation.Status.SUCCESS, foundation.TypeValue(None, 6) + ) + for attr in attributes + ] + return (records,) + + patch_ikeacluster_read = mock.patch.object( + ikea_cluster, "_read_attributes", mock.AsyncMock(side_effect=mock_read) + ) + with patch_ikeacluster_read: + # Reading "measured_value" should read the "air_quality_25pm" value from + # the IkeaAirpurifier cluster + success, fail = await pm25_cluster.read_attributes(["measured_value"]) + assert success + assert 6 in success.values() + assert not fail + + # Same call with allow_cache=True; a bug previously prevented this from working + success, fail = await pm25_cluster.read_attributes( + ["measured_value"], allow_cache=True + ) + assert success + assert 6 in success.values() + assert not fail diff --git a/tests/test_legrand.py b/tests/test_legrand.py index 952e1515af..ab98c25f91 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -26,3 +26,31 @@ async def test_legrand_battery(zigpy_device_from_quirk, voltage, bpr): power_cluster = device.endpoints[1].power power_cluster.update_attribute(0x0020, voltage) assert power_cluster["battery_percentage_remaining"] == bpr + + +def test_light_switch_with_neutral_signature(assert_signature_matches_quirk): + signature = { + "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=1, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4129, maximum_buffer_size=89, maximum_incoming_transfer_size=63, server_mask=10752, maximum_outgoing_transfer_size=63, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", + "endpoints": { + "1": { + "profile_id": 260, + "device_type": "0x0100", + "in_clusters": [ + "0x0000", + "0x0003", + "0x0004", + "0x0005", + "0x0006", + "0x000f", + "0xfc01", + ], + "out_clusters": ["0x0000", "0x0019", "0xfc01"], + } + }, + "manufacturer": " Legrand", + "model": " Light switch with neutral", + "class": "zigpy.device.Device", + } + assert_signature_matches_quirk( + zhaquirks.legrand.switch.LightSwitchWithNeutral, signature + ) diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 8d8aa37e1c..e19f7e6a30 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -772,3 +772,28 @@ def test_attributes_updated_not_replaced(quirk: CustomDevice) -> None: f"Cluster {cluster} deletes parent class's attributes instead of" f" extending them: {base_attr_names - quirk_attr_names}" ) + + +@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES) +def test_no_duplicate_clusters(quirk: CustomDevice) -> None: + """Verify no quirks contain clusters with duplicate cluster ids in the replacement.""" + + def check_for_duplicate_cluster_ids(clusters) -> None: + used_cluster_ids = set() + + for cluster in clusters: + if isinstance(cluster, int): + cluster_id = cluster + else: + cluster_id = cluster.cluster_id + + if cluster_id in used_cluster_ids: + pytest.fail( + f"Cluster ID 0x{cluster_id:04X} is used more than once in the" + f" replacement for endpoint {ep_id} in {quirk}" + ) + used_cluster_ids.add(cluster_id) + + for ep_id, ep_data in quirk.replacement[ENDPOINTS].items(): + check_for_duplicate_cluster_ids(ep_data.get(INPUT_CLUSTERS, [])) + check_for_duplicate_cluster_ids(ep_data.get(OUTPUT_CLUSTERS, [])) diff --git a/tests/test_tuya.py b/tests/test_tuya.py index 4316df6b91..9b5b5d8b73 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -1408,6 +1408,25 @@ def test_ts0601_valve_signature(assert_signature_matches_quirk): assert_signature_matches_quirk(zhaquirks.tuya.ts0601_valve.TuyaValve, signature) +def test_ts0601_motion_signature(assert_signature_matches_quirk): + """Test TS0601 motion by TreatLife remote 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=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=, *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": 260, + "device_type": "0x0051", + "in_clusters": ["0x0000", "0x0004", "0x0005", "0xef00"], + "out_clusters": ["0x000a", "0x0019"], + } + }, + "manufacturer": "_TZE200_ppuj1vem", + "model": "TS0601", + "class": "zigpy.device.Device", + } + assert_signature_matches_quirk(zhaquirks.tuya.ts0601_motion.NeoMotion, signature) + + def test_multiple_attributes_report(): """Test a multi attribute report from Tuya device.""" diff --git a/zhaquirks/develco/power_plug.py b/zhaquirks/develco/power_plug.py new file mode 100644 index 0000000000..6fddb78094 --- /dev/null +++ b/zhaquirks/develco/power_plug.py @@ -0,0 +1,106 @@ +"""Develco smart plugs.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import ( + Alarms, + Basic, + DeviceTemperature, + Groups, + Identify, + OnOff, + Ota, + Scenes, + Time, +) +from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement +from zigpy.zcl.clusters.measurement import OccupancySensing +from zigpy.zcl.clusters.smartenergy import Metering + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.develco import DEVELCO + +DEV_TEMP_ID = DeviceTemperature.attributes_by_name["current_temperature"].id + + +class DevelcoDeviceTemperature(CustomCluster, DeviceTemperature): + """Custom device temperature cluster to multiply the temperature by 100.""" + + def _update_attribute(self, attrid, value): + if attrid == DEV_TEMP_ID: + value = value * 100 + super()._update_attribute(attrid, value) + + +class SPLZB131(CustomDevice): + """Custom device Develco smart plug device.""" + + signature = { + MODELS_INFO: [(DEVELCO, "SPLZB-131")], + ENDPOINTS: { + 1: { + PROFILE_ID: 0xC0C9, + DEVICE_TYPE: 1, + INPUT_CLUSTERS: [ + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Alarms.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Time.cluster_id, + Ota.cluster_id, + OccupancySensing.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DevelcoDeviceTemperature, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Alarms.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Time.cluster_id, + Ota.cluster_id, + OccupancySensing.cluster_id, + ], + }, + } + } diff --git a/zhaquirks/ikea/starkvind.py b/zhaquirks/ikea/starkvind.py index 88d6a57171..1b30afb188 100644 --- a/zhaquirks/ikea/starkvind.py +++ b/zhaquirks/ikea/starkvind.py @@ -121,7 +121,7 @@ async def read_attributes( await self.endpoint.device.endpoints[1] .in_clusters[64637] .read_attributes( - {"air_quality_25pm"}, + ["air_quality_25pm"], allow_cache=allow_cache, only_cache=only_cache, manufacturer=manufacturer, diff --git a/zhaquirks/legrand/__init__.py b/zhaquirks/legrand/__init__.py index c5c474299e..41d5f8d55a 100644 --- a/zhaquirks/legrand/__init__.py +++ b/zhaquirks/legrand/__init__.py @@ -1,2 +1,30 @@ """Module for Legrand devices.""" + +from zigpy.quirks import CustomCluster +import zigpy.types as t +from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster + +from zhaquirks import PowerConfigurationCluster + LEGRAND = "Legrand" +MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC01 # decimal = 64513 + + +class LegrandCluster(CustomCluster, ManufacturerSpecificCluster): + """LegrandCluster.""" + + cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID + name = "LegrandCluster" + ep_attribute = "legrand_cluster" + attributes = { + 0x0000: ("dimmer", t.data16, True), + 0x0001: ("led_dark", t.Bool, True), + 0x0002: ("led_on", t.Bool, True), + } + + +class LegrandPowerConfigurationCluster(PowerConfigurationCluster): + """PowerConfiguration conversor 'V --> %' for Legrand devices.""" + + MIN_VOLTS = 2.5 + MAX_VOLTS = 3.0 diff --git a/zhaquirks/legrand/dimmer.py b/zhaquirks/legrand/dimmer.py index 46abb40c69..e5479ccd06 100644 --- a/zhaquirks/legrand/dimmer.py +++ b/zhaquirks/legrand/dimmer.py @@ -1,7 +1,7 @@ -"""Device handler for Legrand Dimmer switch w/o neutral.""" +"""Module for Legrand dimmers.""" + from zigpy.profiles import zha -from zigpy.quirks import CustomCluster, CustomDevice -import zigpy.types as t +from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, BinaryInput, @@ -15,7 +15,6 @@ Scenes, ) from zigpy.zcl.clusters.lighting import Ballast -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster from zhaquirks import PowerConfigurationCluster from zhaquirks.const import ( @@ -26,29 +25,12 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.legrand import LEGRAND - -MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC01 # decimal = 64513 - - -class LegrandCluster(CustomCluster, ManufacturerSpecificCluster): - """LegrandCluster.""" - - cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID - name = "LegrandCluster" - ep_attribute = "legrand_cluster" - attributes = { - 0x0000: ("dimmer", t.data16, True), - 0x0001: ("led_dark", t.Bool, True), - 0x0002: ("led_on", t.Bool, True), - } - - -class LegrandPowerConfigurationCluster(PowerConfigurationCluster): - """PowerConfiguration conversor 'V --> %' for Legrand devices.""" - - MIN_VOLTS = 2.5 - MAX_VOLTS = 3.0 +from zhaquirks.legrand import ( + LEGRAND, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + LegrandCluster, + LegrandPowerConfigurationCluster, +) class DimmerWithoutNeutral(CustomDevice): diff --git a/zhaquirks/legrand/switch.py b/zhaquirks/legrand/switch.py new file mode 100644 index 0000000000..dd8fbb85eb --- /dev/null +++ b/zhaquirks/legrand/switch.py @@ -0,0 +1,77 @@ +"""Module for Legrand switches (without dimming functionality).""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + BinaryInput, + Groups, + Identify, + OnOff, + Ota, + Scenes, +) + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.legrand import LEGRAND, MANUFACTURER_SPECIFIC_CLUSTER_ID, LegrandCluster + + +class LightSwitchWithNeutral(CustomDevice): + """Light switch with neutral wire.""" + + signature = { + # + MODELS_INFO: [(f" {LEGRAND}", " Light switch with neutral")], + 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, + BinaryInput.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Ota.cluster_id, + MANUFACTURER_SPECIFIC_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, + BinaryInput.cluster_id, + LegrandCluster, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Ota.cluster_id, + LegrandCluster, + ], + } + } + } diff --git a/zhaquirks/smartthings/multi.py b/zhaquirks/smartthings/multi.py index 52c61dfd24..ac987558c6 100644 --- a/zhaquirks/smartthings/multi.py +++ b/zhaquirks/smartthings/multi.py @@ -12,6 +12,13 @@ from zigpy.zcl.clusters.measurement import TemperatureMeasurement from zigpy.zcl.clusters.security import IasZone +from zhaquirks import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + OUTPUT_CLUSTERS, + PROFILE_ID, +) from zhaquirks.smartthings import SmartThingsAccelCluster @@ -19,14 +26,14 @@ class SmartthingsMultiPurposeSensor(CustomDevice): """Custom device representing a Smartthings Multi Purpose Sensor.""" signature = { - "endpoints": { + ENDPOINTS: { # 1: { - "profile_id": zha.PROFILE_ID, - "device_type": zha.DeviceType.IAS_ZONE, - "input_clusters": [ + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ Basic.cluster_id, PowerConfiguration.cluster_id, Identify.cluster_id, @@ -35,25 +42,24 @@ class SmartthingsMultiPurposeSensor(CustomDevice): IasZone.cluster_id, SmartThingsAccelCluster.cluster_id, ], - "output_clusters": [Identify.cluster_id, Ota.cluster_id], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], } } } replacement = { - "endpoints": { + ENDPOINTS: { 1: { - "input_clusters": [ + INPUT_CLUSTERS: [ Basic.cluster_id, PowerConfiguration.cluster_id, Identify.cluster_id, PollControl.cluster_id, TemperatureMeasurement.cluster_id, IasZone.cluster_id, - SmartThingsAccelCluster.cluster_id, SmartThingsAccelCluster, ], - "output_clusters": [Identify.cluster_id, Ota.cluster_id], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], } } } diff --git a/zhaquirks/tuya/ts004f.py b/zhaquirks/tuya/ts004f.py index 39684f12e0..b3981aa5ac 100644 --- a/zhaquirks/tuya/ts004f.py +++ b/zhaquirks/tuya/ts004f.py @@ -78,6 +78,7 @@ class TuyaSmartRemote004FROK(EnchantedDevice): ("_TZ3000_ixla93vd", "TS004F"), ("_TZ3000_qja6nq5z", "TS004F"), ("_TZ3000_csflgqj2", "TS004F"), + ("_TZ3000_abrsvsou", "TS004F"), ], ENDPOINTS: { 1: { diff --git a/zhaquirks/tuya/ts0601_cover.py b/zhaquirks/tuya/ts0601_cover.py index cd012022c6..1976e46ea9 100644 --- a/zhaquirks/tuya/ts0601_cover.py +++ b/zhaquirks/tuya/ts0601_cover.py @@ -371,6 +371,7 @@ class TuyaMoesCover0601(TuyaWindowCover): ("_TZE200_nhyj64w2", "TS0601"), ("_TZE200_cf1sl3tj", "TS0601"), ("_TZE200_7eue9vhc", "TS0601"), + ("_TZE200_bv1jcqqu", "TS0601"), ], ENDPOINTS: { 1: { diff --git a/zhaquirks/tuya/ts0601_din_power.py b/zhaquirks/tuya/ts0601_din_power.py index c77b914797..6b6c919b33 100644 --- a/zhaquirks/tuya/ts0601_din_power.py +++ b/zhaquirks/tuya/ts0601_din_power.py @@ -40,7 +40,7 @@ class TuyaManufClusterDinPower(TuyaManufClusterAttributes): """Manufacturer Specific Cluster of the Tuya Power Meter device.""" attributes = { - TUYA_TOTAL_ENERGY_ATTR: ("energy", t.uint16_t, True), + TUYA_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True), TUYA_CURRENT_ATTR: ("current", t.int16s, True), TUYA_POWER_ATTR: ("power", t.uint16_t, True), TUYA_VOLTAGE_ATTR: ("voltage", t.uint16_t, True), diff --git a/zhaquirks/tuya/ts0601_motion.py b/zhaquirks/tuya/ts0601_motion.py index e1d680f718..4177396585 100644 --- a/zhaquirks/tuya/ts0601_motion.py +++ b/zhaquirks/tuya/ts0601_motion.py @@ -302,6 +302,7 @@ class NeoMotion(CustomDevice): # output_clusters=[10, 25]> MODELS_INFO: [ ("_TZE200_7hfcudw5", "TS0601"), + ("_TZE200_ppuj1vem", "TS0601"), ], ENDPOINTS: { 1: { diff --git a/zhaquirks/xiaomi/aqara/switch_h1.py b/zhaquirks/xiaomi/aqara/switch_h1.py new file mode 100644 index 0000000000..bb7184bd14 --- /dev/null +++ b/zhaquirks/xiaomi/aqara/switch_h1.py @@ -0,0 +1,226 @@ +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Alarms, + Basic, + GreenPowerProxy, + Groups, + Identify, + OnOff, + Ota, + Scenes, + Time, +) + +from zhaquirks.const import ( + ARGS, + ATTR_ID, + BUTTON, + CLUSTER_ID, + COMMAND, + COMMAND_DOUBLE, + COMMAND_HOLD, + COMMAND_SINGLE, + DEVICE_TYPE, + DOUBLE_PRESS, + ENDPOINT_ID, + ENDPOINTS, + INPUT_CLUSTERS, + LONG_PRESS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PRESS_TYPE, + PROFILE_ID, + SHORT_PRESS, + VALUE, +) +from zhaquirks.xiaomi import ( + LUMI, + BasicCluster, + DeviceTemperatureCluster, + OnOffCluster, + XiaomiMeteringCluster, +) +from zhaquirks.xiaomi.aqara.opple_remote import MultistateInputCluster +from zhaquirks.xiaomi.aqara.opple_switch import OppleSwitchCluster + +XIAOMI_COMMAND_SINGLE = "41_single" +XIAOMI_COMMAND_DOUBLE = "41_double" +XIAOMI_COMMAND_HOLD = "1_hold" + + +class AqaraH1SingleRockerBase(CustomDevice): + """Device automation triggers for the Aqara H1 Single Rocker Switches""" + + device_automation_triggers = { + (SHORT_PRESS, BUTTON): { + ENDPOINT_ID: 41, + CLUSTER_ID: 18, + COMMAND: XIAOMI_COMMAND_SINGLE, + ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_SINGLE, VALUE: 1}, + }, + (DOUBLE_PRESS, BUTTON): { + ENDPOINT_ID: 41, + CLUSTER_ID: 18, + COMMAND: XIAOMI_COMMAND_DOUBLE, + ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_DOUBLE, VALUE: 2}, + }, + (LONG_PRESS, BUTTON): { + ENDPOINT_ID: 1, + CLUSTER_ID: 64704, + COMMAND: XIAOMI_COMMAND_HOLD, + ARGS: {ATTR_ID: 0x00FC, PRESS_TYPE: COMMAND_HOLD, VALUE: 0}, + }, + } + + +class AqaraH1SingleRockerSwitchWithNeutral(AqaraH1SingleRockerBase): + """Aqara H1 Single Rocker Switch (with neutral).""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.switch.n1aeu1")], + ENDPOINTS: { + # input_clusters=[0, 2, 3, 4, 5, 6, 18, 64704], output_clusters=[10, 25] + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOff.cluster_id, # 6 + Alarms.cluster_id, # 9 + XiaomiMeteringCluster.cluster_id, # 0x0702 + 0x0B04, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, # 0x000a + Ota.cluster_id, # 0x0019 + ], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 0x0061, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, # 0x0021 + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOffCluster, # 6 + Alarms.cluster_id, # 9 + MultistateInputCluster, # 18 + XiaomiMeteringCluster.cluster_id, # 0x0702 + OppleSwitchCluster, # 0xFCC0 / 64704 + 0x0B04, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 41: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster, # 18 + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + +class AqaraH1SingleRockerSwitchNoNeutral(AqaraH1SingleRockerBase): + """Aqara H1 Single Rocker Switch (no neutral).""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.switch.l1aeu1")], + ENDPOINTS: { + # input_clusters=[0, 2, 3, 4, 5, 6, 9], output_clusters=[10, 25] + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOff.cluster_id, # 6 + Alarms.cluster_id, # 9 + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, # 0x000a + Ota.cluster_id, # 0x0019 + ], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 0x0061, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, # 0x0021 + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOffCluster, # 6 + Alarms.cluster_id, # 9 + MultistateInputCluster, # 18 + OppleSwitchCluster, # 0xFCC0 / 64704 + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 41: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster, # 18 + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } diff --git a/zhaquirks/xiaomi/aqara/tvoc.py b/zhaquirks/xiaomi/aqara/tvoc.py index 52609f67ce..8a4bb87aca 100644 --- a/zhaquirks/xiaomi/aqara/tvoc.py +++ b/zhaquirks/xiaomi/aqara/tvoc.py @@ -28,6 +28,7 @@ ) MEASURED_VALUE = 0x0000 +DISPLAY_UNIT = 0x0114 class AnalogInputCluster(CustomCluster, AnalogInput): @@ -66,6 +67,24 @@ async def bind(self): return result +class TVOCDisplayUnit(t.enum_factory(t.uint8_t)): + """Display values.""" + + mgm3_celsius = 0x00 + ppb_celsius = 0x01 + mgm3_fahrenheit = 0x10 + ppb_fahrenheit = 0x11 + + +class TVOCCluster(XiaomiAqaraE1Cluster): + """Aqara LUMI Config cluster.""" + + ep_attribute = "aqara_cluster" + attributes = { + DISPLAY_UNIT: ("display_unit", TVOCDisplayUnit, True), + } + + class TVOCMonitor(XiaomiCustomDevice): """Aqara LUMI lumi.airmonitor.acn01.""" @@ -114,7 +133,7 @@ def __init__(self, *args, **kwargs): RelativeHumidityCluster, AnalogInputCluster, EmulatedTVOCMeasurement, - XiaomiAqaraE1Cluster, + TVOCCluster, ], OUTPUT_CLUSTERS: [Ota.cluster_id], }