From 21dd8eb2a59115503f6dcbb660dd9a7f2613f17d Mon Sep 17 00:00:00 2001 From: Oleksandr Popovych Date: Sat, 25 May 2024 01:45:01 +0300 Subject: [PATCH 01/16] Remove manufacturer matching for Tuya plug TS011F variant (#3164) --- zhaquirks/tuya/ts011f_plug.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhaquirks/tuya/ts011f_plug.py b/zhaquirks/tuya/ts011f_plug.py index a0734de5b4..3d80c13dee 100644 --- a/zhaquirks/tuya/ts011f_plug.py +++ b/zhaquirks/tuya/ts011f_plug.py @@ -1249,7 +1249,6 @@ class Plug_CB_Metering(EnchantedDevice): signature = { MODEL: "TS011F", - MODELS_INFO: [("_TZ3000_qeuvnohg", "TS011F")], ENDPOINTS: { # Date: Tue, 28 May 2024 02:02:53 +0200 Subject: [PATCH 02/16] Add Tuya soil sensor variant `_TZE204_myd45weu` (#3173) --- zhaquirks/tuya/ts0601_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py index f76648ab51..4cc711dab8 100644 --- a/zhaquirks/tuya/ts0601_sensor.py +++ b/zhaquirks/tuya/ts0601_sensor.py @@ -372,6 +372,7 @@ class TuyaSoilSensor(CustomDevice): ("_TZE200_myd45weu", "TS0601"), ("_TZE200_ga1maeof", "TS0601"), ("_TZE200_9cqcpkgb", "TS0601"), + ("_TZE204_myd45weu", "TS0601"), ], ENDPOINTS: { 1: { From 6f5a939e67facc11e2e1d0d9411ad43ee56e1582 Mon Sep 17 00:00:00 2001 From: Steven Haigh Date: Tue, 28 May 2024 10:05:25 +1000 Subject: [PATCH 03/16] Add Tuya single button switch `TS004F` `_TZ3000_ja5osu5g` (#3153) --- zhaquirks/tuya/ts004f.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/zhaquirks/tuya/ts004f.py b/zhaquirks/tuya/ts004f.py index c19892e0f4..2eb5cf1904 100644 --- a/zhaquirks/tuya/ts004f.py +++ b/zhaquirks/tuya/ts004f.py @@ -401,6 +401,40 @@ class TuyaSmartRemote004FSK(EnchantedDevice): (DOUBLE_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: DOUBLE_PRESS}, } +class TuyaSmartRemote004FSK_v2(TuyaSmartRemote004FSK): + """Tuya Smart (Single) Knob device.""" + + signature = { + # "node_descriptor": "NodeDescriptor(byte1=2, byte2=64, mac_capability_flags=128, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=0, *allocate_address=True, *complex_descriptor_available=False, *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, *is_valid=True, *logical_type=, *user_descriptor_available=False)", + # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=260, device_version=1, input_clusters=[0, 1, 3, 4, 6, 4096, 57345], output_clusters=[25, 10, 3, 4, 6, 8, 4096]) + MODELS_INFO: [ + ("_TZ3000_ja5osu5g", "TS004F"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LightLink.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + Time.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + }, + }, + } class TuyaSmartRemote004F(EnchantedDevice): """Tuya 4-button New version remote device.""" From 5434dfbc3f58ac634310848a4a7818d7bec2aefa Mon Sep 17 00:00:00 2001 From: Adrian Freund Date: Tue, 28 May 2024 02:10:41 +0200 Subject: [PATCH 04/16] Convert IKEA Starkvind fan speed attribute (#3088) --- tests/test_ikea.py | 28 ++++++++++++++++++++++++++++ zhaquirks/ikea/starkvind.py | 6 +++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/test_ikea.py b/tests/test_ikea.py index fa4470090e..6d57427158 100644 --- a/tests/test_ikea.py +++ b/tests/test_ikea.py @@ -10,6 +10,7 @@ from tests.common import ClusterListener import zhaquirks import zhaquirks.ikea.starkvind +from zhaquirks.ikea.starkvind import IkeaAirpurifier zhaquirks.setup() @@ -85,6 +86,33 @@ def test_ikea_starkvind_v2(assert_signature_matches_quirk): assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND_v2, signature) +@pytest.mark.parametrize("attribute", ["fan_speed", "fan_mode"]) +@pytest.mark.parametrize("value,expected", [ + (0, 0), # off + (1, 1), # auto + (10, 2), + (20, 4), + (50, 10), + ] + ) +async def test_fan_speed_mode_update(zigpy_device_from_quirk, attribute, value, expected): + """Test reading the fan speed and mode.""" + + starkvind_device = zigpy_device_from_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND) + assert starkvind_device.model == "STARKVIND Air purifier" + + ikea_cluster = starkvind_device.endpoints[1].in_clusters[ + zhaquirks.ikea.starkvind.IkeaAirpurifier.cluster_id + ] + ikea_listener = ClusterListener(ikea_cluster) + + attr_id = getattr(IkeaAirpurifier.AttributeDefs, attribute).id + + ikea_cluster.update_attribute(attr_id, value) + assert len(ikea_listener.attribute_updates) == 1 + assert ikea_listener.attribute_updates[0] == (attr_id, expected) + + async def test_pm25_cluster_read(zigpy_device_from_quirk): """Test reading from PM25 cluster.""" diff --git a/zhaquirks/ikea/starkvind.py b/zhaquirks/ikea/starkvind.py index 95b228db9d..11a1d1f631 100644 --- a/zhaquirks/ikea/starkvind.py +++ b/zhaquirks/ikea/starkvind.py @@ -68,9 +68,9 @@ def _update_attribute(self, attrid, value): value is not None and value < 5500 ): # > 5500 = out of scale; if value is 65535 (0xFFFF), device is off self.endpoint.device.pm25_bus.listener_event("update_state", value) - elif attrid == 0x0006: - if value > 9 and value < 51: - value = value / 5 + elif attrid in (0x0006, 0x0007): + if value >= 10 and value <= 50: + value = value // 5 super()._update_attribute(attrid, value) async def write_attributes( From 4383f00186fdccfdf93e4dc87b38756ad476f0e8 Mon Sep 17 00:00:00 2001 From: Anton Skorochod Date: Tue, 28 May 2024 02:13:57 +0200 Subject: [PATCH 05/16] Add Tuya blinds variant `_TZE200_2odrmqwq` (#3131) --- zhaquirks/tuya/ts0601_cover.py | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/zhaquirks/tuya/ts0601_cover.py b/zhaquirks/tuya/ts0601_cover.py index a95b49030e..57b981e1de 100644 --- a/zhaquirks/tuya/ts0601_cover.py +++ b/zhaquirks/tuya/ts0601_cover.py @@ -460,6 +460,61 @@ class TuyaMoesCover0601_alt_controls(TuyaWindowCover): } } +class TuyaMoesCover0601_alt_controls2(TuyaWindowCover): + """Tuya blind controller device.""" + + tuya_cover_command = {0x0000: 0x0000, 0x0001: 0x0002, 0x0002: 0x0001} + tuya_cover_inverted_by_default = True + + 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=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, + # maximum_outgoing_transfer_size=82, 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": "0x0051", "in_clusters": [ "0x0000", "0x0004","0x0005","0x0102","0xef00"], "out_clusters": ["0x000a","0x0019"] } + # }, + # "manufacturer": "_TZE200_2odrmqwq", + # "model": "TS0601", + # "class": "zigpy.device.Device" + # } + MODELS_INFO: [ + ("_TZE200_2odrmqwq", "TS0601"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufacturerWindowCover, + TuyaWindowCoverControl, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + } + } + class TuyaMoesCover0601_inv_position(TuyaWindowCover): """Tuya blind controller device.""" From 5ce06cf734a6c57829b2decf3089be7e7d4f00f0 Mon Sep 17 00:00:00 2001 From: Jiaxin Date: Tue, 28 May 2024 08:52:45 +0800 Subject: [PATCH 06/16] Add Aqara E1 motion sensor support (#3163) * Add Aqara E1 motion sensor quirk * Extract `XiaomiMotionManufacturerCluster` out of motion quirks * Fix illuminance Aqara attribute reports not being parsed * Add test for E1 sensor --------- Co-authored-by: TheJulianJES --- tests/test_xiaomi.py | 7 +-- zhaquirks/xiaomi/__init__.py | 19 ++++++- zhaquirks/xiaomi/aqara/motion_ac02.py | 23 +++------ zhaquirks/xiaomi/aqara/motion_acn001.py | 68 +++++++++++++++++++++++++ zhaquirks/xiaomi/aqara/motion_agl02.py | 25 ++------- 5 files changed, 101 insertions(+), 41 deletions(-) create mode 100644 zhaquirks/xiaomi/aqara/motion_acn001.py diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 3836c79c47..f534e0932a 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -1135,12 +1135,13 @@ async def test_xiaomi_e1_thermostat_schedule_settings_deserialization( ( (zhaquirks.xiaomi.aqara.motion_ac02.LumiMotionAC02, 0), (zhaquirks.xiaomi.aqara.motion_agl02.MotionT1, -1), + (zhaquirks.xiaomi.aqara.motion_acn001.MotionE1, -1), ), ) async def test_xiaomi_p1_t1_motion_sensor( zigpy_device_from_quirk, quirk, invalid_iilluminance_report ): - """Test Aqara P1 and T1 motion sensors.""" + """Test Aqara P1, T1, and E1 motion sensors.""" device = zigpy_device_from_quirk(quirk) @@ -1192,12 +1193,12 @@ async def test_xiaomi_p1_t1_motion_sensor( opple_cluster.update_attribute(274, 0xFFFF) # confirm invalid illuminance report is interpreted as 0 for P1 sensor, - # and -1 for the T1 sensor, as it doesn't seem like the T1 sensor sends invalid illuminance reports + # and -1 for the T1/E1 sensors, as they don't seem to send invalid illuminance reports assert len(illuminance_listener.attribute_updates) == 2 assert illuminance_listener.attribute_updates[1][0] == zcl_iilluminance_id assert illuminance_listener.attribute_updates[1][1] == invalid_iilluminance_report - # send illuminance report only + # send illuminance report only, parsed via Xiaomi cluster implementation opple_cluster.update_attribute( XIAOMI_AQARA_ATTRIBUTE_E1, create_aqara_attr_report({101: 20}) ) diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index 91607cfa7b..251d13742b 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -24,6 +24,7 @@ from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, + OccupancySensing, PressureMeasurement, RelativeHumidity, TemperatureMeasurement, @@ -389,7 +390,7 @@ def _parse_aqara_attributes(self, value): attribute_names.update({11: ILLUMINANCE_MEASUREMENT}) elif self.endpoint.device.model == "lumi.curtain.acn002": attribute_names.update({101: BATTERY_PERCENTAGE_REMAINING_ATTRIBUTE}) - elif self.endpoint.device.model in ["lumi.motion.agl02", "lumi.motion.ac02"]: + elif self.endpoint.device.model in ["lumi.motion.agl02", "lumi.motion.ac02", "lumi.motion.acn001"]: attribute_names.update({101: ILLUMINANCE_MEASUREMENT}) if self.endpoint.device.model == "lumi.motion.ac02": attribute_names.update({105: DETECTION_INTERVAL}) @@ -464,6 +465,22 @@ class XiaomiAqaraE1Cluster(XiaomiCluster): ep_attribute = "opple_cluster" +class XiaomiMotionManufacturerCluster(XiaomiAqaraE1Cluster): + """Xiaomi manufacturer cluster to parse motion and illuminance reports.""" + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == 274: + value = value - 65536 + self.endpoint.illuminance.update_attribute( + IlluminanceMeasurement.AttributeDefs.measured_value.id, value + ) + self.endpoint.occupancy.update_attribute( + OccupancySensing.AttributeDefs.occupancy.id, + OccupancySensing.Occupancy.Occupied, + ) + + class BinaryOutputInterlock(CustomCluster, BinaryOutput): """Xiaomi binaryoutput cluster with added interlock attribute.""" diff --git a/zhaquirks/xiaomi/aqara/motion_ac02.py b/zhaquirks/xiaomi/aqara/motion_ac02.py index efb52175b6..c372691828 100644 --- a/zhaquirks/xiaomi/aqara/motion_ac02.py +++ b/zhaquirks/xiaomi/aqara/motion_ac02.py @@ -8,7 +8,6 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing from zhaquirks import Bus, LocalDataCluster from zhaquirks.const import ( @@ -23,7 +22,7 @@ LocalIlluminanceMeasurementCluster, MotionCluster, OccupancyCluster, - XiaomiAqaraE1Cluster, + XiaomiMotionManufacturerCluster, XiaomiPowerConfiguration, ) @@ -34,8 +33,12 @@ _LOGGER = logging.getLogger(__name__) -class OppleCluster(XiaomiAqaraE1Cluster): - """Opple cluster.""" +class OppleCluster(XiaomiMotionManufacturerCluster): + """Xiaomi manufacturer cluster. + + This uses the shared XiaomiMotionManufacturerCluster implementation + which parses motion and illuminance reports from Xiaomi devices. + """ attributes = { DETECTION_INTERVAL: ("detection_interval", types.uint8_t, True), @@ -43,18 +46,6 @@ class OppleCluster(XiaomiAqaraE1Cluster): TRIGGER_INDICATOR: ("trigger_indicator", types.uint8_t, True), } - def _update_attribute(self, attrid: int, value: Any) -> None: - super()._update_attribute(attrid, value) - if attrid == MOTION_ATTRIBUTE: - value = value - 65536 - self.endpoint.illuminance.update_attribute( - IlluminanceMeasurement.AttributeDefs.measured_value.id, value - ) - self.endpoint.occupancy.update_attribute( - OccupancySensing.AttributeDefs.occupancy.id, - OccupancySensing.Occupancy.Occupied, - ) - async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: diff --git a/zhaquirks/xiaomi/aqara/motion_acn001.py b/zhaquirks/xiaomi/aqara/motion_acn001.py new file mode 100644 index 0000000000..168372aa01 --- /dev/null +++ b/zhaquirks/xiaomi/aqara/motion_acn001.py @@ -0,0 +1,68 @@ +"""Xiaomi Aqara E1 motion sensor device.""" +from zigpy.profiles import zha +from zigpy.zcl.clusters.general import Identify, Ota + +from zhaquirks import Bus +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.xiaomi import ( + LUMI, + BasicCluster, + IlluminanceMeasurementCluster, + LocalOccupancyCluster, + MotionCluster, + XiaomiAqaraE1Cluster, + XiaomiCustomDevice, + XiaomiMotionManufacturerCluster, + XiaomiPowerConfiguration, +) + + +class MotionE1(XiaomiCustomDevice): + """Xiaomi motion sensor device lumi.motion.acn001.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.battery_size = 11 + self.motion_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [(LUMI, "lumi.motion.acn001")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + BasicCluster.cluster_id, + XiaomiPowerConfiguration.cluster_id, + Identify.cluster_id, + XiaomiAqaraE1Cluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + BasicCluster, + XiaomiPowerConfiguration, + Identify.cluster_id, + LocalOccupancyCluster, + MotionCluster, + IlluminanceMeasurementCluster, + XiaomiMotionManufacturerCluster, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } diff --git a/zhaquirks/xiaomi/aqara/motion_agl02.py b/zhaquirks/xiaomi/aqara/motion_agl02.py index e8c0fd1ac0..30223320e5 100644 --- a/zhaquirks/xiaomi/aqara/motion_agl02.py +++ b/zhaquirks/xiaomi/aqara/motion_agl02.py @@ -3,7 +3,7 @@ from zigpy.profiles import zha from zigpy.zcl.clusters.general import Identify, Ota -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing +from zigpy.zcl.clusters.measurement import OccupancySensing from zhaquirks import Bus from zhaquirks.const import ( @@ -20,29 +20,11 @@ IlluminanceMeasurementCluster, LocalOccupancyCluster, MotionCluster, - XiaomiAqaraE1Cluster, XiaomiCustomDevice, + XiaomiMotionManufacturerCluster, XiaomiPowerConfiguration, ) -XIAOMI_CLUSTER_ID = 0xFCC0 - - -class XiaomiManufacturerCluster(XiaomiAqaraE1Cluster): - """Xiaomi manufacturer cluster.""" - - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if attrid == 274: - value = value - 65536 - self.endpoint.illuminance.update_attribute( - IlluminanceMeasurement.AttributeDefs.measured_value.id, value - ) - self.endpoint.occupancy.update_attribute( - OccupancySensing.AttributeDefs.occupancy.id, - OccupancySensing.Occupancy.Occupied, - ) - class MotionT1(XiaomiCustomDevice): """Xiaomi motion sensor device.""" @@ -73,6 +55,7 @@ def __init__(self, *args, **kwargs): } }, } + replacement = { ENDPOINTS: { 1: { @@ -83,7 +66,7 @@ def __init__(self, *args, **kwargs): LocalOccupancyCluster, MotionCluster, IlluminanceMeasurementCluster, - XiaomiManufacturerCluster, + XiaomiMotionManufacturerCluster, ], OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], } From 151970c4b45f9588e811a87a4928cb8aaf5ae601 Mon Sep 17 00:00:00 2001 From: Ely Date: Tue, 28 May 2024 03:03:17 +0200 Subject: [PATCH 07/16] Adds BSEED/Tuya dimmer switch variant `_TZE204_vevc4c6g` (#3130) Co-authored-by: TheJulianJES --- zhaquirks/tuya/ts0601_dimmer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 7359a065d1..7b2d665a01 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -151,7 +151,8 @@ class TuyaSingleSwitchDimmerGP(TuyaDimmerSwitch): ("_TZE200_ip2akl4w", "TS0601"), ("_TZE200_vucankjx", "TS0601"), # Loratap ("_TZE200_y8yjulon", "TS0601"), - ("_TZE204_n9ctkb6j", "TS0601"), # For BSEED switch + ("_TZE204_n9ctkb6j", "TS0601"), # BSEED + ("_TZE204_vevc4c6g", "TS0601"), # BSEED ], ENDPOINTS: { # Date: Wed, 29 May 2024 16:38:38 +0200 Subject: [PATCH 08/16] Remove `LevelControl` cluster for IKEA plugs (#3174) * Add IKEA plug quirk v2 to remove `LevelControl` This removes the `LevelControl` cluster for IKEA TRADFRI and TRETAKT smart plugs, so the config options aren't shown in ZHA. * Remove old tradfriplug quirk This is no longer needed, as the v2 quirk also matches all of these devices. --- zhaquirks/ikea/plug.py | 12 +++ zhaquirks/ikea/tradfriplug.py | 166 ---------------------------------- 2 files changed, 12 insertions(+), 166 deletions(-) create mode 100644 zhaquirks/ikea/plug.py delete mode 100644 zhaquirks/ikea/tradfriplug.py diff --git a/zhaquirks/ikea/plug.py b/zhaquirks/ikea/plug.py new file mode 100644 index 0000000000..cd3cd9dcb4 --- /dev/null +++ b/zhaquirks/ikea/plug.py @@ -0,0 +1,12 @@ +"""IKEA plugs quirk.""" +from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.zcl.clusters.general import LevelControl + +from zhaquirks.ikea import IKEA + +# remove LevelControl for plugs to not show config options in ZHA +( + add_to_registry_v2(IKEA, "TRADFRI control outlet") + .also_applies_to(IKEA, "TRETAKT Smart plug") + .removes(LevelControl.cluster_id) +) diff --git a/zhaquirks/ikea/tradfriplug.py b/zhaquirks/ikea/tradfriplug.py deleted file mode 100644 index 963bbff51a..0000000000 --- a/zhaquirks/ikea/tradfriplug.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Tradfri Plug Quirk.""" -from zigpy.profiles import zgp, zha, zll -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - GreenPowerProxy, - Groups, - Identify, - LevelControl, - OnOff, - Ota, - PollControl, - Scenes, -) -from zigpy.zcl.clusters.lightlink import LightLink - -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID - - -class TradfriPlug1(CustomDevice): - """Tradfri Plug.""" - - signature = { - MODELS_INFO: [(IKEA, "TRADFRI control outlet")], - 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, - OnOff.cluster_id, - LevelControl.cluster_id, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - ], - }, - # - 2: { - PROFILE_ID: zll.PROFILE_ID, - DEVICE_TYPE: zll.DeviceType.ON_OFF_PLUGIN_UNIT, - INPUT_CLUSTERS: [LightLink.cluster_id], - OUTPUT_CLUSTERS: [LightLink.cluster_id], - }, - # - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - 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, - OnOff.cluster_id, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - ], - } - } - } - - -class TradfriPlug2(CustomDevice): - """Tradfri Plug.""" - - signature = { - MODELS_INFO: [(IKEA, "TRADFRI control outlet")], - 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, - OnOff.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - ], - }, - # - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - 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, - OnOff.cluster_id, - LightLink.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - ], - }, - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - }, - } From 31c51cecfec8e47b6b06807d157d595ff4cf1bb1 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 29 May 2024 18:02:48 +0200 Subject: [PATCH 09/16] Do not double IKEA battery percentage by default (#3176) --- tests/test_ikea.py | 20 ++++++++++---------- zhaquirks/ikea/__init__.py | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/test_ikea.py b/tests/test_ikea.py index 6d57427158..54a393c8f5 100644 --- a/tests/test_ikea.py +++ b/tests/test_ikea.py @@ -159,14 +159,14 @@ def mock_read(attributes, manufacturer=None): @pytest.mark.parametrize( "firmware, pct_device, pct_correct, expected_pct_updates, expect_log_warning", ( - ("1.0.024", 50, 100, 1, False), # old firmware, doubling - ("2.3.075", 50, 100, 1, False), # old firmware, doubling - ("2.4.5", 50, 50, 2, False), # new firmware, no doubling - ("3.0.0", 50, 50, 2, False), # new firmware, no doubling - ("24.4.5", 50, 50, 2, False), # new firmware, no doubling - ("invalid_fw_string_1", 50, 50, 2, False), # treated as new, no doubling - ("invalid.fw.string.2", 50, 50, 2, True), # treated as new, no doubling + log - ("", 50, 100, 1, False), # treated as old fw, doubling + ("1.0.024", 50, 100, 2, False), # old firmware, doubling + ("2.3.075", 50, 100, 2, False), # old firmware, doubling + ("2.4.5", 50, 50, 1, False), # new firmware, no doubling + ("3.0.0", 50, 50, 1, False), # new firmware, no doubling + ("24.4.5", 50, 50, 1, False), # new firmware, no doubling + ("invalid_fw_string_1", 50, 50, 1, False), # treated as new, no doubling + ("invalid.fw.string.2", 50, 50, 1, True), # treated as new, no doubling + log + ("", 50, 50, 1, False), # treated as new fw, no doubling ), ) async def test_double_power_config_firmware( @@ -206,10 +206,10 @@ def mock_read(attributes, manufacturer=None): ) with p1 as mock_task, p2 as request_mock: - # update battery percentage with no firmware in attr cache, check pct doubled for now + # update battery percentage with no firmware in attr cache, check pct not doubled for now power_cluster.update_attribute(battery_pct_id, pct_device) assert len(power_listener.attribute_updates) == 1 - assert power_listener.attribute_updates[0] == (battery_pct_id, pct_device * 2) + assert power_listener.attribute_updates[0] == (battery_pct_id, pct_device) # but also check that sw_build_id read is requested in the background for next update assert mock_task.call_count == 1 diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py index 65e2151da9..d584269ccf 100644 --- a/zhaquirks/ikea/__init__.py +++ b/zhaquirks/ikea/__init__.py @@ -215,9 +215,9 @@ def _is_firmware_new(self): # get sw_build_id from attribute cache if available sw_build_id = self.endpoint.basic.get(Basic.AttributeDefs.sw_build_id.id) - # sw_build_id is not cached or empty, so we consider it old firmware for now + # sw_build_id is not cached or empty, so we consider it new firmware for now if not sw_build_id: - return False + return True # split sw_build_id into parts to check for new firmware split_fw_version = sw_build_id.split(".") @@ -247,19 +247,19 @@ async def _read_fw_and_update_battery_pct(self, reported_battery_pct): # read sw_build_id from device await self.endpoint.basic.read_attributes([Basic.AttributeDefs.sw_build_id.id]) - # check if sw_build_id was read successfully and new firmware is installed - # if so, update cache with reported battery percentage (non-doubled) - if self._is_firmware_new(): + # check if sw_build_id was read successfully and old firmware is installed + # if so, update cache with reported battery percentage (doubled) + if not self._is_firmware_new(): self._update_attribute( PowerConfiguration.AttributeDefs.battery_percentage_remaining.id, - reported_battery_pct, + reported_battery_pct * 2, ) def _update_attribute(self, attrid, value): - """Update attribute to double battery percentage if firmware is old/unknown. + """Update attribute to double battery percentage if firmware is old. If the firmware version is unknown, a background task to read the firmware version is also started, - but the percentage is also doubled for now then, as that task happens asynchronously. + but the percentage is not doubled for now then, as that task happens asynchronously. """ if attrid == PowerConfiguration.AttributeDefs.battery_percentage_remaining.id: # if sw_build_id is not cached, create task to read from device, since it should be awake now @@ -269,9 +269,9 @@ def _update_attribute(self, attrid, value): ): self.create_catching_task(self._read_fw_and_update_battery_pct(value)) - # double percentage if the firmware is old or unknown - # the coroutine above will not have executed yet if the firmware is unknown, - # so we double for now in that case too, and it updates again later if our doubling was wrong + # double percentage if the firmware is confirmed old + # The coroutine above will not have executed yet if the firmware is unknown, + # so we don't double for now. The coro doubles the value later if needed. if not self._is_firmware_new(): value = value * 2 super()._update_attribute(attrid, value) From b2751a2a37bd0ea8b8790bb664c6e37f38bcbb14 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 29 May 2024 18:28:03 +0200 Subject: [PATCH 10/16] Convert IKEA Styrbar quirks to v2 quirk (#3175) * Convert IKEA Styrbar quirks to v2 quirk * Remove `fourbtnremote` from `test_quirk_device_automation_triggers_unique` The test does not handle v2 quirks yet and we'll need to see how we want to implement this, as all v2 quirks share the same quirk class. For now, this just removes the `fourbtnremote` from the test. --- tests/test_quirks.py | 7 +- zhaquirks/ikea/fourbtnremote.py | 282 +++++++++----------------------- 2 files changed, 78 insertions(+), 211 deletions(-) diff --git a/tests/test_quirks.py b/tests/test_quirks.py index f66e47089c..09ab4f5c68 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -641,12 +641,6 @@ def test_migrated_lighting_automation_triggers(quirk: CustomDevice) -> None: (zhaquirks.aurora.aurora_dimmer.COLOR_DOWN, const.LEFT), ], ], - zhaquirks.ikea.fourbtnremote.IkeaTradfriRemoteV1: [ - [ - (const.LONG_RELEASE, const.DIM_UP), - (const.LONG_RELEASE, const.DIM_DOWN), - ] - ], zhaquirks.paulmann.fourbtnremote.PaulmannRemote4Btn: [ [ (const.LONG_RELEASE, const.BUTTON_1), @@ -666,6 +660,7 @@ def test_migrated_lighting_automation_triggers(quirk: CustomDevice) -> None: } +# XXX: Test does not handle v2 quirks @pytest.mark.parametrize( "quirk", [q for q in ALL_QUIRK_CLASSES if getattr(q, "device_automation_triggers", None)], diff --git a/zhaquirks/ikea/fourbtnremote.py b/zhaquirks/ikea/fourbtnremote.py index ee37f4b9b9..afe88f8012 100644 --- a/zhaquirks/ikea/fourbtnremote.py +++ b/zhaquirks/ikea/fourbtnremote.py @@ -1,16 +1,6 @@ """Device handler for IKEA of Sweden TRADFRI remote control.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - Identify, - LevelControl, - OnOff, - Ota, - PollControl, - PowerConfiguration, -) -from zigpy.zcl.clusters.lightlink import LightLink +from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.zcl import ClusterType from zhaquirks.const import ( CLUSTER_ID, @@ -23,214 +13,96 @@ COMMAND_PRESS, COMMAND_STOP, COMMAND_STOP_ON_OFF, - DEVICE_TYPE, DIM_DOWN, DIM_UP, ENDPOINT_ID, - ENDPOINTS, - INPUT_CLUSTERS, LEFT, LONG_PRESS, LONG_RELEASE, - MODELS_INFO, - OUTPUT_CLUSTERS, PARAMS, - PROFILE_ID, RIGHT, SHORT_PRESS, TURN_OFF, TURN_ON, ) -from zhaquirks.ikea import ( - IKEA, - IKEA_CLUSTER_ID, - WWAH_CLUSTER_ID, - DoublingPowerConfig2AAACluster, - ScenesCluster, -) - - -class IkeaTradfriRemoteV1(CustomDevice): - """Custom device representing IKEA of Sweden TRADFRI remote control V1.0.024.""" - - signature = { - # - MODELS_INFO: [(IKEA, "Remote Control N2")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } - }, - } +from zhaquirks.ikea import IKEA, DoublingPowerConfig2AAACluster, ScenesCluster - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DoublingPowerConfig2AAACluster, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - ScenesCluster, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } - } - } - - device_automation_triggers = { - (SHORT_PRESS, TURN_ON): {COMMAND: COMMAND_ON, CLUSTER_ID: 6, ENDPOINT_ID: 1}, - (LONG_PRESS, DIM_UP): { - COMMAND: COMMAND_MOVE_ON_OFF, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - PARAMS: {"move_mode": 0}, - }, - (LONG_RELEASE, DIM_UP): { - COMMAND: COMMAND_STOP_ON_OFF, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - }, - (SHORT_PRESS, TURN_OFF): {COMMAND: COMMAND_OFF, CLUSTER_ID: 6, ENDPOINT_ID: 1}, - (LONG_PRESS, DIM_DOWN): { - COMMAND: COMMAND_MOVE, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - PARAMS: {"move_mode": 1}, - }, - (LONG_RELEASE, DIM_DOWN): { - COMMAND: COMMAND_STOP, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - }, - (SHORT_PRESS, LEFT): { - COMMAND: COMMAND_PRESS, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 257, - "param2": 13, - "param3": 0, +( + add_to_registry_v2(IKEA, "Remote Control N2") + .replaces(DoublingPowerConfig2AAACluster) # will only double for old firmware + .replaces(ScenesCluster, cluster_type=ClusterType.Client) + .device_automation_triggers( + { + (SHORT_PRESS, TURN_ON): { + COMMAND: COMMAND_ON, + CLUSTER_ID: 6, + ENDPOINT_ID: 1, }, - }, - (LONG_PRESS, LEFT): { - COMMAND: COMMAND_HOLD, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 3329, - "param2": 0, + (LONG_PRESS, DIM_UP): { + COMMAND: COMMAND_MOVE_ON_OFF, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, + PARAMS: {"move_mode": 0}, }, - }, - (SHORT_PRESS, RIGHT): { - COMMAND: COMMAND_PRESS, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 256, - "param2": 13, - "param3": 0, + (LONG_RELEASE, DIM_UP): { + COMMAND: COMMAND_STOP_ON_OFF, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, }, - }, - (LONG_PRESS, RIGHT): { - COMMAND: COMMAND_HOLD, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 3328, - "param2": 0, + (SHORT_PRESS, TURN_OFF): { + COMMAND: COMMAND_OFF, + CLUSTER_ID: 6, + ENDPOINT_ID: 1, + }, + (LONG_PRESS, DIM_DOWN): { + COMMAND: COMMAND_MOVE, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, + PARAMS: {"move_mode": 1}, + }, + (LONG_RELEASE, DIM_DOWN): { + COMMAND: COMMAND_STOP, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, + }, + (SHORT_PRESS, LEFT): { + COMMAND: COMMAND_PRESS, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 257, + "param2": 13, + "param3": 0, + }, + }, + (LONG_PRESS, LEFT): { + COMMAND: COMMAND_HOLD, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 3329, + "param2": 0, + }, + }, + (SHORT_PRESS, RIGHT): { + COMMAND: COMMAND_PRESS, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 256, + "param2": 13, + "param3": 0, + }, + }, + (LONG_PRESS, RIGHT): { + COMMAND: COMMAND_HOLD, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 3328, + "param2": 0, + }, }, - }, - } - - -class IkeaTradfriRemoteV2(CustomDevice): - """Custom device representing IKEA of Sweden TRADFRI remote control Version 2.4.5.""" - - signature = { - # - MODELS_INFO: [(IKEA, "Remote Control N2")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - ScenesCluster.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - ScenesCluster, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } } - } - - device_automation_triggers = IkeaTradfriRemoteV1.device_automation_triggers.copy() + ) +) From c6ed94a52a469e72b32ece2a92d528060c7fd034 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 29 May 2024 18:31:31 +0200 Subject: [PATCH 11/16] Add more Aqara H1 wireless double rocker variants (#3169) --- zhaquirks/xiaomi/aqara/remote_h1.py | 148 +++++++++++++++++++++++----- 1 file changed, 125 insertions(+), 23 deletions(-) diff --git a/zhaquirks/xiaomi/aqara/remote_h1.py b/zhaquirks/xiaomi/aqara/remote_h1.py index d2500cc4a8..2223a3df98 100644 --- a/zhaquirks/xiaomi/aqara/remote_h1.py +++ b/zhaquirks/xiaomi/aqara/remote_h1.py @@ -1,7 +1,15 @@ """Aqara H1-series wireless remote.""" from zigpy.profiles import zha import zigpy.types as t -from zigpy.zcl.clusters.general import Basic, Identify, OnOff, PowerConfiguration +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + LevelControl, + MultistateInput, + OnOff, + PowerConfiguration, +) +from zigpy.zcl.clusters.lighting import Color from zhaquirks import PowerConfigurationCluster from zhaquirks.const import ( @@ -147,9 +155,6 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], ENDPOINTS: { 1: { - # SizePrefixedSimpleDescriptor( - # endpoint=1, profile=260, device_type=259, device_version=1, - # input_clusters=[0, 3, 1], output_clusters=[3, 6]) PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [ @@ -163,9 +168,6 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): ], }, 3: { - # SizePrefixedSimpleDescriptor( - # endpoint=3, profile=260, device_type=259, device_version=1, - # input_clusters=[3], output_clusters=[6]) PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [Identify.cluster_id], @@ -218,7 +220,7 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): } device_automation_triggers = { # triggers when operation_mode == event - # the button doesn't send an release event after hold + # the button doesn't send a release event after hold (SHORT_PRESS, LEFT): {COMMAND: COMMAND_1_SINGLE}, (DOUBLE_PRESS, LEFT): {COMMAND: COMMAND_1_DOUBLE}, (TRIPLE_PRESS, LEFT): {COMMAND: COMMAND_1_TRIPLE}, @@ -238,17 +240,13 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): } -class RemoteH1DoubleRocker2(XiaomiCustomDevice): +class RemoteH1DoubleRocker2(RemoteH1DoubleRocker1): """Aqara H1 Wireless Remote Double Rocker Version WRS-R02, variant 2.""" signature = { MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], ENDPOINTS: { 1: { - # "1": { - # "profile_id": 260, "device_type": "0x0103", - # "in_clusters": [ "0x0000", "0x0001", "0x0003" ], - # "out_clusters": [ "0x0003", "0x0006" ] } PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [ @@ -262,10 +260,6 @@ class RemoteH1DoubleRocker2(XiaomiCustomDevice): ], }, 2: { - # "2": { - # "profile_id": 260, "device_type": "0x0103", - # "in_clusters": [ "0x0003" ], - # "out_clusters": [ "0x0003", "0x0006" ] } PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [ @@ -277,10 +271,6 @@ class RemoteH1DoubleRocker2(XiaomiCustomDevice): ], }, 3: { - # "3": { - # "profile_id": 260, "device_type": "0x0103", - # "in_clusters": [ "0x0003" ], - # "out_clusters": [ "0x0006" ] } PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [Identify.cluster_id], @@ -288,5 +278,117 @@ class RemoteH1DoubleRocker2(XiaomiCustomDevice): }, }, } - replacement = RemoteH1DoubleRocker1.replacement - device_automation_triggers = RemoteH1DoubleRocker1.device_automation_triggers + + +class RemoteH1DoubleRocker3(RemoteH1DoubleRocker1): + """Aqara H1 Wireless Remote Double Rocker Version WRS-R02, variant 3.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + ], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: {}, + 5: {}, + 6: {}, + }, + } + + +class RemoteH1DoubleRocker4(RemoteH1DoubleRocker1): + """Aqara H1 Wireless Remote Double Rocker Version WRS-R02, variant 4.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + ], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + MultistateInput.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + MultistateInput.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 6: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + MultistateInput.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + }, + } From 6907545ddd8b51c516e93e6a7142064b3ffa0ad5 Mon Sep 17 00:00:00 2001 From: NH-Networks <25506868+NH-Networks@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:43:59 +0200 Subject: [PATCH 12/16] Add Avatto/Tuya 1/2-ch dimmer support (#3190) Add support for Avatto 1/2-ch dimmer. `TS0601` models: `_TZE204_o9gyszw2`, `_TZE204_5cuocqty` --------- Co-authored-by: TheJulianJES --- zhaquirks/tuya/ts0601_dimmer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 7b2d665a01..9d60d2ebb9 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -153,6 +153,7 @@ class TuyaSingleSwitchDimmerGP(TuyaDimmerSwitch): ("_TZE200_y8yjulon", "TS0601"), ("_TZE204_n9ctkb6j", "TS0601"), # BSEED ("_TZE204_vevc4c6g", "TS0601"), # BSEED + ("_TZE204_5cuocqty", "TS0601"), # Avattto ZDMS16-1 ], ENDPOINTS: { # Date: Wed, 26 Jun 2024 10:48:17 -0400 Subject: [PATCH 13/16] =?UTF-8?q?Add=20quirks=20for=20new=20Sinop=C3=A9=20?= =?UTF-8?q?Load=20Controller=20(#3195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhaquirks/sinope/light.py | 4 +- zhaquirks/sinope/sensor.py | 2 +- zhaquirks/sinope/switch.py | 74 +++++++++++++++++++++++++++++++--- zhaquirks/sinope/thermostat.py | 31 ++++++++------ 4 files changed, 90 insertions(+), 21 deletions(-) diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py index 72496350c4..381fedf59e 100644 --- a/zhaquirks/sinope/light.py +++ b/zhaquirks/sinope/light.py @@ -42,7 +42,7 @@ class SinopeTechnologiesManufacturerCluster(CustomCluster): """SinopeTechnologiesManufacturerCluster manufacturer cluster.""" class KeypadLock(t.enum8): - """keypad_lockout values.""" + """Keypad_lockout values.""" Unlocked = 0x00 Locked = 0x01 @@ -61,7 +61,7 @@ class DoubleFull(t.enum8): On = 0x01 class Action(t.enum8): - """action_report values.""" + """Action_report values.""" Single_on = 0x01 Single_release_on = 0x02 diff --git a/zhaquirks/sinope/sensor.py b/zhaquirks/sinope/sensor.py index 3fea113764..b28fccf9f9 100644 --- a/zhaquirks/sinope/sensor.py +++ b/zhaquirks/sinope/sensor.py @@ -52,7 +52,7 @@ class SinopeTechnologiesIasZoneCluster(CustomCluster, IasZone): """SinopeTechnologiesIasZoneCluster custom cluster.""" class LeakStatus(t.enum8): - """leak_status values.""" + """Leak_status values.""" Dry = 0x00 Leak = 0x01 diff --git a/zhaquirks/sinope/switch.py b/zhaquirks/sinope/switch.py index fa40d7303a..ee345b8c93 100644 --- a/zhaquirks/sinope/switch.py +++ b/zhaquirks/sinope/switch.py @@ -51,7 +51,7 @@ class SinopeManufacturerCluster(CustomCluster): """SinopeManufacturerCluster manufacturer cluster.""" class KeypadLock(t.enum8): - """keypad_lockout values.""" + """Keypad_lockout values.""" Unlocked = 0x00 Locked = 0x01 @@ -70,6 +70,7 @@ class AlarmAction(t.enum8): Notify = 0x01 Close = 0x02 Close_notify = 0x03 + No_flow = 0x04 class PowerSource(t.uint32_t): """Valve power source types.""" @@ -93,7 +94,7 @@ class AbnormalAction(t.bitmap16): Close_notify = 0x0003 class ColdStatus(t.enum8): - """cold_load_pickup_status values.""" + """Cold_load_pickup_status values.""" Active = 0x00 Off = 0x01 @@ -188,14 +189,14 @@ class CustomMeteringCluster(CustomCluster, Metering): """Custom Metering Cluster.""" class ValveStatus(t.bitmap8): - """valve_status.""" + """Valve_status.""" Off = 0x00 Off_armed = 0x01 On = 0x02 class UnitOfMeasure(t.enum8): - """unit_of_measure.""" + """Unit_of_measure.""" KWh = 0x00 Lh = 0x07 @@ -326,6 +327,67 @@ class SinopeTechnologiesLoadController(CustomDevice): } +class SinopeTechnologiesLoadController_V2(CustomDevice): + """SinopeTechnologiesLoadController version 2 custom device.""" + + signature = { + # + MODELS_INFO: [ + (SINOPE, "RM3250ZB"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.ON_OFF_OUTPUT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + CustomDeviceTemperatureCluster, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + Diagnostic.cluster_id, + SinopeManufacturerCluster, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + } + } + + class SinopeTechnologiesValve(CustomDevice): """SinopeTechnologiesValve custom device.""" @@ -605,8 +667,8 @@ class SinopeTechnologiesCalypso(CustomDevice): } -class SinopeTechnologiesNewSwitch(CustomDevice): - """SinopeTechnologiesNewSwitch custom device.""" +class SinopeTechnologiesSwitch_V2(CustomDevice): + """SinopeTechnologiesSwitch version 2 custom device.""" signature = { # Date: Thu, 27 Jun 2024 00:53:57 +1000 Subject: [PATCH 14/16] Add Mercator/Tuya Ikuu 5/6-ch switches support (#3218) --- zhaquirks/tuya/ts0601_switch.py | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/zhaquirks/tuya/ts0601_switch.py b/zhaquirks/tuya/ts0601_switch.py index 4f3896a68e..1a5bd4deef 100644 --- a/zhaquirks/tuya/ts0601_switch.py +++ b/zhaquirks/tuya/ts0601_switch.py @@ -648,6 +648,82 @@ class TuyaQuadrupleSwitch_GP(TuyaSwitch): } +class TuyaQuintupleSwitchTO(TuyaSwitch): + """Tuya quintuple channel switch time on out cluster device.""" + + quirk_id = TUYA_PLUG_MANUFACTURER + + signature = { + # + MODELS_INFO: [ + ("_TZE200_leaqthqq", "TS0601"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaOnOffManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaOnOffManufCluster, + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + } + } + + class TuyaSextupleSwitchTO(TuyaSwitch): """Tuya sextuple channel switch time on out cluster device.""" @@ -659,6 +735,7 @@ class TuyaSextupleSwitchTO(TuyaSwitch): # output_clusters=[0x000A,0x0019]> MODELS_INFO: [ ("_TZE200_9mahtqtg", "TS0601"), + ("_TZE200_wnp4d4va", "TS0601"), ], ENDPOINTS: { 1: { From c20e40a0f6a7c72eb1a192b99e605a5f1d03116e Mon Sep 17 00:00:00 2001 From: Philippe De Swert Date: Wed, 26 Jun 2024 17:58:43 +0300 Subject: [PATCH 15/16] Add Tuya TY0201 temperature/humidity sensor (#3201) --- zhaquirks/tuya/ty0201.py | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 zhaquirks/tuya/ty0201.py diff --git a/zhaquirks/tuya/ty0201.py b/zhaquirks/tuya/ty0201.py new file mode 100644 index 0000000000..55b6ac198c --- /dev/null +++ b/zhaquirks/tuya/ty0201.py @@ -0,0 +1,62 @@ +"""Tuya TY0201 temperature and humidity sensor.""" + +from zigpy.profiles import zha +from zigpy.profiles.zha import DeviceType +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration +from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.tuya.air import TuyaAirQualityHumidity, TuyaAirQualityTemperature + + +class TuyaTempHumiditySensor(CustomDevice): + """Temu/Aliexpress temperature and humidity sensor.""" + + signature = { + # + MODELS_INFO: [("_TZ3000_bjawzodf", "TY0201"), + ("_TZ3000_zl1kmjqx", "TY0201")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + TuyaAirQualityTemperature, + TuyaAirQualityHumidity, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } From 8317dc69c3b36e7ae5cabe40d7937c02ca259f77 Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Wed, 26 Jun 2024 17:04:26 +0200 Subject: [PATCH 16/16] Add UseeLink/Tuya cover support `_TZ3000_ltiqubue` (#3186) Co-authored-by: TheJulianJES --- zhaquirks/tuya/ts130f.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/zhaquirks/tuya/ts130f.py b/zhaquirks/tuya/ts130f.py index ba97ce2469..c8668df72b 100644 --- a/zhaquirks/tuya/ts130f.py +++ b/zhaquirks/tuya/ts130f.py @@ -8,6 +8,7 @@ Basic, GreenPowerProxy, Groups, + Identify, OnOff, Ota, Scenes, @@ -268,6 +269,45 @@ class TuyaTS130FTI2(CustomDevice): }, } +class TuyaTS130FUL(CustomDevice): + """Tuya Cover variant from UseeLink.""" + + signature = { + # This signature was copied from TuyaTS130FTO and changed to fit the UseeLink device. + MODEL: "TS130F", + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + WindowCovering.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id], + }, + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaWithBacklightOnOffCluster, + TuyaCoveringCluster, + ], + OUTPUT_CLUSTERS: [Time.cluster_id], + }, + }, + } class TuyaTS130FTO(CustomDevice): """Tuya smart curtain roller shutter Time Out."""