diff --git a/pyproject.toml b/pyproject.toml index 473eeafb..eef2fffe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,13 +14,13 @@ readme = "README.md" license = {text = "Apache-2.0"} requires-python = ">=3.12" dependencies = [ - "zigpy==0.65.0", + "zigpy==0.65.2", "bellows==0.40.2", "zigpy-znp==0.12.3", "zigpy-deconz==0.23.2", "zigpy-xbee==0.20.1", "zigpy-zigate==0.13.0", - "zha-quirks==0.0.117", + "zha-quirks==0.0.118", "pyserial==3.5", "pyserial-asyncio-fast", ] diff --git a/requirements_test.txt b/requirements_test.txt index 8a804dd8..75fb89bd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,6 +9,6 @@ pytest-timeout pytest-asyncio pytest-xdist pytest -zigpy>=0.63.5 +zigpy>=0.65.2 ruff looptime diff --git a/tests/test_discover.py b/tests/test_discover.py index 7e4a87fb..84898bdf 100644 --- a/tests/test_discover.py +++ b/tests/test_discover.py @@ -28,10 +28,10 @@ EntityMetadata, EntityType, NumberMetadata, + QuirkBuilder, QuirksV2RegistryEntry, ZCLCommandButtonMetadata, ZCLSensorMetadata, - add_to_registry_v2, ) from zigpy.quirks.v2.homeassistant import UnitOfTime import zigpy.types @@ -504,7 +504,7 @@ async def test_quirks_v2_entity_discovery( ) ( - add_to_registry_v2( + QuirkBuilder( "Ikea of Sweden", "TRADFRI remote control", zigpy.quirks._DEVICE_REGISTRY ) .replaces(PowerConfig1CRCluster) @@ -518,6 +518,7 @@ async def test_quirks_v2_entity_discovery( unit=UnitOfTime.SECONDS, multiplier=1, ) + .add_to_registry() ) zigpy_device = zigpy.quirks._DEVICE_REGISTRY.get_device(zigpy_device) @@ -562,7 +563,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): ) ( - add_to_registry_v2("LUMI", "lumi.curtain.agl006") + QuirkBuilder("LUMI", "lumi.curtain.agl006") .adds(LocalIlluminanceMeasurementCluster) .replaces(BasicCluster) .replaces(XiaomiPowerConfigurationPercent) @@ -584,6 +585,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): entity_type=EntityType.DIAGNOSTIC, ) .binary_sensor("error_detected", FakeXiaomiAqaraDriverE1.cluster_id) + .add_to_registry() ) aqara_E1_device = zigpy_device_mock( @@ -671,8 +673,7 @@ def _get_test_device( zigpy_device_mock, manufacturer: str, model: str, - augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry] - | None = None, + augment_method: Callable[[QuirkBuilder], QuirkBuilder] | None = None, ): zigpy_device = zigpy_device_mock( { @@ -693,8 +694,8 @@ def _get_test_device( model=model, ) - v2_quirk = ( - add_to_registry_v2(manufacturer, model, zigpy.quirks._DEVICE_REGISTRY) + quirk_builder = ( + QuirkBuilder(manufacturer, model, zigpy.quirks._DEVICE_REGISTRY) .replaces(PowerConfig1CRCluster) .replaces(ScenesCluster, cluster_type=ClusterType.Client) .number( @@ -727,7 +728,9 @@ def _get_test_device( ) if augment_method: - v2_quirk = augment_method(v2_quirk) + quirk_builder = augment_method(quirk_builder) + + quirk_builder.add_to_registry() zigpy_device = zigpy.quirks._DEVICE_REGISTRY.get_device(zigpy_device) zigpy_device.endpoints[1].power.PLUGGED_ATTR_READS = { @@ -871,10 +874,10 @@ def validate_translation_keys_device_class( def bad_device_class_unit_combination( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: + quirk_builder: QuirkBuilder, +) -> QuirkBuilder: """Introduce a bad device class and unit combination.""" - return v2_quirk.sensor( + return quirk_builder.sensor( zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, zigpy.zcl.clusters.general.OnOff.cluster_id, entity_type=EntityType.CONFIG, @@ -885,10 +888,10 @@ def bad_device_class_unit_combination( def bad_device_class_translation_key_usage( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: + quirk_builder: QuirkBuilder, +) -> QuirkBuilder: """Introduce a bad device class and translation key combination.""" - return v2_quirk.sensor( + return quirk_builder.sensor( zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, zigpy.zcl.clusters.general.OnOff.cluster_id, entity_type=EntityType.CONFIG, @@ -928,7 +931,7 @@ async def test_quirks_v2_metadata_errors( zha_gateway: Gateway, # pylint: disable=unused-argument zigpy_device_mock, device_joined: Callable[[zigpy.device.Device], Awaitable[Device]], - augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], + augment_method: Callable[[QuirkBuilder], QuirkBuilder], validate_method: Callable, expected_exception_string: str, ) -> None: @@ -939,30 +942,19 @@ async def test_quirks_v2_metadata_errors( # ensure the error is caught and raised with pytest.raises(ValueError, match=expected_exception_string): - try: - # introduce an error - zigpy_device = _get_test_device( - zigpy_device_mock, - "Ikea of Sweden4", - "TRADFRI remote control4", - augment_method=augment_method, - ) - await device_joined(zigpy_device) - - validate_metadata(validate_method) - # if the device was created we remove it - # so we don't pollute the rest of the tests - zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) - except ValueError as e: - # if the device was not created we remove it - # so we don't pollute the rest of the tests - zigpy.quirks._DEVICE_REGISTRY._registry_v2.pop( - ( - "Ikea of Sweden4", - "TRADFRI remote control4", - ) - ) - raise e + # introduce an error + zigpy_device = _get_test_device( + zigpy_device_mock, + "Ikea of Sweden4", + "TRADFRI remote control4", + augment_method=augment_method, + ) + await device_joined(zigpy_device) + + validate_metadata(validate_method) + # if the device was created we remove it + # so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) class BadDeviceClass(enum.Enum): @@ -972,11 +964,11 @@ class BadDeviceClass(enum.Enum): def bad_binary_sensor_device_class( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: + quirk_builder: QuirkBuilder, +) -> QuirkBuilder: """Introduce a bad device class on a binary sensor.""" - return v2_quirk.binary_sensor( + return quirk_builder.binary_sensor( zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_off.name, zigpy.zcl.clusters.general.OnOff.cluster_id, device_class=BadDeviceClass.BAD, @@ -984,11 +976,11 @@ def bad_binary_sensor_device_class( def bad_sensor_device_class( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: + quirk_builder: QuirkBuilder, +) -> QuirkBuilder: """Introduce a bad device class on a sensor.""" - return v2_quirk.sensor( + return quirk_builder.sensor( zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, zigpy.zcl.clusters.general.OnOff.cluster_id, device_class=BadDeviceClass.BAD, @@ -996,11 +988,11 @@ def bad_sensor_device_class( def bad_number_device_class( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: + quirk_builder: QuirkBuilder, +) -> QuirkBuilder: """Introduce a bad device class on a number.""" - return v2_quirk.number( + return quirk_builder.number( zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_time.name, zigpy.zcl.clusters.general.OnOff.cluster_id, device_class=BadDeviceClass.BAD, @@ -1032,7 +1024,7 @@ async def test_quirks_v2_metadata_bad_device_classes( zigpy_device_mock, device_joined: Callable[[zigpy.device.Device], Awaitable[Device]], caplog: pytest.LogCaptureFixture, - augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], + augment_method: Callable[[QuirkBuilder], QuirkBuilder], expected_exception_string: str, ) -> None: """Test bad quirks v2 device classes.""" @@ -1040,8 +1032,8 @@ async def test_quirks_v2_metadata_bad_device_classes( # introduce an error zigpy_device = _get_test_device( zigpy_device_mock, - "Ikea of Sweden4", - "TRADFRI remote control4", + "Ikea of Sweden5", + "TRADFRI remote control5", augment_method=augment_method, ) await device_joined(zigpy_device) diff --git a/tests/test_select.py b/tests/test_select.py index 380d9de8..3562ffdb 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -15,7 +15,7 @@ from zigpy.device import Device as ZigpyDevice from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice, get_device -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 +from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder import zigpy.types as t from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster @@ -162,7 +162,7 @@ async def test_on_off_select_attribute_report( ( - add_to_registry_v2("Fake_Manufacturer", "Fake_Model") + QuirkBuilder("Fake_Manufacturer", "Fake_Model") .replaces(MotionSensitivityQuirk.OppleCluster) .enum( "motion_sensitivity", @@ -176,6 +176,7 @@ async def test_on_off_select_attribute_report( translation_key="motion_sensitivity", initially_disabled=True, ) + .add_to_registry() ) diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 0b42a5a5..fad4b143 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -10,7 +10,7 @@ from zigpy.device import Device as ZigpyDevice import zigpy.profiles.zha from zigpy.quirks import CustomCluster, get_device -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 +from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder import zigpy.types as t from zigpy.zcl import Cluster from zigpy.zcl.clusters import general, homeautomation, hvac, measurement, smartenergy @@ -1115,7 +1115,7 @@ def __init__(self, *args, **kwargs) -> None: ( - add_to_registry_v2("Fake_Manufacturer_sensor", "Fake_Model_sensor") + QuirkBuilder("Fake_Manufacturer_sensor", "Fake_Model_sensor") .replaces(OppleCluster) .sensor( "last_feeding_size", @@ -1124,6 +1124,7 @@ def __init__(self, *args, **kwargs) -> None: multiplier=1, unit=UnitOfMass.GRAMS, ) + .add_to_registry() ) diff --git a/tests/test_switch.py b/tests/test_switch.py index 542860ee..56f7a546 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -17,7 +17,7 @@ from zigpy.exceptions import ZigbeeException from zigpy.profiles import zha from zigpy.quirks import _DEVICE_REGISTRY, CustomCluster, CustomDevice -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 +from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder import zigpy.types as t from zigpy.zcl.clusters import closures, general from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster @@ -525,7 +525,7 @@ async def test_switch_configurable_custom_on_off_values( ) ( - add_to_registry_v2(zigpy_dev.manufacturer, zigpy_dev.model) + QuirkBuilder(zigpy_dev.manufacturer, zigpy_dev.model) .adds(WindowDetectionFunctionQuirk.TuyaManufCluster) .switch( "window_detection_function", @@ -533,6 +533,7 @@ async def test_switch_configurable_custom_on_off_values( on_value=3, off_value=5, ) + .add_to_registry() ) zigpy_device_ = _DEVICE_REGISTRY.get_device(zigpy_dev) @@ -600,7 +601,7 @@ async def test_switch_configurable_custom_on_off_values_force_inverted( ) ( - add_to_registry_v2(zigpy_dev.manufacturer, zigpy_dev.model) + QuirkBuilder(zigpy_dev.manufacturer, zigpy_dev.model) .adds(WindowDetectionFunctionQuirk.TuyaManufCluster) .switch( "window_detection_function", @@ -609,6 +610,7 @@ async def test_switch_configurable_custom_on_off_values_force_inverted( off_value=5, force_inverted=True, ) + .add_to_registry() ) zigpy_device_ = _DEVICE_REGISTRY.get_device(zigpy_dev) @@ -676,7 +678,7 @@ async def test_switch_configurable_custom_on_off_values_inverter_attribute( ) ( - add_to_registry_v2(zigpy_dev.manufacturer, zigpy_dev.model) + QuirkBuilder(zigpy_dev.manufacturer, zigpy_dev.model) .adds(WindowDetectionFunctionQuirk.TuyaManufCluster) .switch( "window_detection_function", @@ -685,6 +687,7 @@ async def test_switch_configurable_custom_on_off_values_inverter_attribute( off_value=5, invert_attribute_name="window_detection_function_inverter", ) + .add_to_registry() ) zigpy_device_ = _DEVICE_REGISTRY.get_device(zigpy_dev) diff --git a/zha/application/gateway.py b/zha/application/gateway.py index 5ba7ff3e..72b5c9db 100644 --- a/zha/application/gateway.py +++ b/zha/application/gateway.py @@ -24,6 +24,7 @@ import zigpy.device import zigpy.endpoint import zigpy.group +from zigpy.quirks.v2 import UNBUILT_QUIRK_BUILDERS from zigpy.state import State from zigpy.types.named import EUI64 @@ -217,6 +218,15 @@ async def async_from_config(cls, config: ZHAData) -> Self: instance = cls(config) if config.config.quirks_configuration.enabled: + for quirk in UNBUILT_QUIRK_BUILDERS: + _LOGGER.warning( + "Found a v2 quirk that was not added to the registry: %s", + quirk, + ) + quirk.add_to_registry() + + UNBUILT_QUIRK_BUILDERS.clear() + await instance.async_add_executor_job( setup_quirks, instance.config.config.quirks_configuration.custom_quirks_path,