From 91e29d5b282b1d2d3b33a169fe3e20e79593d2ea Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 26 Nov 2024 21:08:46 +0100 Subject: [PATCH 1/3] Fix quirks v2 `adds` using `is_server=True` for client clusters (#1511) * Fix quirks v2 `adds` using `is_server=True` for client clusters * Add regression test --- tests/test_quirks_v2.py | 4 ++++ zigpy/quirks/v2/__init__.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_quirks_v2.py b/tests/test_quirks_v2.py index a2c4d40d5..90732d9d4 100644 --- a/tests/test_quirks_v2.py +++ b/tests/test_quirks_v2.py @@ -525,6 +525,8 @@ class CustomOnOffCluster(CustomCluster, OnOff): CustomOnOffCluster.cluster_id ] assert isinstance(quirked_cluster, CustomOnOffCluster) + # verify server cluster type was set when adding + assert quirked_cluster.cluster_type == ClusterType.Server quirked_cluster.apply_custom_configuration = AsyncMock() @@ -532,6 +534,8 @@ class CustomOnOffCluster(CustomCluster, OnOff): 1 ].out_clusters[CustomOnOffCluster.cluster_id] assert isinstance(quirked_client_cluster, CustomOnOffCluster) + # verify client cluster type was set when adding + assert quirked_client_cluster.cluster_type == ClusterType.Client quirked_client_cluster.apply_custom_configuration = AsyncMock() diff --git a/zigpy/quirks/v2/__init__.py b/zigpy/quirks/v2/__init__.py index 66e047426..e17f1bc3b 100644 --- a/zigpy/quirks/v2/__init__.py +++ b/zigpy/quirks/v2/__init__.py @@ -161,7 +161,7 @@ class AddsMetadata: def __call__(self, device: CustomDeviceV2) -> None: """Process the add.""" endpoint: Endpoint = device.endpoints[self.endpoint_id] - if self.cluster_type == ClusterType.Server: + if is_server_cluster := self.cluster_type == ClusterType.Server: add_cluster = endpoint.add_input_cluster else: add_cluster = endpoint.add_output_cluster @@ -170,7 +170,7 @@ def __call__(self, device: CustomDeviceV2) -> None: cluster = None cluster_id = self.cluster else: - cluster = self.cluster(endpoint, is_server=True) + cluster = self.cluster(endpoint, is_server=is_server_cluster) cluster_id = cluster.cluster_id cluster = add_cluster(cluster_id, cluster) From de76b13adf0191899b7be34742961d5c587c1895 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 26 Nov 2024 21:08:56 +0100 Subject: [PATCH 2/3] Fix `entity_metadata` typing not including `ZCLSensorMetadata` (#1512) --- zigpy/quirks/v2/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zigpy/quirks/v2/__init__.py b/zigpy/quirks/v2/__init__.py index e17f1bc3b..355c146d6 100644 --- a/zigpy/quirks/v2/__init__.py +++ b/zigpy/quirks/v2/__init__.py @@ -460,6 +460,7 @@ def __init__( ] = [] self.entity_metadata: list[ ZCLEnumMetadata + | ZCLSensorMetadata | SwitchMetadata | NumberMetadata | BinarySensorMetadata From 95baa8b07547d34c733936b946891990e5695145 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:09:19 -0500 Subject: [PATCH 3/3] Fix logged deprecations in unit tests (#1501) * Fix pytest-asyncio warnings * Fix OTA deprecation warnings * Remove uses of `update_last_seen` * Migrate `handle_message` to `packet_received` * Mark a few more tests as having expected deprecations --- pyproject.toml | 1 + tests/ota/test_ota_config.py | 2 + tests/test_appdb.py | 4 +- tests/test_appdb_migration.py | 3 +- tests/test_application.py | 5 +++ tests/test_datastructures.py | 8 ++-- tests/test_device.py | 80 +++++++++++++++++++++++------------ tests/test_zigbee_util.py | 1 + zigpy/application.py | 3 +- 9 files changed, 72 insertions(+), 35 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b0caf6247..351d660ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ enabled = true [tool.pytest.ini_options] asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" [tool.ruff] required-version = ">=0.5.0" diff --git a/tests/ota/test_ota_config.py b/tests/ota/test_ota_config.py index dd03eb09f..17c29fcce 100644 --- a/tests/ota/test_ota_config.py +++ b/tests/ota/test_ota_config.py @@ -14,6 +14,7 @@ import zigpy.types as t +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_ota_disabled_legacy(tmp_path: pathlib.Path) -> None: (tmp_path / "index.json").write_text("{}") @@ -49,6 +50,7 @@ async def test_ota_disabled_legacy(tmp_path: pathlib.Path) -> None: assert not ota._providers +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_ota_enabled_legacy(tmp_path: pathlib.Path) -> None: (tmp_path / "index.json").write_text("{}") diff --git a/tests/test_appdb.py b/tests/test_appdb.py index fad244caf..f04bbe6c5 100644 --- a/tests/test_appdb.py +++ b/tests/test_appdb.py @@ -983,7 +983,7 @@ async def test_last_seen(tmp_path): # Last-seen is only written to the db every 30s (no write case) now = datetime.fromtimestamp(dev.last_seen + 5, timezone.utc) with freezegun.freeze_time(now): - dev.update_last_seen() + dev.last_seen = datetime.now(timezone.utc) await app.shutdown() @@ -998,7 +998,7 @@ async def test_last_seen(tmp_path): # Last-seen is only written to the db every 30s (write case) now = datetime.fromtimestamp(dev.last_seen + 35, timezone.utc) with freezegun.freeze_time(now): - dev.update_last_seen() + dev.last_seen = datetime.now(timezone.utc) await app.shutdown() diff --git a/tests/test_appdb_migration.py b/tests/test_appdb_migration.py index c490cf656..544f023fe 100644 --- a/tests/test_appdb_migration.py +++ b/tests/test_appdb_migration.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone import logging import pathlib from sqlite3.dump import _iterdump as iterdump @@ -484,7 +485,7 @@ async def test_last_seen_initial_migration(test_db): dev = app.get_device(nwk=0xBD4D) assert dev.last_seen is None - dev.update_last_seen() + dev.last_seen = datetime.now(timezone.utc) assert isinstance(dev.last_seen, float) await app.shutdown() diff --git a/tests/test_application.py b/tests/test_application.py index b85110fc5..ba0dd7175 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -223,6 +223,7 @@ def test_deserialize(app, ieee): assert dev.deserialize.call_count == 1 +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_handle_message_shim(app): dev = MagicMock() dev.nwk = 0x1234 @@ -332,6 +333,9 @@ def test_props(app): assert app.state.network_info.nwk_update_id is not None +@pytest.mark.filterwarnings( + "ignore::DeprecationWarning" +) # TODO: migrate `handle_message_from_uninitialized_sender` away from `handle_message` async def test_uninitialized_message_handlers(app, ieee): """Test uninitialized message handlers.""" handler_1 = MagicMock(return_value=None) @@ -1403,6 +1407,7 @@ async def test_watchdog(app): assert app._watchdog_task.done() +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_permit_with_key(app): app = make_app({}) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index b347ca053..54a6a0018 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -136,7 +136,7 @@ async def inner(): loop2.run_until_complete(inner()) -async def test_dynamic_bounded_semaphore_runtime_limit_increase(event_loop): +async def test_dynamic_bounded_semaphore_runtime_limit_increase(): """Test changing the max_value at runtime.""" sem = datastructures.PriorityDynamicBoundedSemaphore(2) @@ -144,7 +144,7 @@ async def test_dynamic_bounded_semaphore_runtime_limit_increase(event_loop): def set_limit(n): sem.max_value = n - event_loop.call_later(0.1, set_limit, 3) + asyncio.get_running_loop().call_later(0.1, set_limit, 3) async with sem: # Play with the value, testing edge cases @@ -189,7 +189,7 @@ def set_limit(n): assert sem.max_value == 3 -async def test_dynamic_bounded_semaphore_errors(event_loop): +async def test_dynamic_bounded_semaphore_errors(): """Test semaphore handling errors and cancellation.""" sem = datastructures.PriorityDynamicBoundedSemaphore(1) @@ -273,7 +273,7 @@ async def acquire(): assert not sem.locked() -async def test_priority_lock(event_loop): +async def test_priority_lock(): """Test priority lock.""" lock = datastructures.PriorityLock() diff --git a/tests/test_device.py b/tests/test_device.py index cfc8f7a66..a9b3b5b02 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -18,7 +18,7 @@ from zigpy.zcl.clusters.general import Basic, Ota from zigpy.zdo import types as zdo_t -from .async_mock import ANY, AsyncMock, MagicMock, int_sentinel, patch, sentinel +from .async_mock import AsyncMock, MagicMock, int_sentinel, patch, sentinel @pytest.fixture @@ -182,12 +182,17 @@ async def test_handle_message_read_report_conf(dev): dev._pending[tsn] = req_mock # Read Report Configuration Success - rsp = dev.handle_message( - 0x104, # profile - 0x702, # cluster - 3, # source EP - 3, # dest EP - b"\x18\x56\x09\x00\x00\x00\x00\x25\x1e\x00\x84\x03\x01\x02\x03\x04\x05\x06", # message + rsp = dev.packet_received( + t.ZigbeePacket( + profile_id=0x104, + cluster_id=0x702, + src_ep=3, + dst_ep=3, + data=t.SerializableBytes( + b"\x18\x56\x09\x00\x00\x00\x00\x25\x1e\x00\x84\x03\x01\x02\x03\x04\x05\x06" + ), # message + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + ) ) # Returns decoded msg when response is not pending, None otherwise assert rsp is None @@ -206,12 +211,17 @@ async def test_handle_message_read_report_conf(dev): tsn2 = 0x5B req_mock2 = MagicMock() dev._pending[tsn2] = req_mock2 - rsp2 = dev.handle_message( - 0x104, # profile - 0x702, # cluster - 3, # source EP - 3, # dest EP - b"\x18\x5b\x09\x86\x00\x00\x00\x86\x00\x12\x00\x86\x00\x00\x04", # message 3x("Unsupported attribute" response) + rsp2 = dev.packet_received( + t.ZigbeePacket( + profile_id=0x104, + cluster_id=0x702, + src_ep=3, + dst_ep=3, + data=t.SerializableBytes( + b"\x18\x5b\x09\x86\x00\x00\x00\x86\x00\x12\x00\x86\x00\x00\x04" + ), # message 3x("Unsupported attribute" response) + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + ) ) # Returns decoded msg when response is not pending, None otherwise assert rsp2 is None @@ -232,12 +242,17 @@ async def test_handle_message_read_report_conf(dev): tsn3 = 0x5C req_mock3 = MagicMock() dev._pending[tsn3] = req_mock3 - rsp3 = dev.handle_message( - 0x104, # profile - 0x702, # cluster - 3, # source EP - 3, # dest EP - b"\x18\x5c\x09\x86\x00\x00\x00\x00\x00\x00\x00\x25\x1e\x00\x84\x03\x01\x02\x03\x04\x05\x06", + rsp3 = dev.packet_received( + t.ZigbeePacket( + profile_id=0x104, + cluster_id=0x702, + src_ep=3, + dst_ep=3, + data=t.SerializableBytes( + b"\x18\x5c\x09\x86\x00\x00\x00\x00\x00\x00\x00\x25\x1e\x00\x84\x03\x01\x02\x03\x04\x05\x06" + ), + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + ) ) assert rsp3 is None cfg_unsup4, cfg_sup2 = req_mock3.result.set_result.call_args[0][0].attribute_configs @@ -248,9 +263,20 @@ async def test_handle_message_read_report_conf(dev): async def test_handle_message_deserialize_error(dev): ep = dev.add_endpoint(3) - dev.deserialize = MagicMock(side_effect=ValueError) + ep.deserialize = MagicMock(side_effect=ValueError) ep.handle_message = MagicMock() - dev.handle_message(99, 98, 3, 3, b"abcd") + + dev.packet_received( + t.ZigbeePacket( + profile_id=99, + cluster_id=98, + src_ep=3, + dst_ep=3, + data=t.SerializableBytes(b"abcd"), + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + ) + ) + assert ep.handle_message.call_count == 0 @@ -412,10 +438,9 @@ def test_device_last_seen(dev, monkeypatch): dev.listener_event.assert_called_once_with("device_last_seen_updated", epoch) dev.listener_event.reset_mock() - dev.update_last_seen() - dev.listener_event.assert_called_once_with("device_last_seen_updated", ANY) - event_time = dev.listener_event.mock_calls[0].args[1] - assert (event_time - datetime.now(timezone.utc)).total_seconds() < 0.1 + now = datetime.now(timezone.utc) + dev.last_seen = now + dev.listener_event.assert_called_once_with("device_last_seen_updated", now) async def test_ignore_unknown_endpoint(dev, caplog): @@ -1220,6 +1245,7 @@ async def send_packet(packet: t.ZigbeePacket): cluster.image_block_response = image_block_response +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_deserialize_backwards_compat(dev): """Test that deserialization uses the method if it is overloaded.""" dev._packet_debouncer.filter = MagicMock(return_value=False) @@ -1254,7 +1280,7 @@ async def test_deserialize_backwards_compat(dev): assert dev.deserialize.call_count == 1 -async def test_request_exception_propagation(dev, event_loop): +async def test_request_exception_propagation(dev): """Test that exceptions are propagated to the caller.""" tsn = 0x12 @@ -1264,7 +1290,7 @@ async def test_request_exception_propagation(dev, event_loop): dev.get_sequence = MagicMock(return_value=tsn) - event_loop.call_soon( + asyncio.get_running_loop().call_soon( dev.packet_received, t.ZigbeePacket( profile_id=260, diff --git a/tests/test_zigbee_util.py b/tests/test_zigbee_util.py index c1189f5c5..57aa5b87a 100644 --- a/tests/test_zigbee_util.py +++ b/tests/test_zigbee_util.py @@ -569,6 +569,7 @@ async def slow_error(self, n=None): assert f.slow_error_calls == 2 +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_deprecated(): @util.deprecated("This function is deprecated") def foo(): diff --git a/zigpy/application.py b/zigpy/application.py index fef732ef9..79a1bf58f 100644 --- a/zigpy/application.py +++ b/zigpy/application.py @@ -5,6 +5,7 @@ import collections from collections.abc import Coroutine import contextlib +from datetime import datetime, timezone import errno import logging import os @@ -559,7 +560,7 @@ def handle_join( # Not all stacks send a ZDO command when a device joins so the last_seen should # be updated - dev.update_last_seen() + dev.last_seen = datetime.now(timezone.utc) # Cancel all pending requests for the device dev._concurrent_requests_semaphore.cancel_waiting(