From c99f86b282e131d92f0690c172c3ea15bbf0622f Mon Sep 17 00:00:00 2001 From: prairiesnpr Date: Wed, 22 Jan 2025 19:24:31 -0500 Subject: [PATCH 01/19] Add Nous (Tuya) SZ-T04 (#3683) --- tests/test_tuya_builder.py | 59 +++++- tests/test_tuya_sensor.py | 8 + zhaquirks/tuya/ts0601_sensor.py | 124 ------------- zhaquirks/tuya/tuya_sensor.py | 319 ++++++++++++++++++++++++++++++++ 4 files changed, 384 insertions(+), 126 deletions(-) delete mode 100644 zhaquirks/tuya/ts0601_sensor.py create mode 100644 zhaquirks/tuya/tuya_sensor.py diff --git a/tests/test_tuya_builder.py b/tests/test_tuya_builder.py index f10eed0386..27a669f28a 100644 --- a/tests/test_tuya_builder.py +++ b/tests/test_tuya_builder.py @@ -1,5 +1,6 @@ """Tests for TuyaQuirkBuilder.""" +import datetime from unittest import mock import pytest @@ -9,10 +10,11 @@ from zigpy.zcl import foundation from zigpy.zcl.clusters.general import Basic, BatterySize -from tests.common import ClusterListener, wait_for_zigpy_tasks +from tests.common import ClusterListener, MockDatetime, wait_for_zigpy_tasks import zhaquirks from zhaquirks.tuya import ( TUYA_QUERY_DATA, + TUYA_SET_TIME, TuyaPowerConfigurationCluster, TuyaPowerConfigurationCluster2AAA, ) @@ -31,6 +33,9 @@ TuyaValveWaterConsumed, ) from zhaquirks.tuya.mcu import TuyaMCUCluster, TuyaOnOffNM +from zhaquirks.tuya.tuya_sensor import NoManufTimeTuyaMCUCluster + +ZCL_TUYA_SET_TIME = b"\x09\x12\x24\x0d\x00" zhaquirks.setup() @@ -140,6 +145,9 @@ class TestEnum(t.enum8): A = 0x00 B = 0x01 + class ModTuyaMCUCluster(TuyaMCUCluster): + """Modified Cluster.""" + entry = ( TuyaQuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .tuya_battery(dp_id=1) @@ -178,7 +186,7 @@ class TestEnum(t.enum8): fallback_name="Test enum", ) .skip_configuration() - .add_to_registry() + .add_to_registry(replacement_cluster=ModTuyaMCUCluster) ) # coverage for overridden __eq__ method @@ -195,6 +203,7 @@ class TestEnum(t.enum8): assert isinstance(ep.basic, Basic) assert ep.tuya_manufacturer is not None + assert isinstance(ep.tuya_manufacturer, ModTuyaMCUCluster) assert isinstance(ep.tuya_manufacturer, TuyaMCUCluster) tuya_cluster = ep.tuya_manufacturer @@ -303,3 +312,49 @@ async def test_tuya_spell(device_mock, read_attr_spell, data_query_spell): messages += 1 request_mock.reset_mock() + + +async def test_tuya_mcu_set_time(device_mock): + """Test TuyaQuirkBuilder replacement cluster, set_time requests (0x24) messages for MCU devices.""" + + registry = DeviceRegistry() + + ( + TuyaQuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) + .tuya_battery(dp_id=1) + .skip_configuration() + .add_to_registry(replacement_cluster=NoManufTimeTuyaMCUCluster) + ) + + quirked = registry.get_device(device_mock) + assert isinstance(quirked, CustomDeviceV2) + assert quirked in registry + + ep = quirked.endpoints[1] + + assert not ep.tuya_manufacturer._is_manuf_specific + assert not ep.tuya_manufacturer.server_commands[ + TUYA_SET_TIME + ].is_manufacturer_specific + + # Mock datetime + origdatetime = datetime.datetime + datetime.datetime = MockDatetime + + # simulate a SET_TIME message + hdr, args = ep.tuya_manufacturer.deserialize(ZCL_TUYA_SET_TIME) + assert hdr.command_id == TUYA_SET_TIME + + with mock.patch.object( + ep.tuya_manufacturer._endpoint, + "request", + return_value=foundation.Status.SUCCESS, + ) as m1: + ep.tuya_manufacturer.handle_message(hdr, args) + await wait_for_zigpy_tasks() + + res_hdr = foundation.ZCLHeader.deserialize(m1.await_args[1]["data"]) + assert not res_hdr[0].manufacturer + assert not res_hdr[0].frame_control.is_manufacturer_specific + + datetime.datetime = origdatetime # restore datetime diff --git a/tests/test_tuya_sensor.py b/tests/test_tuya_sensor.py index 8ce1533203..649247a665 100644 --- a/tests/test_tuya_sensor.py +++ b/tests/test_tuya_sensor.py @@ -28,6 +28,14 @@ ("_TZE200_bq5c8xfe", "TS0601", 100, 10, True), ("_TZE200_vs0skpuc", "TS0601", 100, 10, True), ("_TZE200_44af8vyi", "TS0601", 100, 10, True), + ("_TZE200_lve3dvpy", "TS0601", 100, 10, False), # TH01Z - Temp & humid w/ clock + ("_TZE200_c7emyjom", "TS0601", 100, 10, False), + ("_TZE200_locansqn", "TS0601", 100, 10, False), + ("_TZE200_qrztc3ev", "TS0601", 100, 10, False), + ("_TZE200_snloy4rw", "TS0601", 100, 10, False), + ("_TZE200_eanjj2pa", "TS0601", 100, 10, False), + ("_TZE200_ydrdfkim", "TS0601", 100, 10, False), + ("_TZE284_locansqn", "TS0601", 100, 10, False), ], ) async def test_handle_get_data( diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py deleted file mode 100644 index e68cff5a99..0000000000 --- a/zhaquirks/tuya/ts0601_sensor.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Tuya temp and humidity sensors.""" - -from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass -import zigpy.types as t - -from zhaquirks.tuya import TuyaPowerConfigurationCluster2AAA -from zhaquirks.tuya.builder import TuyaQuirkBuilder, TuyaTemperatureMeasurement - -( - TuyaQuirkBuilder("_TZE200_bjawzodf", "TS0601") - .applies_to("_TZE200_zl1kmjqx", "TS0601") - # Not using tuya_temperature because device reports negative values incorrectly - .tuya_dp( - dp_id=1, - ep_attribute=TuyaTemperatureMeasurement.ep_attribute, - attribute_name=TuyaTemperatureMeasurement.AttributeDefs.measured_value.name, - converter=lambda x: ((x - 0xFFFF if x > 0x2000 else x) * 10), - ) - .adds(TuyaTemperatureMeasurement) - .tuya_humidity(dp_id=2, scale=10) - .tuya_battery(dp_id=4) - .skip_configuration() - .add_to_registry() -) - -( - TuyaQuirkBuilder("_TZE200_bq5c8xfe", "TS0601") - .applies_to("_TZE200_vs0skpuc", "TS0601") - .applies_to("_TZE200_qyflbnbj", "TS0601") - .applies_to("_TZE284_qyflbnbj", "TS0601") - .applies_to("_TZE200_44af8vyi", "TS0601") - # Not using tuya_temperature because device reports negative values incorrectly - .tuya_dp( - dp_id=1, - ep_attribute=TuyaTemperatureMeasurement.ep_attribute, - attribute_name=TuyaTemperatureMeasurement.AttributeDefs.measured_value.name, - converter=lambda x: ((x - 0xFFFF if x > 0x2000 else x) * 10), - ) - .adds(TuyaTemperatureMeasurement) - .tuya_humidity(dp_id=2) - .tuya_battery(dp_id=4) - .skip_configuration() - .add_to_registry() -) - -( - TuyaQuirkBuilder("_TZE200_a8sdabtg", "TS0601") # Variant without screen, round - .applies_to("_TZE200_qoy0ekbd", "TS0601") - .applies_to("_TZE200_znbl8dj5", "TS0601") - .applies_to("_TZE200_zppcgbdj", "TS0601") - .applies_to("_TZE204_s139roas", "TS0601") - .applies_to("_TZE200_s1xgth2u", "TS0601") # Nedis ZBSC30WT - .tuya_temperature(dp_id=1, scale=10) - .adds(TuyaTemperatureMeasurement) - .tuya_humidity(dp_id=2) - .tuya_battery(dp_id=4) - .skip_configuration() - .add_to_registry() -) - -( - TuyaQuirkBuilder("_TZE200_yjjdcqsq", "TS0601") - .applies_to("_TZE200_9yapgbuv", "TS0601") - .applies_to("_TZE204_9yapgbuv", "TS0601") - .applies_to("_TZE204_yjjdcqsq", "TS0601") - .applies_to("_TZE200_utkemkbs", "TS0601") - .applies_to("_TZE204_utkemkbs", "TS0601") - .applies_to("_TZE204_yjjdcqsq", "TS0601") - .applies_to("_TZE204_ksz749x8", "TS0601") - .tuya_temperature(dp_id=1, scale=10) - .tuya_humidity(dp_id=2) - .tuya_dp( - dp_id=4, - ep_attribute=TuyaPowerConfigurationCluster2AAA.ep_attribute, - attribute_name="battery_percentage_remaining", - converter=lambda x: {0: 50, 1: 100, 2: 200}[x], - ) - .adds(TuyaPowerConfigurationCluster2AAA) - .skip_configuration() - .add_to_registry() -) - - -( - TuyaQuirkBuilder("_TZE284_aao3yzhs", "TS0601") - .applies_to("_TZE284_sgabhwa6", "TS0601") - .applies_to("_TZE284_nhgdf6qr", "TS0601") # Giex GX04 - .tuya_temperature(dp_id=5, scale=10) - .tuya_battery(dp_id=15) - .tuya_soil_moisture(dp_id=3) - .skip_configuration() - .add_to_registry() -) - - -( - TuyaQuirkBuilder("_TZE200_myd45weu", "TS0601") - .applies_to("_TZE200_ga1maeof", "TS0601") - .applies_to("_TZE200_9cqcpkgb", "TS0601") - .applies_to("_TZE204_myd45weu", "TS0601") - .applies_to("_TZE200_2se8efxh", "TS0601") # Immax Neo - .tuya_temperature(dp_id=5) - .tuya_battery(dp_id=15) - .tuya_soil_moisture(dp_id=3) - .skip_configuration() - .add_to_registry() -) - -( - TuyaQuirkBuilder("_TZE200_pay2byax", "TS0601") # Cusam ZG-102ZL - .applies_to("_TZE200_n8dljorx", "TS0601") - .tuya_sensor( - dp_id=101, - attribute_name="measured_value", - type=t.uint16_t, - fallback_name="Illuminance", - device_class=SensorDeviceClass.ILLUMINANCE, - state_class=SensorStateClass.MEASUREMENT, - ) - .tuya_contact(dp_id=1) - .tuya_battery(dp_id=2) - .skip_configuration() - .add_to_registry() -) diff --git a/zhaquirks/tuya/tuya_sensor.py b/zhaquirks/tuya/tuya_sensor.py new file mode 100644 index 0000000000..0a19164acd --- /dev/null +++ b/zhaquirks/tuya/tuya_sensor.py @@ -0,0 +1,319 @@ +"""Tuya temp and humidity sensors.""" + +import copy + +from zigpy.quirks.v2 import EntityPlatform, EntityType +from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfTemperature, UnitOfTime +from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass +import zigpy.types as t +from zigpy.zcl import foundation + +from zhaquirks.tuya import ( + TUYA_SET_TIME, + TuyaPowerConfigurationCluster2AAA, + TuyaTimePayload, +) +from zhaquirks.tuya.builder import TuyaQuirkBuilder, TuyaTemperatureMeasurement +from zhaquirks.tuya.mcu import TuyaMCUCluster + + +class TuyaTempUnitConvert(t.enum8): + """Tuya temperature unit convert enum.""" + + Celsius = 0x00 + Fahrenheit = 0x01 + + +class TuyaNousTempHumiAlarm(t.enum8): + """Tuya temperature and humidity alarm enum.""" + + LowerAlarm = 0x00 + UpperAlarm = 0x01 + Canceled = 0x02 + + +( + TuyaQuirkBuilder("_TZE200_bjawzodf", "TS0601") + .applies_to("_TZE200_zl1kmjqx", "TS0601") + # Not using tuya_temperature because device reports negative values incorrectly + .tuya_dp( + dp_id=1, + ep_attribute=TuyaTemperatureMeasurement.ep_attribute, + attribute_name=TuyaTemperatureMeasurement.AttributeDefs.measured_value.name, + converter=lambda x: ((x - 0xFFFF if x > 0x2000 else x) * 10), + ) + .adds(TuyaTemperatureMeasurement) + .tuya_humidity(dp_id=2, scale=10) + .tuya_battery(dp_id=4) + .skip_configuration() + .add_to_registry() +) + + +( + TuyaQuirkBuilder("_TZE200_bq5c8xfe", "TS0601") + .applies_to("_TZE200_vs0skpuc", "TS0601") + .applies_to("_TZE200_qyflbnbj", "TS0601") + .applies_to("_TZE284_qyflbnbj", "TS0601") + .applies_to("_TZE200_44af8vyi", "TS0601") + # Not using tuya_temperature because device reports negative values incorrectly + .tuya_dp( + dp_id=1, + ep_attribute=TuyaTemperatureMeasurement.ep_attribute, + attribute_name=TuyaTemperatureMeasurement.AttributeDefs.measured_value.name, + converter=lambda x: ((x - 0xFFFF if x > 0x2000 else x) * 10), + ) + .adds(TuyaTemperatureMeasurement) + .tuya_humidity(dp_id=2) + .tuya_battery(dp_id=4) + .skip_configuration() + .add_to_registry() +) + + +( + TuyaQuirkBuilder("_TZE200_a8sdabtg", "TS0601") # Variant without screen, round + .applies_to("_TZE200_qoy0ekbd", "TS0601") + .applies_to("_TZE200_znbl8dj5", "TS0601") + .applies_to("_TZE200_zppcgbdj", "TS0601") + .applies_to("_TZE204_s139roas", "TS0601") + .applies_to("_TZE200_s1xgth2u", "TS0601") # Nedis ZBSC30WT + .tuya_temperature(dp_id=1, scale=10) + .adds(TuyaTemperatureMeasurement) + .tuya_humidity(dp_id=2) + .tuya_battery(dp_id=4) + .skip_configuration() + .add_to_registry() +) + + +class NoManufTimeTuyaMCUCluster(TuyaMCUCluster): + """Tuya Manufacturer Cluster with set_time mod.""" + + set_time_offset = 1970 + set_time_local_offset = 1970 + + # Deepcopy required to override 'set_time', without, it will revert + server_commands = copy.deepcopy(TuyaMCUCluster.server_commands) + server_commands.update( + { + TUYA_SET_TIME: foundation.ZCLCommandDef( + "set_time", + {"time": TuyaTimePayload}, + False, + is_manufacturer_specific=False, + ), + } + ) + + +# TH01Z - Temperature and humidity sensor with clock +( + TuyaQuirkBuilder("_TZE200_lve3dvpy", "TS0601") + .applies_to("_TZE200_c7emyjom", "TS0601") + .applies_to("_TZE200_locansqn", "TS0601") + .applies_to("_TZE200_qrztc3ev", "TS0601") + .applies_to("_TZE200_snloy4rw", "TS0601") + .applies_to("_TZE200_eanjj2pa", "TS0601") + .applies_to("_TZE200_ydrdfkim", "TS0601") + .applies_to("_TZE284_locansqn", "TS0601") + .tuya_temperature(dp_id=1, scale=10) + .tuya_humidity(dp_id=2) + .tuya_battery(dp_id=4) + .tuya_number( + dp_id=17, + attribute_name="temperature_report_interval", + type=t.uint16_t, + device_class=SensorDeviceClass.DURATION, + unit=UnitOfTime.MINUTES, + min_value=5, + max_value=120, + step=5, + entity_type=EntityType.CONFIG, + translation_key="temperature_report_interval", + fallback_name="Temperature report interval", + ) + .tuya_number( + dp_id=18, + attribute_name="humidity_report_interval", + type=t.uint16_t, + device_class=SensorDeviceClass.DURATION, + unit=UnitOfTime.MINUTES, + min_value=5, + max_value=120, + step=5, + entity_type=EntityType.CONFIG, + translation_key="humidity_report_interval", + fallback_name="Humidity report interval", + ) + .tuya_enum( + dp_id=9, + attribute_name="display_unit", + enum_class=TuyaTempUnitConvert, + entity_type=EntityType.CONFIG, + translation_key="display_unit", + fallback_name="Display unit", + ) + .tuya_enum( + dp_id=14, + attribute_name="temperature_alarm", + enum_class=TuyaNousTempHumiAlarm, + entity_platform=EntityPlatform.SENSOR, + entity_type=EntityType.STANDARD, + translation_key="temperature_alarm", + fallback_name="Temperature alarm", + ) + .tuya_number( + dp_id=10, + attribute_name="alarm_temperature_max", + type=t.uint16_t, + unit=UnitOfTemperature.CELSIUS, + min_value=-20, + max_value=60, + step=1, + multiplier=0.1, + entity_type=EntityType.CONFIG, + translation_key="alarm_temperature_max", + fallback_name="Alarm temperature max", + ) + .tuya_number( + dp_id=11, + attribute_name="alarm_temperature_min", + type=t.uint16_t, + unit=UnitOfTemperature.CELSIUS, + min_value=-20, + max_value=60, + step=1, + multiplier=0.1, + entity_type=EntityType.CONFIG, + translation_key="alarm_temperature_min", + fallback_name="Alarm temperature min", + ) + .tuya_number( + dp_id=19, + attribute_name="temperature_sensitivity", + type=t.uint16_t, + unit=UnitOfTemperature.CELSIUS, + min_value=0.1, + max_value=50, + step=0.1, + multiplier=0.1, + entity_type=EntityType.CONFIG, + translation_key="temperature_sensitivity", + fallback_name="Temperature sensitivity", + ) + .tuya_enum( + dp_id=15, + attribute_name="humidity_alarm", + enum_class=TuyaNousTempHumiAlarm, + entity_platform=EntityPlatform.SENSOR, + entity_type=EntityType.STANDARD, + translation_key="humidity_alarm", + fallback_name="Humidity alarm", + ) + .tuya_number( + dp_id=12, + attribute_name="alarm_humidity_max", + type=t.uint16_t, + unit=PERCENTAGE, + min_value=0, + max_value=100, + step=1, + entity_type=EntityType.CONFIG, + translation_key="alarm_humidity_max", + fallback_name="Alarm humidity max", + ) + .tuya_number( + dp_id=13, + attribute_name="alarm_humidity_min", + type=t.uint16_t, + unit=PERCENTAGE, + min_value=0, + max_value=100, + step=1, + entity_type=EntityType.CONFIG, + translation_key="alarm_humidity_min", + fallback_name="Alarm humidity min", + ) + .tuya_number( + dp_id=20, + attribute_name="humidity_sensitivity", + type=t.uint16_t, + unit=PERCENTAGE, + min_value=1, + max_value=100, + step=1, + entity_type=EntityType.CONFIG, + translation_key="humidity_sensitivity", + fallback_name="Humidity sensitivity", + ) + .skip_configuration() + .add_to_registry(replacement_cluster=NoManufTimeTuyaMCUCluster) +) + + +( + TuyaQuirkBuilder("_TZE200_yjjdcqsq", "TS0601") + .applies_to("_TZE200_9yapgbuv", "TS0601") + .applies_to("_TZE204_9yapgbuv", "TS0601") + .applies_to("_TZE204_yjjdcqsq", "TS0601") + .applies_to("_TZE200_utkemkbs", "TS0601") + .applies_to("_TZE204_utkemkbs", "TS0601") + .applies_to("_TZE204_yjjdcqsq", "TS0601") + .applies_to("_TZE204_ksz749x8", "TS0601") + .tuya_temperature(dp_id=1, scale=10) + .tuya_humidity(dp_id=2) + .tuya_dp( + dp_id=4, + ep_attribute=TuyaPowerConfigurationCluster2AAA.ep_attribute, + attribute_name="battery_percentage_remaining", + converter=lambda x: {0: 50, 1: 100, 2: 200}[x], + ) + .adds(TuyaPowerConfigurationCluster2AAA) + .skip_configuration() + .add_to_registry() +) + + +( + TuyaQuirkBuilder("_TZE284_aao3yzhs", "TS0601") + .applies_to("_TZE284_sgabhwa6", "TS0601") + .applies_to("_TZE284_nhgdf6qr", "TS0601") # Giex GX04 + .tuya_temperature(dp_id=5, scale=10) + .tuya_battery(dp_id=15) + .tuya_soil_moisture(dp_id=3) + .skip_configuration() + .add_to_registry() +) + + +( + TuyaQuirkBuilder("_TZE200_myd45weu", "TS0601") + .applies_to("_TZE200_ga1maeof", "TS0601") + .applies_to("_TZE200_9cqcpkgb", "TS0601") + .applies_to("_TZE204_myd45weu", "TS0601") + .applies_to("_TZE200_2se8efxh", "TS0601") # Immax Neo + .tuya_temperature(dp_id=5) + .tuya_battery(dp_id=15) + .tuya_soil_moisture(dp_id=3) + .skip_configuration() + .add_to_registry() +) + + +( + TuyaQuirkBuilder("_TZE200_pay2byax", "TS0601") # Cusam ZG-102ZL + .applies_to("_TZE200_n8dljorx", "TS0601") + .tuya_sensor( + dp_id=101, + attribute_name="measured_value", + type=t.uint16_t, + fallback_name="Illuminance", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + ) + .tuya_contact(dp_id=1) + .tuya_battery(dp_id=2) + .skip_configuration() + .add_to_registry() +) From bb6d26d7b457515d798365a7979fa10256f2c5cc Mon Sep 17 00:00:00 2001 From: prairiesnpr Date: Wed, 22 Jan 2025 20:04:07 -0500 Subject: [PATCH 02/19] Add Tuya valve `_TZE204_z7a2jmyy` (#3731) --- tests/test_tuya.py | 2 +- tests/test_tuya_builder.py | 4 +- tests/test_tuya_spells.py | 2 +- tests/test_tuya_valve.py | 14 +- tuya.md | 2 +- zhaquirks/tuya/builder/__init__.py | 8 +- .../tuya/{ts0601_valve.py => tuya_valve.py} | 179 +++++++++++++++++- 7 files changed, 194 insertions(+), 17 deletions(-) rename zhaquirks/tuya/{ts0601_valve.py => tuya_valve.py} (73%) diff --git a/tests/test_tuya.py b/tests/test_tuya.py index abaa0b71ec..a3e637bb36 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -37,9 +37,9 @@ import zhaquirks.tuya.ts0601_electric_heating import zhaquirks.tuya.ts0601_motion import zhaquirks.tuya.ts0601_trv -import zhaquirks.tuya.ts0601_valve import zhaquirks.tuya.ts601_door import zhaquirks.tuya.ts1201 +import zhaquirks.tuya.tuya_valve zhaquirks.setup() diff --git a/tests/test_tuya_builder.py b/tests/test_tuya_builder.py index 27a669f28a..e82c023c65 100644 --- a/tests/test_tuya_builder.py +++ b/tests/test_tuya_builder.py @@ -30,7 +30,7 @@ TuyaRelativeHumidity, TuyaSoilMoisture, TuyaTemperatureMeasurement, - TuyaValveWaterConsumed, + TuyaValveWaterConsumedNoInstDemand, ) from zhaquirks.tuya.mcu import TuyaMCUCluster, TuyaOnOffNM from zhaquirks.tuya.tuya_sensor import NoManufTimeTuyaMCUCluster @@ -44,7 +44,7 @@ "method_name,attr_name,exp_class", [ ("tuya_battery", "power", TuyaPowerConfigurationCluster), - ("tuya_metering", "smartenergy_metering", TuyaValveWaterConsumed), + ("tuya_metering", "smartenergy_metering", TuyaValveWaterConsumedNoInstDemand), ("tuya_onoff", "on_off", TuyaOnOffNM), ("tuya_soil_moisture", "soil_moisture", TuyaSoilMoisture), ("tuya_temperature", "temperature", TuyaTemperatureMeasurement), diff --git a/tests/test_tuya_spells.py b/tests/test_tuya_spells.py index 60721a894c..eafac8bd85 100644 --- a/tests/test_tuya_spells.py +++ b/tests/test_tuya_spells.py @@ -22,7 +22,7 @@ TuyaNewManufCluster, TuyaZBOnOffAttributeCluster, ) -import zhaquirks.tuya.ts0601_valve +import zhaquirks.tuya.tuya_valve zhaquirks.setup() diff --git a/tests/test_tuya_valve.py b/tests/test_tuya_valve.py index 5beb973c46..a4c1dfb5c6 100644 --- a/tests/test_tuya_valve.py +++ b/tests/test_tuya_valve.py @@ -173,15 +173,15 @@ async def test_giex_02_quirk(zigpy_device_from_v2_quirk, model, manuf, use_minut ][entity] if not use_minutes: - assert number_metadata.max == zhaquirks.tuya.ts0601_valve.GIEX_12HRS_AS_SEC + assert number_metadata.max == zhaquirks.tuya.tuya_valve.GIEX_12HRS_AS_SEC else: - assert number_metadata.max == zhaquirks.tuya.ts0601_valve.GIEX_24HRS_AS_MIN + assert number_metadata.max == zhaquirks.tuya.tuya_valve.GIEX_24HRS_AS_MIN async def test_giex_functions(): """Test various Giex Valve functions.""" - assert zhaquirks.tuya.ts0601_valve.giex_string_to_td("12:01:05,3") == 43265 - assert zhaquirks.tuya.ts0601_valve.giex_string_to_ts("--:--:--") is None + assert zhaquirks.tuya.tuya_valve.giex_string_to_td("12:01:05,3") == 43265 + assert zhaquirks.tuya.tuya_valve.giex_string_to_ts("--:--:--") is None class MockDatetime: def now(self, tz: timezone): @@ -192,11 +192,11 @@ def strptime(self, v: str, fmt: str): """Mock strptime.""" return datetime.strptime(v, fmt) - with patch("zhaquirks.tuya.ts0601_valve.datetime", MockDatetime()): + with patch("zhaquirks.tuya.tuya_valve.datetime", MockDatetime()): assert ( - zhaquirks.tuya.ts0601_valve.giex_string_to_ts("20:12:01") + zhaquirks.tuya.tuya_valve.giex_string_to_ts("20:12:01") == datetime.fromisoformat("2024-10-02T12:10:23+04:00").timestamp() - + zhaquirks.tuya.ts0601_valve.UNIX_EPOCH_TO_ZCL_EPOCH + + zhaquirks.tuya.tuya_valve.UNIX_EPOCH_TO_ZCL_EPOCH ) diff --git a/tuya.md b/tuya.md index f77458f117..9ac2c0e06a 100644 --- a/tuya.md +++ b/tuya.md @@ -70,7 +70,7 @@ Adds a battery power cluster. .tuya_battery(dp_id=2, battery_type=BatterySize.AA, battery_qty=4) ``` -#### tuya_metering(dp_id: int, metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumed) +#### tuya_metering(dp_id: int, metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumedNoInstDemand) Adds a metering cluster. diff --git a/zhaquirks/tuya/builder/__init__.py b/zhaquirks/tuya/builder/__init__.py index d92e54d6d5..b4a1f132d7 100644 --- a/zhaquirks/tuya/builder/__init__.py +++ b/zhaquirks/tuya/builder/__init__.py @@ -115,8 +115,12 @@ class TuyaValveWaterConsumed(Metering, TuyaLocalCluster): Metering.AttributeDefs.metering_device_type.id: WATER_METERING, } + +class TuyaValveWaterConsumedNoInstDemand(TuyaValveWaterConsumed): + """Tuya Valve Water consumed cluster without instantaneous demand.""" + def __init__(self, *args, **kwargs): - """Init a TuyaValveWaterConsumed cluster.""" + """Init a TuyaValveWaterConsumedNoInstDemand cluster.""" super().__init__(*args, **kwargs) self.add_unsupported_attribute(Metering.AttributeDefs.instantaneous_demand.id) @@ -316,7 +320,7 @@ def tuya_ias( def tuya_metering( self, dp_id: int, - metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumed, + metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumedNoInstDemand, scale: float = 1, ) -> QuirkBuilder: """Add a Tuya Metering Configuration.""" diff --git a/zhaquirks/tuya/ts0601_valve.py b/zhaquirks/tuya/tuya_valve.py similarity index 73% rename from zhaquirks/tuya/ts0601_valve.py rename to zhaquirks/tuya/tuya_valve.py index 66cbb71b73..96b2f1f1f3 100644 --- a/zhaquirks/tuya/ts0601_valve.py +++ b/zhaquirks/tuya/tuya_valve.py @@ -3,13 +3,14 @@ from datetime import datetime, timedelta, timezone from zigpy.quirks.v2 import EntityPlatform, EntityType -from zigpy.quirks.v2.homeassistant import UnitOfTime +from zigpy.quirks.v2.homeassistant import UnitOfTime, UnitOfVolume from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass import zigpy.types as t from zigpy.zcl.clusters.general import BatterySize +from zigpy.zcl.clusters.smartenergy import Metering from zhaquirks.tuya import TUYA_CLUSTER_ID -from zhaquirks.tuya.builder import TuyaQuirkBuilder +from zhaquirks.tuya.builder import TuyaQuirkBuilder, TuyaValveWaterConsumed from zhaquirks.tuya.mcu import TuyaMCUCluster @@ -22,6 +23,18 @@ class TuyaValveWeatherDelay(t.enum8): Delayed_72h = 0x03 +class TuyaValveWeatherDelayVar02(t.enum8): + """Tuya Irrigation Valve weather delay enum var 02. + + Disabled is 0x03 + """ + + Delayed_24h = 0x00 + Delayed_48h = 0x01 + Delayed_72h = 0x02 + Disabled = 0x03 + + class TuyaValveTimerState(t.enum8): """Tuya Irrigation Valve timer state enum.""" @@ -30,6 +43,14 @@ class TuyaValveTimerState(t.enum8): Enabled = 0x02 +class TuyaValveStatus(t.enum8): + """Tuya Irrigation Valve status enum.""" + + Off = 0x00 + Auto = 0x01 + Disabled = 0x02 + + ( TuyaQuirkBuilder("_TZE200_81isopgh", "TS0601") .applies_to("_TZE200_1n2zev06", "TS0601") @@ -241,7 +262,7 @@ def giex_string_to_ts(v: str) -> int | None: device_class=SensorDeviceClass.DURATION, unit=UnitOfTime.SECONDS, translation_key="irrigation_duration", - fallback_name="Last irrigation duration", + fallback_name="Irrigation duration", ) .tuya_sensor( dp_id=101, @@ -291,6 +312,8 @@ def giex_string_to_ts(v: str) -> int | None: ) .add_to_registry() ) + + ( gx02_base_quirk.clone() .applies_to("_TZE200_a7sghmms", "TS0601") @@ -471,6 +494,156 @@ class GiexIrrigationStatus(t.enum8): ) +# NEO NAS-WV03B +( + TuyaQuirkBuilder("_TZE204_rzrrjkz2", "TS0601") + .applies_to("_TZE204_uab532m0", "TS0601") + .applies_to("_TZE204_z7a2jmyy", "TS0601") + .tuya_onoff(dp_id=1) + .tuya_enum( + dp_id=3, + attribute_name="valve_status", + enum_class=TuyaValveStatus, + entity_platform=EntityPlatform.SENSOR, + translation_key="valve_status", + fallback_name="Valve status", + ) + .tuya_number( + dp_id=5, + attribute_name="valve_countdown", + type=t.uint16_t, + device_class=SensorDeviceClass.DURATION, + unit=UnitOfTime.MINUTES, + min_value=1, + max_value=240, + step=1, + translation_key="valve_countdown", + fallback_name="Irrigation time", + ) + .tuya_sensor( + dp_id=6, + attribute_name="valve_duration", + type=t.uint32_t, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + unit=UnitOfTime.MINUTES, + entity_type=EntityType.STANDARD, + translation_key="valve_duration", + fallback_name="Irrigation duration", + ) + .tuya_dp( + dp_id=9, + ep_attribute=TuyaValveWaterConsumed.ep_attribute, + attribute_name=Metering.AttributeDefs.instantaneous_demand.name, + ) + .tuya_metering(dp_id=15, metering_cfg=TuyaValveWaterConsumed) + .tuya_battery(dp_id=11, battery_type=BatterySize.AA, battery_qty=2) + .tuya_binary_sensor( + dp_id=19, + attribute_name="valve_fault", + translation_key="valve_fault", + fallback_name="Valve fault", + ) + .tuya_enum( + dp_id=37, + attribute_name="weather_delay", + enum_class=TuyaValveWeatherDelayVar02, + translation_key="weather_delay", + fallback_name="Weather delay", + initially_disabled=True, + ) + # Charstring normal timed, dp 38 omitted + .tuya_switch( + dp_id=42, + attribute_name="switch_enabled", + entity_type=EntityType.STANDARD, + translation_key="switch_enabled", + fallback_name="Switch enabled", + ) + .tuya_sensor( + dp_id=47, + attribute_name="smart_irrigation", + type=t.uint16_t, + entity_type=EntityType.DIAGNOSTIC, + translation_key="smart_irrigation", + fallback_name="Smart irrigation", + ) + .tuya_switch( + dp_id=101, + attribute_name="total_flow_reset_switch", + entity_type=EntityType.STANDARD, + translation_key="total_flow_reset_switch", + fallback_name="Total flow reset switch", + ) + .tuya_sensor( + dp_id=102, + attribute_name="irrigation_duration", + type=t.uint32_t, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLUME, + unit=UnitOfVolume.LITERS, + translation_key="irrigation_duration", + fallback_name="Irrigation duration", + ) + .tuya_number( + dp_id=102, + attribute_name="quantitative_watering", + type=t.uint16_t, + unit=UnitOfVolume.LITERS, + device_class=SensorDeviceClass.VOLUME, + min_value=0, + max_value=10000, + step=1, + translation_key="quantitative_watering", + fallback_name="Quantitative watering", + ) + .tuya_binary_sensor( + dp_id=103, + attribute_name="flow_switch", + entity_type=EntityType.STANDARD, + translation_key="flow_switch", + fallback_name="Flow switch", + ) + .tuya_binary_sensor( + dp_id=104, + attribute_name="child_lock", + entity_type=EntityType.STANDARD, + translation_key="child_lock", + fallback_name="Child lock", + ) + .tuya_sensor( + dp_id=105, + attribute_name="surplus_flow", + type=t.uint16_t, + entity_type=EntityType.DIAGNOSTIC, + translation_key="surplus_flow", + fallback_name="Surplus flow", + ) + .tuya_sensor( + dp_id=106, + attribute_name="single_watering_duration", + type=t.CharacterString, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + unit=UnitOfTime.SECONDS, + translation_key="single_watering_duration", + fallback_name="Single watering duration", + ) + .tuya_sensor( + dp_id=108, + attribute_name="single_watering_amount", + type=t.CharacterString, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLUME, + unit=UnitOfVolume.LITERS, + translation_key="single_watering_amount", + fallback_name="Single watering amount", + ) + .skip_configuration() + .add_to_registry() +) + + # Tuya Rain Seer Valve ( TuyaQuirkBuilder("_TZ3210_0jxeoadc", "TS0049") From a156f8efd84c8a616a70289dec64c57d1b3aa972 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Thu, 23 Jan 2025 02:16:47 +0100 Subject: [PATCH 03/19] Add illuminance method to `TuyaQuirkBuilder` (#3718) Co-authored-by: TheJulianJES --- tests/test_tuya_builder.py | 2 + zhaquirks/tuya/builder/__init__.py | 28 +++++++++ zhaquirks/tuya/ts0601_motion.py | 93 +++++------------------------- zhaquirks/tuya/tuya_sensor.py | 11 +--- 4 files changed, 45 insertions(+), 89 deletions(-) diff --git a/tests/test_tuya_builder.py b/tests/test_tuya_builder.py index e82c023c65..425c7ac425 100644 --- a/tests/test_tuya_builder.py +++ b/tests/test_tuya_builder.py @@ -25,6 +25,7 @@ TuyaIasContact, TuyaIasFire, TuyaIasGas, + TuyaIlluminance, TuyaPM25Concentration, TuyaQuirkBuilder, TuyaRelativeHumidity, @@ -60,6 +61,7 @@ TuyaFormaldehydeConcentration, ), ("tuya_gas", "ias_zone", TuyaIasGas), + ("tuya_illuminance", "illuminance", TuyaIlluminance), ], ) async def test_convenience_methods(device_mock, method_name, attr_name, exp_class): diff --git a/zhaquirks/tuya/builder/__init__.py b/zhaquirks/tuya/builder/__init__.py index b4a1f132d7..5f64aef1f9 100644 --- a/zhaquirks/tuya/builder/__init__.py +++ b/zhaquirks/tuya/builder/__init__.py @@ -2,6 +2,7 @@ from collections.abc import Callable from enum import Enum +import math from typing import Any, Optional from zigpy.quirks import _DEVICE_REGISTRY @@ -18,6 +19,7 @@ PM25, CarbonDioxideConcentration, FormaldehydeConcentration, + IlluminanceMeasurement, RelativeHumidity, SoilMoisture, TemperatureMeasurement, @@ -161,6 +163,14 @@ class AttributeDefs(BaseAttributeDefs): ) +class TuyaIlluminance(IlluminanceMeasurement, TuyaLocalCluster): + """Tuya local illuminance cluster.""" + + _CONSTANT_ATTRIBUTES = { + IlluminanceMeasurement.AttributeDefs.light_sensor_type.id: IlluminanceMeasurement.LightSensorType.Photodiode + } + + class TuyaQuirkBuilder(QuirkBuilder): """Tuya QuirkBuilder.""" @@ -222,6 +232,24 @@ class TuyaPowerConfigurationClusterBattery(TuyaPowerConfigurationCluster): dp_id=dp_id, power_cfg=TuyaPowerConfigurationClusterBattery, scale=scale ) + def tuya_illuminance( + self, + dp_id: int, + illuminance_cfg: TuyaLocalCluster = TuyaIlluminance, + converter: Optional[Callable[[Any], Any]] = ( + lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0 + ), + ) -> QuirkBuilder: + """Add a Tuya Illuminance Configuration.""" + self.tuya_dp( + dp_id, + illuminance_cfg.ep_attribute, + IlluminanceMeasurement.AttributeDefs.measured_value.name, + converter=converter, + ) + self.adds(illuminance_cfg) + return self + def tuya_contact(self, dp_id: int): """Add a Tuya IAS contact sensor.""" self.tuya_ias( diff --git a/zhaquirks/tuya/ts0601_motion.py b/zhaquirks/tuya/ts0601_motion.py index b4df9dd800..e73937044a 100644 --- a/zhaquirks/tuya/ts0601_motion.py +++ b/zhaquirks/tuya/ts0601_motion.py @@ -1,7 +1,6 @@ """BlitzWolf IS-3/Tuya motion rechargeable occupancy sensor.""" import asyncio -import math from typing import Any from zigpy.quirks.v2 import EntityPlatform, EntityType @@ -9,17 +8,13 @@ from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass import zigpy.types as t -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing +from zigpy.zcl.clusters.measurement import OccupancySensing from zigpy.zcl.clusters.security import IasZone from zhaquirks.tuya import TuyaLocalCluster from zhaquirks.tuya.builder import TuyaQuirkBuilder -class TuyaIlluminanceCluster(IlluminanceMeasurement, TuyaLocalCluster): - """Tuya Illuminance cluster.""" - - class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster): """Tuya local OccupancySensing cluster.""" @@ -207,7 +202,6 @@ class TuyaSensitivityMode(t.enum8): translation_key="distance", fallback_name="Target distance", ) - .adds(TuyaIlluminanceCluster) .skip_configuration() ) @@ -241,12 +235,7 @@ class TuyaSensitivityMode(t.enum8): translation_key="presence_sensitivity", fallback_name="Presence sensitivity", ) - .tuya_dp( - dp_id=103, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) + .tuya_illuminance(dp_id=103) .tuya_number( dp_id=105, attribute_name="presence_timeout", @@ -315,12 +304,7 @@ class TuyaSensitivityMode(t.enum8): translation_key="presence_sensitivity", fallback_name="Presence sensitivity", ) - .tuya_dp( - dp_id=104, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) + .tuya_illuminance(dp_id=104) # 103 cli, z2m lists as not working .add_to_registry() ) @@ -363,12 +347,7 @@ class TuyaSensitivityMode(t.enum8): translation_key="fading_time", fallback_name="Fading time", ) - .tuya_dp( - dp_id=104, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) + .tuya_illuminance(dp_id=104) .add_to_registry() ) @@ -424,13 +403,7 @@ class TuyaSensitivityMode(t.enum8): translation_key="target_distance", fallback_name="Target distance", ) - .tuya_dp( - dp_id=102, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) - .adds(TuyaIlluminanceCluster) + .tuya_illuminance(dp_id=102) .tuya_number( dp_id=103, attribute_name="fading_time", @@ -510,13 +483,7 @@ class TuyaSensitivityMode(t.enum8): translation_key="target_distance", fallback_name="Target distance", ) - .tuya_dp( - dp_id=102, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) - .adds(TuyaIlluminanceCluster) + .tuya_illuminance(dp_id=102) .tuya_number( dp_id=103, attribute_name="hold_delay_time", @@ -686,12 +653,7 @@ class TuyaSensitivityMode(t.enum8): fallback_name="Fading time", ) # 103 cline, z2m doesn't expose - .tuya_dp( - dp_id=104, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) + .tuya_illuminance(dp_id=104) .tuya_number( dp_id=105, attribute_name="entry_sensitivity", @@ -937,12 +899,7 @@ class TuyaSensitivityMode(t.enum8): attribute_name=OccupancySensing.AttributeDefs.occupancy.name, converter=lambda x: x == 1, ) - .tuya_dp( - dp_id=101, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) + .tuya_illuminance(dp_id=101) .adds(TuyaOccupancySensing) .tuya_switch( dp_id=102, @@ -981,7 +938,6 @@ class TuyaSensitivityMode(t.enum8): translation_key="presence_timeout", fallback_name="Fade time", ) - .adds(TuyaIlluminanceCluster) .skip_configuration() .add_to_registry() ) @@ -1069,15 +1025,10 @@ class TuyaSensitivityMode(t.enum8): translation_key="work_mode", fallback_name="Work mode", ) - .tuya_dp( + .tuya_illuminance( dp_id=102, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: {0: 10, 1: 20, 2: 50, 3: 100}[ - x - ], # z2m has 10lux, 20lux, 50lux, then 24hrs? + converter=lambda x: {0: 10, 1: 20, 2: 50, 3: 100}[x], ) - .adds(TuyaIlluminanceCluster) .tuya_number( dp_id=103, attribute_name="output_time", @@ -1136,13 +1087,7 @@ class TuyaSensitivityMode(t.enum8): translation_key="fade_time", fallback_name="Fade time", ) - .tuya_dp( - dp_id=12, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) - .adds(TuyaIlluminanceCluster) + .tuya_illuminance(dp_id=12) .tuya_number( dp_id=105, attribute_name="illuminance_interval", @@ -1164,13 +1109,7 @@ class TuyaSensitivityMode(t.enum8): ( TuyaQuirkBuilder("_TZE204_sxm7l9xa", "TS0601") .applies_to("_TZE204_e5m9c5hl", "TS0601") - .tuya_dp( - dp_id=104, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) - .adds(TuyaIlluminanceCluster) + .tuya_illuminance(dp_id=104) .tuya_dp( dp_id=105, ep_attribute=TuyaOccupancySensing.ep_attribute, @@ -1271,13 +1210,7 @@ class TuyaSensitivityMode(t.enum8): translation_key="motion_sensitivity", fallback_name="Motion sensitivity", ) - .tuya_dp( - dp_id=12, - ep_attribute=TuyaIlluminanceCluster.ep_attribute, - attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name, - converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, - ) - .adds(TuyaIlluminanceCluster) + .tuya_illuminance(dp_id=12) .tuya_number( dp_id=101, attribute_name="illuminance_interval", diff --git a/zhaquirks/tuya/tuya_sensor.py b/zhaquirks/tuya/tuya_sensor.py index 0a19164acd..fbbdaa88c5 100644 --- a/zhaquirks/tuya/tuya_sensor.py +++ b/zhaquirks/tuya/tuya_sensor.py @@ -4,7 +4,7 @@ from zigpy.quirks.v2 import EntityPlatform, EntityType from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfTemperature, UnitOfTime -from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass +from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass import zigpy.types as t from zigpy.zcl import foundation @@ -304,14 +304,7 @@ class NoManufTimeTuyaMCUCluster(TuyaMCUCluster): ( TuyaQuirkBuilder("_TZE200_pay2byax", "TS0601") # Cusam ZG-102ZL .applies_to("_TZE200_n8dljorx", "TS0601") - .tuya_sensor( - dp_id=101, - attribute_name="measured_value", - type=t.uint16_t, - fallback_name="Illuminance", - device_class=SensorDeviceClass.ILLUMINANCE, - state_class=SensorStateClass.MEASUREMENT, - ) + .tuya_illuminance(dp_id=101) .tuya_contact(dp_id=1) .tuya_battery(dp_id=2) .skip_configuration() From fb4dcecd3b254443af9c77d4e19c64d29b5bae5b Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 02:20:21 +0100 Subject: [PATCH 04/19] Add lx unit to Tuya motion "illuminance threshold" (#3740) --- zhaquirks/tuya/ts0601_motion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhaquirks/tuya/ts0601_motion.py b/zhaquirks/tuya/ts0601_motion.py index e73937044a..8ec048ccb3 100644 --- a/zhaquirks/tuya/ts0601_motion.py +++ b/zhaquirks/tuya/ts0601_motion.py @@ -4,7 +4,7 @@ from typing import Any from zigpy.quirks.v2 import EntityPlatform, EntityType -from zigpy.quirks.v2.homeassistant import UnitOfLength, UnitOfTime +from zigpy.quirks.v2.homeassistant import LIGHT_LUX, UnitOfLength, UnitOfTime from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass import zigpy.types as t @@ -703,7 +703,7 @@ class TuyaSensitivityMode(t.enum8): attribute_name="illuminance_threshold", type=t.uint16_t, device_class=SensorDeviceClass.ILLUMINANCE, - # unit=LIGHT_LUX, # Not supported ZHA yet + unit=LIGHT_LUX, min_value=0, max_value=420, step=0.1, From c82ffed2a1ee72d311396d4d9428cff4a3736a45 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 02:22:03 +0100 Subject: [PATCH 05/19] Add % unit to Schneider TRV "display brightness" (#3739) --- zhaquirks/schneiderelectric/thermostat.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zhaquirks/schneiderelectric/thermostat.py b/zhaquirks/schneiderelectric/thermostat.py index 1e16cf640b..6a40a0b787 100644 --- a/zhaquirks/schneiderelectric/thermostat.py +++ b/zhaquirks/schneiderelectric/thermostat.py @@ -5,6 +5,7 @@ from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import EntityType, QuirkBuilder from zigpy.quirks.v2.homeassistant import ( + PERCENTAGE, EntityPlatform, UnitOfPower, UnitOfTemperature, @@ -597,7 +598,7 @@ class AttributeDefs(CustomCluster.AttributeDefs): attribute_name=SEUserInterface.AttributeDefs.se_brightness.name, translation_key="display_brightness", fallback_name="Display brightness", - # unit="%", + unit=PERCENTAGE, min_value=0, max_value=100, step=1, @@ -608,7 +609,7 @@ class AttributeDefs(CustomCluster.AttributeDefs): attribute_name=SEUserInterface.AttributeDefs.se_inactive_brightness.name, translation_key="display_inactive_brightness", fallback_name="Display inactive brightness", - # unit="%", + unit=PERCENTAGE, min_value=0, max_value=100, step=1, From eaa46acd5714db55574037ff2a96e302d642a252 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 03:31:00 +0100 Subject: [PATCH 06/19] Add 2 empty lines between v2 quirk definitions (#3742) --- zhaquirks/nimly/lock.py | 1 + zhaquirks/nodon/pilot_wire.py | 2 ++ zhaquirks/schneiderelectric/dimmers.py | 1 + 3 files changed, 4 insertions(+) diff --git a/zhaquirks/nimly/lock.py b/zhaquirks/nimly/lock.py index 6e762b2f81..b65be79042 100644 --- a/zhaquirks/nimly/lock.py +++ b/zhaquirks/nimly/lock.py @@ -33,6 +33,7 @@ .add_to_registry() ) + ( QuirkBuilder(NIMLY, "NimlyPRO") .applies_to(NIMLY, "NimlyCode") diff --git a/zhaquirks/nodon/pilot_wire.py b/zhaquirks/nodon/pilot_wire.py index c0a6403e5b..c05ebe9685 100644 --- a/zhaquirks/nodon/pilot_wire.py +++ b/zhaquirks/nodon/pilot_wire.py @@ -58,11 +58,13 @@ class AdeoPilotWireCluster(NodOnPilotWireCluster): .replaces(NodOnPilotWireCluster) ) # fmt: skip + adeo = ( nodon.clone(omit_man_model_data=True) .applies_to(ADEO, "SIN-4-FP-21_EQU") .replaces(AdeoPilotWireCluster) ) + nodon.add_to_registry() adeo.add_to_registry() diff --git a/zhaquirks/schneiderelectric/dimmers.py b/zhaquirks/schneiderelectric/dimmers.py index 4621b1426a..098a113740 100644 --- a/zhaquirks/schneiderelectric/dimmers.py +++ b/zhaquirks/schneiderelectric/dimmers.py @@ -23,6 +23,7 @@ .add_to_registry() ) + ( QuirkBuilder(SE_MANUF_NAME, "NHPB/SWITCH/1") .replaces(SEBasic) From eabffb261647dcfd21851cec0985ebfe76151a80 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 03:33:13 +0100 Subject: [PATCH 07/19] Replace deprecated `dev.handle_message` usage in tests (#3746) --- tests/test_inovelli_blue.py | 22 ++++++- tests/test_konke.py | 31 +++++++++- tests/test_linxura.py | 11 +++- tests/test_tuya_air.py | 21 +++++-- tests/test_xbee.py | 85 ++++++++++++++++---------- tests/test_xiaomi.py | 118 ++++++++++++++++++++++-------------- 6 files changed, 200 insertions(+), 88 deletions(-) diff --git a/tests/test_inovelli_blue.py b/tests/test_inovelli_blue.py index 1b81f40fe3..f7f8e45cf8 100644 --- a/tests/test_inovelli_blue.py +++ b/tests/test_inovelli_blue.py @@ -3,6 +3,8 @@ from unittest import mock from unittest.mock import MagicMock +import zigpy.types as t + import zhaquirks from zhaquirks.inovelli.VZM31SN import InovelliVZM31SNv11 @@ -25,7 +27,15 @@ class Listener: cluster_listener ) - device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data) + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster_id, + src_ep=endpoint_id, + dst_ep=endpoint_id, + data=t.SerializableBytes(data), + ) + ) assert cluster_listener.zha_send_event.call_count == 1 assert cluster_listener.zha_send_event.call_args == mock.call( @@ -35,8 +45,14 @@ class Listener: cluster_listener.zha_send_event.reset_mock() led_effect_complete_data = b"\x15/\x12\x0c$\x10" - device.handle_message( - 260, cluster_id, endpoint_id, endpoint_id, led_effect_complete_data + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster_id, + src_ep=endpoint_id, + dst_ep=endpoint_id, + data=t.SerializableBytes(led_effect_complete_data), + ) ) assert cluster_listener.zha_send_event.call_count == 1 diff --git a/tests/test_konke.py b/tests/test_konke.py index 00d90f09f9..f1eb4f6eb6 100644 --- a/tests/test_konke.py +++ b/tests/test_konke.py @@ -4,6 +4,7 @@ from unittest import mock import pytest +import zigpy.types as t from tests.common import ZCL_IAS_MOTION_COMMAND, ClusterListener import zhaquirks @@ -83,7 +84,15 @@ async def test_konke_button(zigpy_device_from_quirk, quirk): # single press message = b"\x08W\n\x00\x00\x10\x80" - device.handle_message(260, cluster.cluster_id, 1, 1, message) + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster.cluster_id, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(message), + ) + ) assert listener.zha_send_event.call_count == 1 assert listener.zha_send_event.call_args_list[0][0][0] == COMMAND_SINGLE assert listener.zha_send_event.call_args_list[0][0][1][PRESS_TYPE] == COMMAND_SINGLE @@ -92,7 +101,15 @@ async def test_konke_button(zigpy_device_from_quirk, quirk): # double press listener.reset_mock() message = b"\x08X\n\x00\x00\x10\x81" - device.handle_message(260, cluster.cluster_id, 1, 1, message) + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster.cluster_id, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(message), + ) + ) assert listener.zha_send_event.call_count == 1 assert listener.zha_send_event.call_args_list[0][0][0] == COMMAND_DOUBLE assert listener.zha_send_event.call_args_list[0][0][1][PRESS_TYPE] == COMMAND_DOUBLE @@ -101,7 +118,15 @@ async def test_konke_button(zigpy_device_from_quirk, quirk): # long press listener.reset_mock() message = b"\x08Y\n\x00\x00\x10\x82" - device.handle_message(260, cluster.cluster_id, 1, 1, message) + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster.cluster_id, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(message), + ) + ) assert listener.zha_send_event.call_count == 1 assert listener.zha_send_event.call_args_list[0][0][0] == COMMAND_HOLD assert listener.zha_send_event.call_args_list[0][0][1][PRESS_TYPE] == COMMAND_HOLD diff --git a/tests/test_linxura.py b/tests/test_linxura.py index 0ac867dc54..e4fd778113 100644 --- a/tests/test_linxura.py +++ b/tests/test_linxura.py @@ -3,6 +3,7 @@ from unittest import mock import pytest +import zigpy.types as t from zigpy.zcl.clusters.security import IasZone import zhaquirks @@ -105,7 +106,15 @@ async def test_button_triggers(zigpy_device_from_quirk, message, button, press_t listener = mock.MagicMock() cluster.add_listener(listener) - device.handle_message(260, cluster.cluster_id, 1, 1, message) + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=cluster.cluster_id, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(message), + ) + ) assert listener.zha_send_event.call_count == 1 assert listener.zha_send_event.call_args == mock.call( f"{button}_{press_type}", diff --git a/tests/test_tuya_air.py b/tests/test_tuya_air.py index 999cafcc0d..a12d535330 100644 --- a/tests/test_tuya_air.py +++ b/tests/test_tuya_air.py @@ -5,6 +5,7 @@ import pytest import zigpy.profiles.zha +import zigpy.types as t import zhaquirks from zhaquirks.tuya import TuyaNewManufCluster @@ -65,8 +66,14 @@ def air_quality_device(zigpy_device_from_v2_quirk): def test_co2_sensor(air_quality_device, data, ep_attr, expected_value): """Test Tuya Air Quality Sensor.""" - air_quality_device.handle_message( - zigpy.profiles.zha.PROFILE_ID, TuyaNewManufCluster.cluster_id, 1, 1, data + air_quality_device.packet_received( + t.ZigbeePacket( + profile_id=zigpy.profiles.zha.PROFILE_ID, + cluster_id=TuyaNewManufCluster.cluster_id, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(data), + ) ) cluster = getattr(air_quality_device.endpoints[1], ep_attr) assert cluster.get("measured_value") == expected_value @@ -132,8 +139,14 @@ def smart_air_quality_device(zigpy_device_from_v2_quirk): def test_smart_air_sensor(smart_air_quality_device, data, ep_attr, expected_value): """Test Tuya Smart Air Sensor.""" - smart_air_quality_device.handle_message( - zigpy.profiles.zha.PROFILE_ID, TuyaNewManufCluster.cluster_id, 1, 1, data + smart_air_quality_device.packet_received( + t.ZigbeePacket( + profile_id=zigpy.profiles.zha.PROFILE_ID, + cluster_id=TuyaNewManufCluster.cluster_id, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(data), + ) ) cluster = getattr(smart_air_quality_device.endpoints[1], ep_attr) assert cluster.get("measured_value") == expected_value diff --git a/tests/test_xbee.py b/tests/test_xbee.py index 8e5c7cc666..4c83f5299b 100644 --- a/tests/test_xbee.py +++ b/tests/test_xbee.py @@ -3,6 +3,7 @@ from unittest import mock import pytest +import zigpy.types as t from zigpy.zcl import foundation from zigpy.zcl.clusters.general import AnalogOutput, Basic, LevelControl, OnOff @@ -177,12 +178,14 @@ async def test_receive_serial_data(zigpy_device_from_quirk): ].add_listener(listener) # Receive serial data - xbee3_device.handle_message( - XBEE_PROFILE_ID, - XBEE_DATA_CLUSTER, - XBEE_DATA_ENDPOINT, - XBEE_DATA_ENDPOINT, - b"Test UART data", + xbee3_device.packet_received( + t.ZigbeePacket( + profile_id=XBEE_PROFILE_ID, + cluster_id=XBEE_DATA_CLUSTER, + src_ep=XBEE_DATA_ENDPOINT, + dst_ep=XBEE_DATA_ENDPOINT, + data=t.SerializableBytes(b"Test UART data"), + ) ) listener.zha_send_event.assert_called_once_with( @@ -350,12 +353,14 @@ async def test_remote_at_non_native( def mock_at_response(*args, **kwargs): """Simulate remote AT command response from device.""" - xbee3_device.handle_message( - XBEE_PROFILE_ID, - XBEE_AT_RESPONSE_CLUSTER, - XBEE_AT_ENDPOINT, - XBEE_AT_ENDPOINT, - response_data, + xbee3_device.packet_received( + t.ZigbeePacket( + profile_id=XBEE_PROFILE_ID, + cluster_id=XBEE_AT_RESPONSE_CLUSTER, + src_ep=XBEE_AT_ENDPOINT, + dst_ep=XBEE_AT_ENDPOINT, + data=t.SerializableBytes(response_data), + ) ) return mock.DEFAULT @@ -477,12 +482,14 @@ async def test_remote_at_tx_failure(zigpy_device_from_quirk): def mock_at_response(*args, **kwargs): """Simulate remote AT command response from device.""" - xbee3_device.handle_message( - XBEE_PROFILE_ID, - XBEE_AT_RESPONSE_CLUSTER, - XBEE_AT_ENDPOINT, - XBEE_AT_ENDPOINT, - b"\x01TP\x04", + xbee3_device.packet_received( + t.ZigbeePacket( + profile_id=XBEE_PROFILE_ID, + cluster_id=XBEE_AT_RESPONSE_CLUSTER, + src_ep=XBEE_AT_ENDPOINT, + dst_ep=XBEE_AT_ENDPOINT, + data=t.SerializableBytes(b"\x01TP\x04"), + ) ) return mock.DEFAULT @@ -515,12 +522,16 @@ async def test_io_sample_report(zigpy_device_from_quirk): ] # {'digital_samples': [1, None, 0, None, 1, None, 0, None, 1, None, 0, None, 1, None, 0], 'analog_samples': [341, None, 682, None, None, None, None, 3305]} - xbee3_device.handle_message( - XBEE_PROFILE_ID, - XBEE_IO_CLUSTER, - XBEE_DATA_ENDPOINT, - XBEE_DATA_ENDPOINT, - b"\x01\x55\x55\x85\x11\x11\x01\x55\x02\xaa\x0c\xe9", + xbee3_device.packet_received( + t.ZigbeePacket( + profile_id=XBEE_PROFILE_ID, + cluster_id=XBEE_IO_CLUSTER, + src_ep=XBEE_DATA_ENDPOINT, + dst_ep=XBEE_DATA_ENDPOINT, + data=t.SerializableBytes( + b"\x01\x55\x55\x85\x11\x11\x01\x55\x02\xaa\x0c\xe9" + ), + ) ) for i in range(len(digital_listeners)): @@ -564,12 +575,16 @@ async def test_io_sample_report_on_at_response(zigpy_device_from_quirk): def mock_at_response(*args, **kwargs): """Simulate remote AT command response from device.""" - xbee_device.handle_message( - XBEE_PROFILE_ID, - XBEE_AT_RESPONSE_CLUSTER, - XBEE_AT_ENDPOINT, - XBEE_AT_ENDPOINT, - b"\x01IS\x00\x01\x55\x55\x85\x11\x11\x01\x55\x02\xaa\x0c\xe9", + xbee_device.packet_received( + t.ZigbeePacket( + profile_id=XBEE_PROFILE_ID, + cluster_id=XBEE_AT_RESPONSE_CLUSTER, + src_ep=XBEE_AT_ENDPOINT, + dst_ep=XBEE_AT_ENDPOINT, + data=t.SerializableBytes( + b"\x01IS\x00\x01\x55\x55\x85\x11\x11\x01\x55\x02\xaa\x0c\xe9" + ), + ) ) return mock.DEFAULT @@ -649,7 +664,15 @@ async def test_zdo(handle_mgmt_lqi_resp, zigpy_device_from_quirk): xbee3_device = zigpy_device_from_quirk(XBee3Sensor) # Receive ZDOCmd.IEEE_addr_req - xbee3_device.handle_message(0, 0x0001, 0, 0, b"\x07\x34\x12\x00\x00") + xbee3_device.packet_received( + t.ZigbeePacket( + profile_id=0, + cluster_id=0x01, + src_ep=0, + dst_ep=0, + data=t.SerializableBytes(b"\x07\x34\x12\x00\x00"), + ) + ) assert handle_mgmt_lqi_resp.call_count == 1 assert len(handle_mgmt_lqi_resp.call_args_list[0][0]) == 4 diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 811a66746a..a6f4454b91 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -336,8 +336,14 @@ async def test_xiaomi_battery(zigpy_device_from_quirk, voltage, bpr): ) device = zigpy_device_from_quirk(zhaquirks.xiaomi.aqara.vibration_aq1.VibrationAQ1) - device.handle_message( - 0x260, 0x0000, 1, 1, data_1 + t.uint16_t(voltage).serialize() + data_2 + device.packet_received( + t.ZigbeePacket( + profile_id=0x260, + cluster_id=0x0000, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(data_1 + t.uint16_t(voltage).serialize() + data_2), + ) ) power_cluster = device.endpoints[1].power assert power_cluster["battery_percentage_remaining"] == bpr @@ -362,8 +368,14 @@ async def test_mija_battery(zigpy_device_from_quirk, voltage, bpr): data_2 = b"!\xa8\x01$\x00\x00\x00\x00\x00!n\x00 P" device = zigpy_device_from_quirk(zhaquirks.xiaomi.mija.motion.Motion) - device.handle_message( - 0x260, 0x0000, 1, 1, data_1 + t.uint16_t(voltage).serialize() + data_2 + device.packet_received( + t.ZigbeePacket( + profile_id=0x260, + cluster_id=0x0000, + src_ep=1, + dst_ep=1, + data=t.SerializableBytes(data_1 + t.uint16_t(voltage).serialize() + data_2), + ) ) power_cluster = device.endpoints[1].power assert power_cluster["battery_percentage_remaining"] == bpr @@ -764,12 +776,14 @@ class Listener: cluster_listener = Listener() opple_cluster.add_listener(cluster_listener) - device.handle_message( - 260, - opple_cluster.cluster_id, - opple_cluster.endpoint.endpoint_id, - opple_cluster.endpoint.endpoint_id, - bytes_received, + device.packet_received( + t.ZigbeePacket( + profile_id=0x260, + cluster_id=opple_cluster.cluster_id, + src_ep=opple_cluster.endpoint.endpoint_id, + dst_ep=opple_cluster.endpoint.endpoint_id, + data=t.SerializableBytes(bytes_received), + ) ) assert cluster_listener.attribute_updated.call_count == call_count @@ -840,12 +854,14 @@ async def test_aqara_smoke_sensor_xiaomi_attribute_report( ias_cluster = device.endpoints[1].ias_zone ias_listener = ClusterListener(ias_cluster) - device.handle_message( - 260, - opple_cluster.cluster_id, - opple_cluster.endpoint.endpoint_id, - opple_cluster.endpoint.endpoint_id, - raw_report, + device.packet_received( + t.ZigbeePacket( + profile_id=0x260, + cluster_id=opple_cluster.cluster_id, + src_ep=opple_cluster.endpoint.endpoint_id, + dst_ep=opple_cluster.endpoint.endpoint_id, + data=t.SerializableBytes(raw_report), + ) ) # check that Xiaomi attribute report also updates attribute cache @@ -1264,12 +1280,14 @@ async def test_xiaomi_weather( PowerConfiguration.AttributeDefs.battery_percentage_remaining.id ) - device.handle_message( - 260, - xiaomi_attr_cluster.cluster_id, - xiaomi_attr_cluster.endpoint.endpoint_id, - xiaomi_attr_cluster.endpoint.endpoint_id, - raw_report, + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=xiaomi_attr_cluster.cluster_id, + src_ep=xiaomi_attr_cluster.endpoint.endpoint_id, + dst_ep=xiaomi_attr_cluster.endpoint.endpoint_id, + data=t.SerializableBytes(raw_report), + ) ) assert len(temperature_listener.attribute_updates) == 1 @@ -1331,12 +1349,14 @@ async def test_xiaomi_motion_sensor_misc( PowerConfiguration.AttributeDefs.battery_percentage_remaining.id ) - device.handle_message( - 260, - basic_cluster.cluster_id, - basic_cluster.endpoint.endpoint_id, - basic_cluster.endpoint.endpoint_id, - raw_report, + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=basic_cluster.cluster_id, + src_ep=basic_cluster.endpoint.endpoint_id, + dst_ep=basic_cluster.endpoint.endpoint_id, + data=t.SerializableBytes(raw_report), + ) ) assert len(device_temperature_listener.attribute_updates) == 1 @@ -1451,12 +1471,14 @@ async def test_xiaomi_t1_door_sensor( on_off_listener = ClusterListener(on_off_cluster) # check open state - device.handle_message( - 260, - on_off_cluster.cluster_id, - on_off_cluster.endpoint.endpoint_id, - on_off_cluster.endpoint.endpoint_id, - bytes.fromhex("185D0A00001001"), + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=on_off_cluster.cluster_id, + src_ep=on_off_cluster.endpoint.endpoint_id, + dst_ep=on_off_cluster.endpoint.endpoint_id, + data=t.SerializableBytes(bytes.fromhex("185D0A00001001")), + ) ) assert len(on_off_listener.attribute_updates) == 1 @@ -1464,12 +1486,14 @@ async def test_xiaomi_t1_door_sensor( assert on_off_listener.attribute_updates[0][1] == t.Bool.true # check closed state - device.handle_message( - 260, - on_off_cluster.cluster_id, - on_off_cluster.endpoint.endpoint_id, - on_off_cluster.endpoint.endpoint_id, - bytes.fromhex("18640A00001000"), + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=on_off_cluster.cluster_id, + src_ep=on_off_cluster.endpoint.endpoint_id, + dst_ep=on_off_cluster.endpoint.endpoint_id, + data=t.SerializableBytes(bytes.fromhex("185D0A00001000")), + ) ) assert len(on_off_listener.attribute_updates) == 2 @@ -1488,12 +1512,14 @@ async def test_xiaomi_t1_door_sensor( ) # check Xiaomi attribute report - device.handle_message( - 260, - opple_cluster.cluster_id, - opple_cluster.endpoint.endpoint_id, - opple_cluster.endpoint.endpoint_id, - raw_report, + device.packet_received( + t.ZigbeePacket( + profile_id=260, + cluster_id=opple_cluster.cluster_id, + src_ep=opple_cluster.endpoint.endpoint_id, + dst_ep=opple_cluster.endpoint.endpoint_id, + data=t.SerializableBytes(raw_report), + ) ) assert len(power_listener.attribute_updates) == 2 From 1b6964f14185a85e3966ad43a20947610745f391 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 03:44:24 +0100 Subject: [PATCH 08/19] Update GitHub stale action to v9 (#3754) --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 976a847821..08c8b3d843 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 180 days stale issues & PRs policy - uses: actions/stale@v8 + uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 180 @@ -49,7 +49,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v8 + uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs more information" From dec4339ddcebd888093d06501961b634a6fd7714 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 03:51:49 +0100 Subject: [PATCH 09/19] Only run GitHub stale action once per day (#3755) --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 08c8b3d843..16c84a2807 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,7 +3,7 @@ name: Stale # yamllint disable-line rule:truthy on: schedule: - - cron: "0 * * * *" + - cron: "30 1 * * *" workflow_dispatch: jobs: From 49621a63be759b31687ef70b96279742e8efc4b1 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 04:09:27 +0100 Subject: [PATCH 10/19] Bump zigpy to 0.75.0 (#3753) * Bump zigpy to 0.75.0 * Use new quirks registry attribute name * Access using `registry_v1` property * Access using `registry_v2` property * Access using `DEVICE_REGISTRY` --- pyproject.toml | 2 +- requirements_test.txt | 2 +- tests/test_quirks.py | 2 +- tests/test_quirks_v2.py | 2 +- tests/test_tuya_spells.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bd97df93ca..ccd15c296e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ readme = "README.md" license = {text = "Apache-2.0"} requires-python = ">=3.12" dependencies = [ - "zigpy>=0.74.0", + "zigpy>=0.75.0", ] [tool.setuptools.packages.find] diff --git a/requirements_test.txt b/requirements_test.txt index 417a0ce190..7cef89a8d5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-sugar pytest-timeout pytest-asyncio pytest>=7.1.3 -zigpy>=0.74.0 +zigpy>=0.75.0 ruff==0.0.261 diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 721cbc986a..5c25654ca6 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -60,7 +60,7 @@ zhaquirks.setup() ALL_QUIRK_CLASSES = [] -for manufacturer in zq._DEVICE_REGISTRY._registry.values(): +for manufacturer in zq.DEVICE_REGISTRY.registry_v1.values(): for model_quirk_list in manufacturer.values(): for quirk in model_quirk_list: if quirk in ALL_QUIRK_CLASSES: diff --git a/tests/test_quirks_v2.py b/tests/test_quirks_v2.py index 103cd36084..7af17f4f57 100644 --- a/tests/test_quirks_v2.py +++ b/tests/test_quirks_v2.py @@ -12,7 +12,7 @@ ALL_QUIRK_V2_CLASSES: list[QuirksV2RegistryEntry] = itertools.chain.from_iterable( - zigpy.quirks._DEVICE_REGISTRY._registry_v2.values() + zigpy.quirks.DEVICE_REGISTRY.registry_v2.values() ) diff --git a/tests/test_tuya_spells.py b/tests/test_tuya_spells.py index eafac8bd85..08ab1676a9 100644 --- a/tests/test_tuya_spells.py +++ b/tests/test_tuya_spells.py @@ -65,7 +65,7 @@ class TuyaTestSpellDevice(EnchantedDevice): ENCHANTED_QUIRKS = [TuyaTestSpellDevice] -for manufacturer in zigpy.quirks._DEVICE_REGISTRY._registry.values(): +for manufacturer in zigpy.quirks.DEVICE_REGISTRY.registry_v1.values(): for model_quirk_list in manufacturer.values(): for quirk_entry in model_quirk_list: if quirk_entry in ENCHANTED_QUIRKS: From c00eb0139d48da1f3d5b92ec18b3135d131541ef Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 05:02:14 +0100 Subject: [PATCH 11/19] Fix `ALL_QUIRK_V2_CLASSES` in tests (#3756) --- tests/test_quirks_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_quirks_v2.py b/tests/test_quirks_v2.py index 7af17f4f57..7423226ccc 100644 --- a/tests/test_quirks_v2.py +++ b/tests/test_quirks_v2.py @@ -11,8 +11,8 @@ zhaquirks.setup() -ALL_QUIRK_V2_CLASSES: list[QuirksV2RegistryEntry] = itertools.chain.from_iterable( - zigpy.quirks.DEVICE_REGISTRY.registry_v2.values() +ALL_QUIRK_V2_CLASSES: list[QuirksV2RegistryEntry] = list( + itertools.chain.from_iterable(zigpy.quirks.DEVICE_REGISTRY.registry_v2.values()) ) From d900f4bd65cf6a6278672410888dc730b8fdbacd Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 05:41:06 +0100 Subject: [PATCH 12/19] Fix `TuyaQuirkBuilder` `quirk_file` location (#3757) --- zhaquirks/tuya/builder/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/zhaquirks/tuya/builder/__init__.py b/zhaquirks/tuya/builder/__init__.py index 5f64aef1f9..4d1310d746 100644 --- a/zhaquirks/tuya/builder/__init__.py +++ b/zhaquirks/tuya/builder/__init__.py @@ -2,7 +2,10 @@ from collections.abc import Callable from enum import Enum +import inspect import math +import pathlib +from types import FrameType from typing import Any, Optional from zigpy.quirks import _DEVICE_REGISTRY @@ -185,6 +188,12 @@ def __init__( self.tuya_dp_to_attribute: dict[int, DPToAttributeMapping] = {} self.new_attributes: set[foundation.ZCLAttributeDef] = set() super().__init__(manufacturer, model, registry) + # quirk_file will point to the init call above if called from this QuirkBuilder, + # so we need to re-set it correctly + current_frame: FrameType = inspect.currentframe() + caller: FrameType = current_frame.f_back + self.quirk_file = pathlib.Path(caller.f_code.co_filename) + self.quirk_file_line = caller.f_lineno def _tuya_battery( self, From 0ca585f9dee7c234d599c8d01be11edebf28340f Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 06:23:24 +0100 Subject: [PATCH 13/19] Fix duplicate manufacturer/model in Tuya temp sensor (#3759) --- zhaquirks/tuya/tuya_sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhaquirks/tuya/tuya_sensor.py b/zhaquirks/tuya/tuya_sensor.py index fbbdaa88c5..1a97b4b897 100644 --- a/zhaquirks/tuya/tuya_sensor.py +++ b/zhaquirks/tuya/tuya_sensor.py @@ -256,7 +256,6 @@ class NoManufTimeTuyaMCUCluster(TuyaMCUCluster): TuyaQuirkBuilder("_TZE200_yjjdcqsq", "TS0601") .applies_to("_TZE200_9yapgbuv", "TS0601") .applies_to("_TZE204_9yapgbuv", "TS0601") - .applies_to("_TZE204_yjjdcqsq", "TS0601") .applies_to("_TZE200_utkemkbs", "TS0601") .applies_to("_TZE204_utkemkbs", "TS0601") .applies_to("_TZE204_yjjdcqsq", "TS0601") From 2aad9e17c9dacceca82eed6e150143d10fc8c367 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 06:35:28 +0100 Subject: [PATCH 14/19] Fix Tuya siren manufacturer/model `_TZE204_hcxvyxa5` (#3760) --- zhaquirks/tuya/tuya_siren.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/zhaquirks/tuya/tuya_siren.py b/zhaquirks/tuya/tuya_siren.py index 7069bbd233..f4246d7646 100644 --- a/zhaquirks/tuya/tuya_siren.py +++ b/zhaquirks/tuya/tuya_siren.py @@ -359,9 +359,7 @@ class NeoBatteryState(t.enum8): # Tuya ZA03 ( - TuyaQuirkBuilder("_TZE200_t1blo2bj", "TS0601") - .applies_to("_TZE204_t1blo2bj", "TS0601") - .applies_to("_TZE204_q76rtoa9", "TS0601") + TuyaQuirkBuilder("_TZE204_hcxvyxa5", "TS0601") .tuya_enum( dp_id=5, attribute_name="alarm_volume", From c52aee365ca8cc2bc81653d20ffe84ae66c7fc33 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 06:38:51 +0100 Subject: [PATCH 15/19] Test manufacturer/model are unique across v2 quirks (#3758) --- tests/test_quirks_v2.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_quirks_v2.py b/tests/test_quirks_v2.py index 7423226ccc..1671f060bc 100644 --- a/tests/test_quirks_v2.py +++ b/tests/test_quirks_v2.py @@ -10,8 +10,9 @@ zhaquirks.setup() - -ALL_QUIRK_V2_CLASSES: list[QuirksV2RegistryEntry] = list( +# zigpy registry v2 contains duplicates (due to being keyed by manufacturer and model), +# so to avoid duplicates but maintain insertion order, we use a dict instead of a set +ALL_QUIRK_V2_CLASSES: dict[QuirksV2RegistryEntry, None] = dict.fromkeys( itertools.chain.from_iterable(zigpy.quirks.DEVICE_REGISTRY.registry_v2.values()) ) @@ -46,3 +47,26 @@ def test_translation_key_and_fallback_name_match() -> None: assert ( len(set(fallback_names)) == 1 ), f"Translation key '{translation_key}' is shared by quirks with different fallback names: {quirk_locations}" + + +def test_manufacturer_model_metadata_unique() -> None: + """Ensure that each manufacturer-model pair is unique across all v2 quirks.""" + # quirk_locations are a list and not a set below, + # as they are not guaranteed to be unique when set up incorrectly + + # (manufacturer, model) -> {quirk_location} + man_model_quirk_map: dict[tuple[str, str], list[str]] = collections.defaultdict( + list + ) + + for quirk in ALL_QUIRK_V2_CLASSES: + for metadata in quirk.manufacturer_model_metadata: + man_model_quirk_map[(metadata.manufacturer, metadata.model)].append( + f"{quirk.quirk_file}:{quirk.quirk_file_line}" + ) + + # check that each manufacturer-model pair is unique + for (manufacturer, model), quirk_locations in man_model_quirk_map.items(): + assert ( + len(quirk_locations) == 1 + ), f"Manufacturer-model pair '{manufacturer}' '{model}' is shared by multiple quirks: {quirk_locations}" From b15a6dc4a63061d9c3d7075002b0fd465f70f020 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 07:04:11 +0100 Subject: [PATCH 16/19] Fix Hue remote test not marked as `async` (#3761) --- tests/test_philips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_philips.py b/tests/test_philips.py index da2069703b..4610e331da 100644 --- a/tests/test_philips.py +++ b/tests/test_philips.py @@ -628,7 +628,7 @@ def test_PhilipsRemoteCluster_long_press( (4), ), ) -def test_ButtonPressQueue_presses_without_pause(button_presses): +async def test_ButtonPressQueue_presses_without_pause(button_presses): """Test ButtonPressQueue presses without pause in between presses.""" q = ButtonPressQueue() From 4f64c181d6a98c7c733bccfb6a7e52e47ad9f146 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 07:54:22 +0100 Subject: [PATCH 17/19] Fix CI badge in README (#3762) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e55ad09ed..99e3c6fbbe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ZHA Device Handlers For Home Assistant -![CI](https://github.com/zigpy/zha-device-handlers/workflows/CI/badge.svg?branch=dev) +[![CI](https://github.com/zigpy/zha-device-handlers/actions/workflows/ci.yml/badge.svg)](https://github.com/zigpy/zha-device-handlers/actions/workflows/ci.yml) [![Coverage Status](https://codecov.io/gh/zigpy/zha-device-handlers/branch/dev/graph/badge.svg)](https://app.codecov.io/gh/zigpy/zha-device-handlers/tree/dev) ZHA Device Handlers are custom quirks implementations for [Zigpy](https://github.com/zigpy/zigpy), the library that provides the [Zigbee](http://www.zigbee.org) support for the [ZHA](https://www.home-assistant.io/components/zha/) component in [Home Assistant](https://www.home-assistant.io). From e349a0dc29ab067120e05ef0a39ad488b5f66dc9 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 23 Jan 2025 21:51:10 +0100 Subject: [PATCH 18/19] Bump zigpy to 0.75.1 (#3766) --- pyproject.toml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ccd15c296e..6636cb6280 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ readme = "README.md" license = {text = "Apache-2.0"} requires-python = ">=3.12" dependencies = [ - "zigpy>=0.75.0", + "zigpy>=0.75.1", ] [tool.setuptools.packages.find] diff --git a/requirements_test.txt b/requirements_test.txt index 7cef89a8d5..61cd54f6e9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-sugar pytest-timeout pytest-asyncio pytest>=7.1.3 -zigpy>=0.75.0 +zigpy>=0.75.1 ruff==0.0.261 From d9481d36a8483810696fe1c0e796895b92d6479b Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 24 Jan 2025 04:07:07 +0100 Subject: [PATCH 19/19] Add `BatterySize` enum (#3752) --- tests/test_tuya_builder.py | 6 ++++- tests/test_xiaomi.py | 34 ++++++++++++++++++++++--- zhaquirks/const.py | 19 ++++++++++++++ zhaquirks/ikea/__init__.py | 11 ++++---- zhaquirks/tuya/__init__.py | 9 ++----- zhaquirks/tuya/builder/__init__.py | 5 +++- zhaquirks/tuya/tuya_gas.py | 2 +- zhaquirks/tuya/tuya_sensor.py | 3 ++- zhaquirks/tuya/tuya_siren.py | 2 +- zhaquirks/tuya/tuya_valve.py | 2 +- zhaquirks/xiaomi/__init__.py | 7 +++-- zhaquirks/xiaomi/aqara/cube.py | 3 ++- zhaquirks/xiaomi/aqara/cube_aqgl01.py | 5 ++-- zhaquirks/xiaomi/aqara/magnet_ac01.py | 3 ++- zhaquirks/xiaomi/aqara/magnet_acn001.py | 3 ++- zhaquirks/xiaomi/aqara/magnet_agl02.py | 3 ++- zhaquirks/xiaomi/aqara/magnet_aq2.py | 3 ++- zhaquirks/xiaomi/aqara/motion_ac02.py | 3 ++- zhaquirks/xiaomi/aqara/motion_acn001.py | 3 ++- zhaquirks/xiaomi/aqara/motion_agl02.py | 3 ++- zhaquirks/xiaomi/aqara/motion_agl04.py | 3 ++- zhaquirks/xiaomi/aqara/motion_aq2.py | 3 ++- zhaquirks/xiaomi/aqara/motion_aq2b.py | 3 ++- zhaquirks/xiaomi/mija/motion.py | 3 ++- zhaquirks/xiaomi/mija/sensor_magnet.py | 3 ++- zhaquirks/xiaomi/mija/sensor_switch.py | 3 ++- zhaquirks/xiaomi/mija/smoke.py | 3 ++- 27 files changed, 109 insertions(+), 41 deletions(-) diff --git a/tests/test_tuya_builder.py b/tests/test_tuya_builder.py index 425c7ac425..10ad800838 100644 --- a/tests/test_tuya_builder.py +++ b/tests/test_tuya_builder.py @@ -8,10 +8,11 @@ from zigpy.quirks.v2 import CustomDeviceV2 import zigpy.types as t from zigpy.zcl import foundation -from zigpy.zcl.clusters.general import Basic, BatterySize +from zigpy.zcl.clusters.general import Basic from tests.common import ClusterListener, MockDatetime, wait_for_zigpy_tasks import zhaquirks +from zhaquirks.const import BatterySize from zhaquirks.tuya import ( TUYA_QUERY_DATA, TUYA_SET_TIME, @@ -93,6 +94,9 @@ async def test_convenience_methods(device_mock, method_name, attr_name, exp_clas (TuyaPowerConfigurationCluster2AAA, None, None, None, BatterySize.AAA, 2, 15), (None, BatterySize.CR123A, 1, 60, BatterySize.CR123A, 1, 60), (None, BatterySize.CR123A, 1, None, BatterySize.CR123A, 1, 30), + (None, BatterySize.CR2450, 1, None, BatterySize.CR2450, 1, 30), + (None, BatterySize.CR2032, 1, None, BatterySize.CR2032, 1, 30), + (None, BatterySize.CR1632, 1, None, BatterySize.CR1632, 1, 30), (None, BatterySize.AA, None, None, BatterySize.AA, None, None), (None, None, None, None, None, None, None), ], diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index a6f4454b91..fb6a45248f 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -47,6 +47,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ZONE_STATUS_CHANGE_COMMAND, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -58,6 +59,8 @@ XiaomiQuickInitDevice, handle_quick_init, ) +import zhaquirks.xiaomi.aqara.cube +import zhaquirks.xiaomi.aqara.cube_aqgl01 import zhaquirks.xiaomi.aqara.driver_curtain_e1 from zhaquirks.xiaomi.aqara.feeder_acn001 import ( FEEDER_ATTR, @@ -75,9 +78,14 @@ AqaraFeederAcn001, OppleCluster, ) +import zhaquirks.xiaomi.aqara.magnet_ac01 +import zhaquirks.xiaomi.aqara.magnet_acn001 import zhaquirks.xiaomi.aqara.magnet_agl02 +import zhaquirks.xiaomi.aqara.magnet_aq2 import zhaquirks.xiaomi.aqara.motion_ac02 +import zhaquirks.xiaomi.aqara.motion_acn001 import zhaquirks.xiaomi.aqara.motion_agl02 +import zhaquirks.xiaomi.aqara.motion_agl04 import zhaquirks.xiaomi.aqara.motion_aq2 import zhaquirks.xiaomi.aqara.motion_aq2b import zhaquirks.xiaomi.aqara.plug @@ -89,6 +97,7 @@ from zhaquirks.xiaomi.aqara.thermostat_agl001 import ScheduleEvent, ScheduleSettings import zhaquirks.xiaomi.aqara.weather import zhaquirks.xiaomi.mija.motion +import zhaquirks.xiaomi.mija.smoke zhaquirks.setup() @@ -384,10 +393,27 @@ async def test_mija_battery(zigpy_device_from_quirk, voltage, bpr): @pytest.mark.parametrize( "quirk, batt_size", ( - (zhaquirks.xiaomi.aqara.vibration_aq1.VibrationAQ1, 0x0A), - (zhaquirks.xiaomi.mija.motion.Motion, 0x09), - (zhaquirks.xiaomi.mija.sensor_switch.MijaButton, 0x0A), - (zhaquirks.xiaomi.mija.sensor_magnet.Magnet, 0x0B), + (zhaquirks.xiaomi.aqara.vibration_aq1.VibrationAQ1, BatterySize.CR2032), + (zhaquirks.xiaomi.aqara.cube.Cube, BatterySize.CR2450), + (zhaquirks.xiaomi.aqara.cube_aqgl01.CubeAQGL01, BatterySize.CR2450), + (zhaquirks.xiaomi.aqara.cube_aqgl01.CubeCAGL02, BatterySize.CR2450), + (zhaquirks.xiaomi.aqara.magnet_ac01.LumiMagnetAC01, BatterySize.CR123A), + (zhaquirks.xiaomi.aqara.magnet_acn001.MagnetE1, BatterySize.CR1632), + (zhaquirks.xiaomi.aqara.magnet_agl02.MagnetT1, BatterySize.CR1632), + (zhaquirks.xiaomi.aqara.magnet_aq2.MagnetAQ2, BatterySize.CR1632), + (zhaquirks.xiaomi.aqara.motion_ac02.LumiMotionAC02, BatterySize.CR1632), + (zhaquirks.xiaomi.aqara.motion_acn001.MotionE1, BatterySize.CR1632), + (zhaquirks.xiaomi.aqara.motion_agl02.MotionT1, BatterySize.CR1632), + (zhaquirks.xiaomi.aqara.motion_agl04.LumiLumiMotionAgl04, BatterySize.CR1632), + (zhaquirks.xiaomi.aqara.motion_aq2.MotionAQ2, BatterySize.CR2450), + (zhaquirks.xiaomi.aqara.motion_aq2b.MotionAQ2, BatterySize.CR2450), + (zhaquirks.xiaomi.mija.motion.Motion, BatterySize.CR2450), + (zhaquirks.xiaomi.mija.sensor_switch.MijaButton, BatterySize.CR2032), + (zhaquirks.xiaomi.mija.sensor_magnet.Magnet, BatterySize.CR1632), + ( + zhaquirks.xiaomi.mija.smoke.MijiaHoneywellSmokeDetectorSensor, + BatterySize.CR123A, + ), ), ) async def test_xiaomi_batt_size(zigpy_device_from_quirk, quirk, batt_size): diff --git a/zhaquirks/const.py b/zhaquirks/const.py index 16d87635e1..5d0d81480f 100644 --- a/zhaquirks/const.py +++ b/zhaquirks/const.py @@ -12,6 +12,7 @@ SIG_NODE_DESC, SIG_SKIP_CONFIG, ) +import zigpy.types as t ARGS = "args" ATTR_ID = "attr_id" @@ -125,3 +126,21 @@ ZONE_STATE = 0x0000 ZONE_TYPE = 0x0001 ZONE_STATUS = 0x0002 + + +class BatterySize(t.enum8): + """Battery sizes.""" + + No_battery = 0x00 + Built_in = 0x01 + Other = 0x02 + AA = 0x03 + AAA = 0x04 + C = 0x05 + D = 0x06 + CR2 = 0x07 + CR123A = 0x08 + CR2450 = 0x09 + CR2032 = 0x0A + CR1632 = 0x0B + Unknown = 0xFF diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py index d584269ccf..5d3a80fe12 100644 --- a/zhaquirks/ikea/__init__.py +++ b/zhaquirks/ikea/__init__.py @@ -8,6 +8,7 @@ from zigpy.zcl.clusters.general import Basic, PowerConfiguration, Scenes from zhaquirks import EventableCluster +from zhaquirks.const import BatterySize _LOGGER = logging.getLogger(__name__) @@ -148,7 +149,7 @@ class PowerConfig1AAACluster(CustomCluster, PowerConfiguration): """Updating power attributes: 2 AAA.""" _CONSTANT_ATTRIBUTES = { - BATTERY_SIZE: 4, + BATTERY_SIZE: BatterySize.AAA, BATTERY_QUANTITY: 1, BATTERY_RATED_VOLTAGE: 15, } @@ -158,7 +159,7 @@ class PowerConfig2AAACluster(CustomCluster, PowerConfiguration): """Updating power attributes: 2 AAA.""" _CONSTANT_ATTRIBUTES = { - BATTERY_SIZE: 4, + BATTERY_SIZE: BatterySize.AAA, BATTERY_QUANTITY: 2, BATTERY_RATED_VOLTAGE: 15, } @@ -168,7 +169,7 @@ class PowerConfig2CRCluster(CustomCluster, PowerConfiguration): """Updating power attributes: 2 CR2032.""" _CONSTANT_ATTRIBUTES = { - BATTERY_SIZE: 10, + BATTERY_SIZE: BatterySize.CR2032, BATTERY_QUANTITY: 2, BATTERY_RATED_VOLTAGE: 30, } @@ -178,7 +179,7 @@ class PowerConfig1CRCluster(CustomCluster, PowerConfiguration): """Updating power attributes: 1 CR2032.""" _CONSTANT_ATTRIBUTES = { - BATTERY_SIZE: 10, + BATTERY_SIZE: BatterySize.CR2032, BATTERY_QUANTITY: 1, BATTERY_RATED_VOLTAGE: 30, } @@ -189,7 +190,7 @@ class PowerConfig1CRXCluster(CustomCluster, PowerConfiguration): _CONSTANT_ATTRIBUTES = { BATTERY_VOLTAGE: 0, - BATTERY_SIZE: 10, + BATTERY_SIZE: BatterySize.CR2032, BATTERY_QUANTITY: 1, BATTERY_RATED_VOLTAGE: 30, } diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index f380f3e065..9b2b32c787 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -11,13 +11,7 @@ import zigpy.types as t from zigpy.zcl import BaseAttributeDefs, foundation from zigpy.zcl.clusters.closures import WindowCovering -from zigpy.zcl.clusters.general import ( - Basic, - BatterySize, - LevelControl, - OnOff, - PowerConfiguration, -) +from zigpy.zcl.clusters.general import Basic, LevelControl, OnOff, PowerConfiguration from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.hvac import Thermostat, UserInterface from zigpy.zcl.clusters.smartenergy import Metering @@ -30,6 +24,7 @@ RIGHT, SHORT_PRESS, ZHA_SEND_EVENT, + BatterySize, ) # --------------------------------------------------------- diff --git a/zhaquirks/tuya/builder/__init__.py b/zhaquirks/tuya/builder/__init__.py index 4d1310d746..c8a92b8934 100644 --- a/zhaquirks/tuya/builder/__init__.py +++ b/zhaquirks/tuya/builder/__init__.py @@ -17,7 +17,6 @@ from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass import zigpy.types as t from zigpy.zcl import foundation -from zigpy.zcl.clusters.general import BatterySize from zigpy.zcl.clusters.measurement import ( PM25, CarbonDioxideConcentration, @@ -31,6 +30,7 @@ from zigpy.zcl.clusters.smartenergy import Metering from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef +from zhaquirks.const import BatterySize from zhaquirks.tuya import ( TUYA_CLUSTER_ID, BaseEnchantedDevice, @@ -54,6 +54,9 @@ BatterySize.AA: 15, BatterySize.CR2: 30, BatterySize.CR123A: 30, + BatterySize.CR2450: 30, + BatterySize.CR2032: 30, + BatterySize.CR1632: 30, BatterySize.Unknown: None, } diff --git a/zhaquirks/tuya/tuya_gas.py b/zhaquirks/tuya/tuya_gas.py index 2b30a95b39..0feeb8a8f7 100644 --- a/zhaquirks/tuya/tuya_gas.py +++ b/zhaquirks/tuya/tuya_gas.py @@ -4,9 +4,9 @@ from zigpy.quirks.v2.homeassistant import CONCENTRATION_PARTS_PER_MILLION, UnitOfTime from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass import zigpy.types as t -from zigpy.zcl.clusters.general import BatterySize from zigpy.zcl.clusters.security import IasZone +from zhaquirks.const import BatterySize from zhaquirks.tuya import TuyaLocalCluster from zhaquirks.tuya.builder import TuyaQuirkBuilder diff --git a/zhaquirks/tuya/tuya_sensor.py b/zhaquirks/tuya/tuya_sensor.py index 1a97b4b897..767f29b2d4 100644 --- a/zhaquirks/tuya/tuya_sensor.py +++ b/zhaquirks/tuya/tuya_sensor.py @@ -8,6 +8,7 @@ import zigpy.types as t from zigpy.zcl import foundation +from zhaquirks.const import BatterySize from zhaquirks.tuya import ( TUYA_SET_TIME, TuyaPowerConfigurationCluster2AAA, @@ -305,7 +306,7 @@ class NoManufTimeTuyaMCUCluster(TuyaMCUCluster): .applies_to("_TZE200_n8dljorx", "TS0601") .tuya_illuminance(dp_id=101) .tuya_contact(dp_id=1) - .tuya_battery(dp_id=2) + .tuya_battery(dp_id=2, battery_type=BatterySize.CR2032, battery_qty=1) .skip_configuration() .add_to_registry() ) diff --git a/zhaquirks/tuya/tuya_siren.py b/zhaquirks/tuya/tuya_siren.py index f4246d7646..319092e361 100644 --- a/zhaquirks/tuya/tuya_siren.py +++ b/zhaquirks/tuya/tuya_siren.py @@ -4,8 +4,8 @@ from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfTemperature, UnitOfTime from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass import zigpy.types as t -from zigpy.zcl.clusters.general import BatterySize +from zhaquirks.const import BatterySize from zhaquirks.tuya.builder import TuyaQuirkBuilder diff --git a/zhaquirks/tuya/tuya_valve.py b/zhaquirks/tuya/tuya_valve.py index 96b2f1f1f3..e036496fff 100644 --- a/zhaquirks/tuya/tuya_valve.py +++ b/zhaquirks/tuya/tuya_valve.py @@ -6,9 +6,9 @@ from zigpy.quirks.v2.homeassistant import UnitOfTime, UnitOfVolume from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass import zigpy.types as t -from zigpy.zcl.clusters.general import BatterySize from zigpy.zcl.clusters.smartenergy import Metering +from zhaquirks.const import BatterySize from zhaquirks.tuya import TUYA_CLUSTER_ID from zhaquirks.tuya.builder import TuyaQuirkBuilder, TuyaValveWaterConsumed from zhaquirks.tuya.mcu import TuyaMCUCluster diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index f07243e512..4d2f53670a 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -48,6 +48,7 @@ UNKNOWN, VALUE, ZHA_SEND_EVENT, + BatterySize, ) BATTERY_LEVEL = "battery_level" @@ -116,7 +117,7 @@ class XiaomiCustomDevice(CustomDevice): def __init__(self, *args, **kwargs): """Init.""" if not hasattr(self, BATTERY_SIZE): - self.battery_size = 10 + self.battery_size = BatterySize.CR2032 super().__init__(*args, **kwargs) @@ -507,7 +508,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._CONSTANT_ATTRIBUTES = { BATTERY_QUANTITY_ATTR: 1, - BATTERY_SIZE_ATTR: getattr(self.endpoint.device, BATTERY_SIZE, 0xFF), + BATTERY_SIZE_ATTR: getattr( + self.endpoint.device, BATTERY_SIZE, BatterySize.Unknown + ), } self._slope = 200 / (self.MAX_VOLTS_MV - self.MIN_VOLTS_MV) diff --git a/zhaquirks/xiaomi/aqara/cube.py b/zhaquirks/xiaomi/aqara/cube.py index c73709ea95..4e782aed90 100644 --- a/zhaquirks/xiaomi/aqara/cube.py +++ b/zhaquirks/xiaomi/aqara/cube.py @@ -26,6 +26,7 @@ TURN_ON, VALUE, ZHA_SEND_EVENT, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -161,7 +162,7 @@ class Cube(XiaomiQuickInitDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 9 + self.battery_size = BatterySize.CR2450 super().__init__(*args, **kwargs) class MultistateInputCluster(CustomCluster, MultistateInput): diff --git a/zhaquirks/xiaomi/aqara/cube_aqgl01.py b/zhaquirks/xiaomi/aqara/cube_aqgl01.py index d5efbec549..f5cdfcb351 100644 --- a/zhaquirks/xiaomi/aqara/cube_aqgl01.py +++ b/zhaquirks/xiaomi/aqara/cube_aqgl01.py @@ -27,6 +27,7 @@ TURN_ON, VALUE, ZHA_SEND_EVENT, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -214,7 +215,7 @@ class CubeAQGL01(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 9 + self.battery_size = BatterySize.CR2450 super().__init__(*args, **kwargs) signature = { @@ -353,7 +354,7 @@ class CubeCAGL02(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 9 + self.battery_size = BatterySize.CR2450 super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/magnet_ac01.py b/zhaquirks/xiaomi/aqara/magnet_ac01.py index 1a9f67df81..3edbe41538 100644 --- a/zhaquirks/xiaomi/aqara/magnet_ac01.py +++ b/zhaquirks/xiaomi/aqara/magnet_ac01.py @@ -12,6 +12,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -42,7 +43,7 @@ class LumiMagnetAC01(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 8 + self.battery_size = BatterySize.CR123A super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/magnet_acn001.py b/zhaquirks/xiaomi/aqara/magnet_acn001.py index 53044995e9..415de2fe6e 100644 --- a/zhaquirks/xiaomi/aqara/magnet_acn001.py +++ b/zhaquirks/xiaomi/aqara/magnet_acn001.py @@ -11,6 +11,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -28,7 +29,7 @@ class MagnetE1(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/magnet_agl02.py b/zhaquirks/xiaomi/aqara/magnet_agl02.py index 2a63c40645..e6d4f96181 100644 --- a/zhaquirks/xiaomi/aqara/magnet_agl02.py +++ b/zhaquirks/xiaomi/aqara/magnet_agl02.py @@ -11,6 +11,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + BatterySize, ) from zhaquirks.xiaomi import ( BasicCluster, @@ -25,7 +26,7 @@ class MagnetT1(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/magnet_aq2.py b/zhaquirks/xiaomi/aqara/magnet_aq2.py index f59f8c834c..20066b7e3b 100644 --- a/zhaquirks/xiaomi/aqara/magnet_aq2.py +++ b/zhaquirks/xiaomi/aqara/magnet_aq2.py @@ -12,6 +12,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -31,7 +32,7 @@ class MagnetAQ2(XiaomiQuickInitDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/motion_ac02.py b/zhaquirks/xiaomi/aqara/motion_ac02.py index 58da99acb2..d6c2145c83 100644 --- a/zhaquirks/xiaomi/aqara/motion_ac02.py +++ b/zhaquirks/xiaomi/aqara/motion_ac02.py @@ -18,6 +18,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + BatterySize, ) from zhaquirks.xiaomi import ( LocalIlluminanceMeasurementCluster, @@ -91,7 +92,7 @@ class LumiMotionAC02(CustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 self.battery_quantity = 2 self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/aqara/motion_acn001.py b/zhaquirks/xiaomi/aqara/motion_acn001.py index 138f705a41..189957c314 100644 --- a/zhaquirks/xiaomi/aqara/motion_acn001.py +++ b/zhaquirks/xiaomi/aqara/motion_acn001.py @@ -11,6 +11,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -30,7 +31,7 @@ class MotionE1(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/aqara/motion_agl02.py b/zhaquirks/xiaomi/aqara/motion_agl02.py index aa7d0fa87f..6ad286cebb 100644 --- a/zhaquirks/xiaomi/aqara/motion_agl02.py +++ b/zhaquirks/xiaomi/aqara/motion_agl02.py @@ -14,6 +14,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -32,7 +33,7 @@ class MotionT1(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/aqara/motion_agl04.py b/zhaquirks/xiaomi/aqara/motion_agl04.py index 846bb7e127..19e19c3942 100644 --- a/zhaquirks/xiaomi/aqara/motion_agl04.py +++ b/zhaquirks/xiaomi/aqara/motion_agl04.py @@ -17,6 +17,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + BatterySize, ) from zhaquirks.xiaomi import ( DeviceTemperatureCluster, @@ -64,7 +65,7 @@ class LumiLumiMotionAgl04(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 self.battery_quantity = 2 self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/aqara/motion_aq2.py b/zhaquirks/xiaomi/aqara/motion_aq2.py index e3647cc92c..319158fcdd 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2.py @@ -15,6 +15,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -36,7 +37,7 @@ class MotionAQ2(XiaomiQuickInitDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 9 + self.battery_size = BatterySize.CR2450 self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/aqara/motion_aq2b.py b/zhaquirks/xiaomi/aqara/motion_aq2b.py index 212b896d14..fd07fe89ae 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2b.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2b.py @@ -13,6 +13,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -32,7 +33,7 @@ class MotionAQ2(XiaomiCustomDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 9 + self.battery_size = BatterySize.CR2450 self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/mija/motion.py b/zhaquirks/xiaomi/mija/motion.py index 15b2d4cf53..fdb1685a62 100644 --- a/zhaquirks/xiaomi/mija/motion.py +++ b/zhaquirks/xiaomi/mija/motion.py @@ -21,6 +21,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -40,7 +41,7 @@ class Motion(XiaomiQuickInitDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 9 + self.battery_size = BatterySize.CR2450 self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/mija/sensor_magnet.py b/zhaquirks/xiaomi/mija/sensor_magnet.py index 7413f135d6..72b34cc445 100644 --- a/zhaquirks/xiaomi/mija/sensor_magnet.py +++ b/zhaquirks/xiaomi/mija/sensor_magnet.py @@ -19,6 +19,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -38,7 +39,7 @@ class Magnet(XiaomiQuickInitDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 11 + self.battery_size = BatterySize.CR1632 super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/mija/sensor_switch.py b/zhaquirks/xiaomi/mija/sensor_switch.py index 3ab9e56a95..1fa2285aa8 100644 --- a/zhaquirks/xiaomi/mija/sensor_switch.py +++ b/zhaquirks/xiaomi/mija/sensor_switch.py @@ -44,6 +44,7 @@ TRIPLE_PRESS, UNKNOWN, ZHA_SEND_EVENT, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -68,7 +69,7 @@ class MijaButton(XiaomiQuickInitDevice): def __init__(self, *args, **kwargs): """Init.""" - self.battery_size = 10 + self.battery_size = BatterySize.CR2032 super().__init__(*args, **kwargs) class MijaOnOff(CustomCluster, OnOff): diff --git a/zhaquirks/xiaomi/mija/smoke.py b/zhaquirks/xiaomi/mija/smoke.py index d2780a2494..d4a4b44ba3 100644 --- a/zhaquirks/xiaomi/mija/smoke.py +++ b/zhaquirks/xiaomi/mija/smoke.py @@ -34,6 +34,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, + BatterySize, ) from zhaquirks.xiaomi import ( LUMI, @@ -66,7 +67,7 @@ class MijiaHoneywellSmokeDetectorSensor(XiaomiQuickInitDevice): def __init__(self, *args, **kwargs): """Init method.""" - self.battery_size = 8 # CR123a + self.battery_size = BatterySize.CR123A super().__init__(*args, **kwargs) signature = {