From 2b978b10d4b11040eed5c9a1299e914f32c1b3f5 Mon Sep 17 00:00:00 2001 From: badrpc Date: Wed, 19 Apr 2023 19:09:41 +0100 Subject: [PATCH 1/5] Add another version of Aqara roller shade driver E1 (#2341) Extends existing quirk. The only difference is DEVICE_TYPE in signature. --- zhaquirks/xiaomi/aqara/roller_curtain_e1.py | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/zhaquirks/xiaomi/aqara/roller_curtain_e1.py b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py index 4714286bc7..0adb57970b 100644 --- a/zhaquirks/xiaomi/aqara/roller_curtain_e1.py +++ b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py @@ -300,3 +300,49 @@ class RollerE1AQ_2(RollerE1AQ): }, }, } + + +class RollerE1AQ_3(RollerE1AQ): + """Aqara Roller Shade Driver E1 (version 3) device.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.curtain.acn002")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Alarms.cluster_id, + AnalogOutput.cluster_id, + Basic.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + MultistateOutput.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + WindowCovering.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + Time.cluster_id, + ], + }, + # + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 0x0061, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, + ], + }, + }, + } From 6b919ecf96c9327fc219da016c17e2f26b2e4b99 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 26 Apr 2023 07:13:19 -0400 Subject: [PATCH 2/5] Fix bug in Aqara pet feeder (#2343) --- zhaquirks/xiaomi/aqara/feeder_acn001.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/xiaomi/aqara/feeder_acn001.py b/zhaquirks/xiaomi/aqara/feeder_acn001.py index d6df9a0cda..49ed2b9ba3 100644 --- a/zhaquirks/xiaomi/aqara/feeder_acn001.py +++ b/zhaquirks/xiaomi/aqara/feeder_acn001.py @@ -161,7 +161,7 @@ def _parse_feeder_attribute(self, value: bytes) -> None: self._update_attribute( ZCL_LAST_FEEDING_SOURCE, OppleCluster.FeedingSource(feeding_source) ) - self._update_attribute(ZCL_LAST_FEEDING_SIZE, int(feeding_size)) + self._update_attribute(ZCL_LAST_FEEDING_SIZE, int(feeding_size, base=16)) elif attribute == PORTIONS_DISPENSED: portions_per_day, _ = types.uint16_t_be.deserialize(attribute_value) self._update_attribute(ZCL_PORTIONS_DISPENSED, portions_per_day) From c3f1fcc0544258f9d1ab54b8ed3d13c9278360c0 Mon Sep 17 00:00:00 2001 From: LuRy Date: Wed, 26 Apr 2023 17:28:02 +0200 Subject: [PATCH 3/5] Add Tuya TS011F `_TZ3000_jak16dll` plug variant (#2348) * Add support for _TZ3000_jak16dll NEO LITE Smart two socket https://zigbee.blakadder.com/Immax_07752L.html --- zhaquirks/tuya/ts011f_plug.py | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/zhaquirks/tuya/ts011f_plug.py b/zhaquirks/tuya/ts011f_plug.py index 8a47ed55ae..ac4558df0d 100644 --- a/zhaquirks/tuya/ts011f_plug.py +++ b/zhaquirks/tuya/ts011f_plug.py @@ -1210,3 +1210,95 @@ class Plug_CB_Metering(EnchantedDevice): }, }, } + + +class Plug_2AC_var05(EnchantedDevice): + """Immax TS0011F 2 outlet plug.""" + + signature = { + MODEL: "TS011F", + ENDPOINTS: { + 1: { + # "profile_id": 260, + # "device_type": "0x010a", + # "in_clusters": ["0x0000","0x0003","0x0004","0x0005","0x0006","0x0702","0x0b04","0xe000","0xe001"], + # "out_clusters": ["0x000a","0x0019"] + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + # "profile_id": 260, + # "device_type": "0x010a", + # "in_clusters": ["0x0004","0x0005","0x0006","0xe001"], + # "out_clusters": [] + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + # "profile_id": 41440, + # "device_type": "0x0061", + # "in_clusters": [], + # "out_clusters": ["0x0021"] + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBMeteringCluster, + TuyaZBElectricalMeasurement, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } From 08c043ef7109e6024e79578e5eff7fe57f76def8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 27 Apr 2023 14:58:07 -0400 Subject: [PATCH 4/5] Ensure quirk clusters are proper subclasses (#2358) * Ensure quirks unit tests pass with zigpy `_dblistener` changes * Ensure all custom clusters are proper subclasses of their parent class * Fix Philips motion * Fix Konke --- tests/conftest.py | 5 +++- tests/test_konke.py | 2 +- tests/test_quirks.py | 17 ++++++++++-- zhaquirks/konke/__init__.py | 49 +++++++++++++++++------------------ zhaquirks/philips/__init__.py | 8 +++++- zhaquirks/philips/motion.py | 6 ++--- 6 files changed, 54 insertions(+), 33 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2d6f8879db..d10497827a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ """Fixtures for all tests.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, Mock import pytest import zigpy.application @@ -191,6 +191,9 @@ def __getattr__(self, key): return self.endpoints.get(key) test_dev = FakeDevice(signature) + test_dev._application = Mock() + test_dev._application._dblistener = None + device = zigpy.quirks.get_device(test_dev) assert isinstance(device, quirk) diff --git a/tests/test_konke.py b/tests/test_konke.py index 2df6f74c10..8ef1ddff58 100644 --- a/tests/test_konke.py +++ b/tests/test_konke.py @@ -77,7 +77,7 @@ async def test_konke_button(zigpy_device_from_quirk, quirk): """Test Konke button remotes.""" device = zigpy_device_from_quirk(quirk) - cluster = device.endpoints[1].custom_on_off + cluster = device.endpoints[1].konke_on_off listener = mock.MagicMock() cluster.add_listener(listener) diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 92f858b86a..a0f477a156 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -700,8 +700,21 @@ def test_attributes_updated_not_replaced(quirk: CustomDevice) -> None: base_cluster = list(base_clusters)[0] # Ensure the attribute IDs are extended - if not set(base_cluster.attributes) <= set(cluster.attributes): + base_attr_ids = set(base_cluster.attributes) + quirk_attr_ids = set(cluster.attributes) + + if not base_attr_ids <= quirk_attr_ids: + pytest.fail( + f"Cluster {cluster} deletes parent class's attributes instead of" + f" extending them: {base_attr_ids - quirk_attr_ids}" + ) + + # Ensure the attribute names are extended + base_attr_names = {a.name for a in base_cluster.attributes.values()} + quirk_attr_names = {a.name for a in cluster.attributes.values()} + + if not base_attr_names <= quirk_attr_names: pytest.fail( f"Cluster {cluster} deletes parent class's attributes instead of" - f" extending them: {base_cluster}" + f" extending them: {base_attr_names - quirk_attr_names}" ) diff --git a/zhaquirks/konke/__init__.py b/zhaquirks/konke/__init__.py index 64f4e6947e..c9cbe726c6 100644 --- a/zhaquirks/konke/__init__.py +++ b/zhaquirks/konke/__init__.py @@ -18,6 +18,19 @@ KONKE = "Konke" +class KonkeButtonEvent(t.enum8): + Single = 0x80 + Double = 0x81 + Hold = 0x82 + + +PRESS_TYPES = { + KonkeButtonEvent.Single: COMMAND_SINGLE, + KonkeButtonEvent.Double: COMMAND_DOUBLE, + KonkeButtonEvent.Hold: COMMAND_HOLD, +} + + class OccupancyCluster(LocalDataCluster, OccupancyOnEvent): """Occupancy cluster.""" @@ -31,14 +44,17 @@ class MotionCluster(MotionWithReset): send_occupancy_event: bool = True -class KonkeOnOffCluster(CustomCluster, OnOff): +class KonkeOnOffCluster(CustomCluster): """Konke OnOff cluster implementation.""" - PRESS_TYPES = {0x80: COMMAND_SINGLE, 0x81: COMMAND_DOUBLE, 0x82: COMMAND_HOLD} - ep_attribute = "custom_on_off" + cluster_id = OnOff.cluster_id + ep_attribute = "konke_on_off" attributes = OnOff.attributes.copy() - attributes[0x0000] = (PRESS_TYPE, t.uint8_t) + attributes[0x0000] = ("konke_button_event", KonkeButtonEvent) + + server_commands = OnOff.server_commands.copy() + client_commands = OnOff.client_commands.copy() def handle_cluster_general_request( self, @@ -60,30 +76,13 @@ def handle_cluster_general_request( return attr = args[0][0] - if attr.attrid != 0x0000: + + if attr.attrid != self.attributes_by_name["konke_button_event"].id: return value = attr.value.value event_args = { - PRESS_TYPE: self.PRESS_TYPES.get(value, value), - COMMAND_ID: value, + PRESS_TYPE: PRESS_TYPES[value], + COMMAND_ID: value.value, # to maintain backwards compatibility } self.listener_event(ZHA_SEND_EVENT, event_args[PRESS_TYPE], event_args) - - def deserialize(self, data): - """Deserialize fix for Konke butchered Bool ZCL type.""" - try: - return super().deserialize(data) - except ValueError: - hdr, data = zigpy.zcl.foundation.ZCLHeader.deserialize(data) - if ( - hdr.frame_control.is_cluster - or hdr.command_id - != zigpy.zcl.foundation.GeneralCommand.Report_Attributes - ): - raise - attr_id, data = t.uint16_t.deserialize(data) - attr = zigpy.zcl.foundation.Attribute( - attr_id, zigpy.zcl.foundation.TypeValue(t.uint8_t, data[1]) - ) - return hdr, [[attr]] diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index de58050450..bc78a0a5be 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -71,13 +71,19 @@ } -class OccupancyCluster(CustomCluster, OccupancySensing): +class PhilipsOccupancySensing(CustomCluster): """Philips occupancy cluster.""" + cluster_id = OccupancySensing.cluster_id + ep_attribute = "philips_occupancy" + attributes = OccupancySensing.attributes.copy() attributes[0x0030] = ("sensitivity", t.uint8_t, True) attributes[0x0031] = ("sensitivity_max", t.uint8_t, True) + server_commands = OccupancySensing.server_commands.copy() + client_commands = OccupancySensing.client_commands.copy() + class PhilipsBasicCluster(CustomCluster, Basic): """Philips Basic cluster.""" diff --git a/zhaquirks/philips/motion.py b/zhaquirks/philips/motion.py index 18bc2aea33..0b265cbb44 100644 --- a/zhaquirks/philips/motion.py +++ b/zhaquirks/philips/motion.py @@ -27,7 +27,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.philips import PHILIPS, SIGNIFY, OccupancyCluster +from zhaquirks.philips import PHILIPS, SIGNIFY, PhilipsOccupancySensing class BasicCluster(CustomCluster, Basic): @@ -106,7 +106,7 @@ class PhilipsMotion(CustomDevice): Identify.cluster_id, IlluminanceMeasurement.cluster_id, TemperatureMeasurement.cluster_id, - OccupancyCluster, + PhilipsOccupancySensing, ], OUTPUT_CLUSTERS: [Ota.cluster_id], }, @@ -152,7 +152,7 @@ class SignifyMotion(CustomDevice): Identify.cluster_id, IlluminanceMeasurement.cluster_id, TemperatureMeasurement.cluster_id, - OccupancyCluster, + PhilipsOccupancySensing, ], OUTPUT_CLUSTERS: [ Basic.cluster_id, From 0f4cab499b593fa9e41534c40709fc548e642992 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Thu, 27 Apr 2023 14:59:11 -0400 Subject: [PATCH 5/5] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 26ba74d143..bee6e39d4a 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.97" +VERSION = "0.0.98" setup(