From a10294bfa5af0e52fda4fccff06608de8f3c6d97 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 31 Dec 2024 01:41:42 -0500 Subject: [PATCH 1/8] WIP --- bellows/zigbee/application.py | 144 ++++++++++++++-------------------- 1 file changed, 58 insertions(+), 86 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 888ab562..5d6054d5 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -44,7 +44,6 @@ import bellows.zigbee.util as util APS_ACK_TIMEOUT = 120 -RETRY_DELAYS = [0.5, 1.0, 1.5] COUNTER_EZSP_BUFFERS = "EZSP_FREE_BUFFERS" COUNTER_NWK_CONFLICTS = "nwk_conflicts" COUNTER_RESET_REQ = "reset_requests" @@ -826,7 +825,6 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: aps_frame.sourceEndpoint = t.uint8_t(packet.src_ep) aps_frame.destinationEndpoint = t.uint8_t(packet.dst_ep or 0) aps_frame.options = t.EmberApsOption.APS_OPTION_NONE - aps_frame.options |= t.EmberApsOption.APS_OPTION_RETRY if packet.dst.addr_mode == zigpy.types.AddrMode.Group: aps_frame.groupId = t.uint16_t(packet.dst.address) @@ -843,100 +841,74 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: message_tag = self.get_sequence() pending_tag = (packet.dst.address, message_tag) with self._pending.new(pending_tag) as req: - for attempt, retry_delay in enumerate(RETRY_DELAYS): - async with self._req_lock: - if packet.dst.addr_mode == zigpy.types.AddrMode.NWK: - if packet.extended_timeout and device is not None: - await self._ezsp.set_extended_timeout( - nwk=device.nwk, - ieee=device.ieee, - extended_timeout=True, - ) - - if packet.source_route is not None: - if ( - FirmwareFeatures.MANUAL_SOURCE_ROUTE - in self._ezsp._xncp_features - and self.config[CONF_BELLOWS_CONFIG][ - CONF_MANUAL_SOURCE_ROUTING - ] - ): - await self._ezsp.xncp_set_manual_source_route( - nwk=packet.dst.address, - relays=packet.source_route, - ) - else: - await self._ezsp.set_source_route( - nwk=packet.dst.address, - relays=packet.source_route, - ) - - status, _ = await self._ezsp.send_unicast( - nwk=packet.dst.address, - aps_frame=aps_frame, - message_tag=message_tag, - data=packet.data.serialize(), - ) - elif packet.dst.addr_mode == zigpy.types.AddrMode.Group: - status, _ = await self._ezsp.send_multicast( - aps_frame=aps_frame, - radius=packet.radius, - non_member_radius=packet.non_member_radius, - message_tag=message_tag, - data=packet.data.serialize(), - ) - elif packet.dst.addr_mode == zigpy.types.AddrMode.Broadcast: - status, _ = await self._ezsp.send_broadcast( - address=packet.dst.address, - aps_frame=aps_frame, - radius=packet.radius, - message_tag=message_tag, - aps_sequence=packet.tsn, - data=packet.data.serialize(), + async with self._req_lock: + if packet.dst.addr_mode == zigpy.types.AddrMode.NWK: + if packet.extended_timeout and device is not None: + await self._ezsp.set_extended_timeout( + nwk=device.nwk, + ieee=device.ieee, + extended_timeout=True, ) - if status == t.sl_Status.OK: - break - elif status not in ( - t.sl_Status.ZIGBEE_MAX_MESSAGE_LIMIT_REACHED, - t.sl_Status.TRANSMIT_BUSY, - t.sl_Status.ALLOCATION_FAILED, - ): - raise zigpy.exceptions.DeliveryError( - f"Failed to enqueue message: {status!r}", status + if packet.source_route is not None: + if ( + FirmwareFeatures.MANUAL_SOURCE_ROUTE + in self._ezsp._xncp_features + and self.config[CONF_BELLOWS_CONFIG][ + CONF_MANUAL_SOURCE_ROUTING + ] + ): + await self._ezsp.xncp_set_manual_source_route( + nwk=packet.dst.address, + relays=packet.source_route, + ) + else: + await self._ezsp.set_source_route( + nwk=packet.dst.address, + relays=packet.source_route, + ) + + status, _ = await self._ezsp.send_unicast( + nwk=packet.dst.address, + aps_frame=aps_frame, + message_tag=message_tag, + data=packet.data.serialize(), ) - else: - if attempt < len(RETRY_DELAYS): - LOGGER.debug( - "Request %s failed to enqueue, retrying in %ss: %s", - pending_tag, - retry_delay, - status, - ) - await asyncio.sleep(retry_delay) - else: + elif packet.dst.addr_mode == zigpy.types.AddrMode.Group: + status, _ = await self._ezsp.send_multicast( + aps_frame=aps_frame, + radius=packet.radius, + non_member_radius=packet.non_member_radius, + message_tag=message_tag, + data=packet.data.serialize(), + ) + elif packet.dst.addr_mode == zigpy.types.AddrMode.Broadcast: + status, _ = await self._ezsp.send_broadcast( + address=packet.dst.address, + aps_frame=aps_frame, + radius=packet.radius, + message_tag=message_tag, + aps_sequence=packet.tsn, + data=packet.data.serialize(), + ) + + if status != t.sl_Status.OK: raise zigpy.exceptions.DeliveryError( - ( - f"Failed to enqueue message after {len(RETRY_DELAYS)}" - f" attempts: {status!r}" - ), - status, + f"Failed to enqueue message: {status!r}", status ) # Only throw a delivery exception for packets sent with NWK addressing. # https://github.com/home-assistant/core/issues/79832 # Broadcasts/multicasts don't have ACKs or confirmations either. - if packet.dst.addr_mode != zigpy.types.AddrMode.NWK: - return - - # Wait for `messageSentHandler` message - async with asyncio_timeout(APS_ACK_TIMEOUT): - send_status, _ = await req.result + if packet.dst.addr_mode == zigpy.types.AddrMode.NWK: + # Wait for `messageSentHandler` message + async with asyncio_timeout(APS_ACK_TIMEOUT): + send_status, _ = await req.result - if t.sl_Status.from_ember_status(send_status) != t.sl_Status.OK: - raise zigpy.exceptions.DeliveryError( - f"Failed to deliver message: {send_status!r}", send_status - ) + if t.sl_Status.from_ember_status(send_status) != t.sl_Status.OK: + raise zigpy.exceptions.DeliveryError( + f"Failed to deliver message: {send_status!r}", send_status + ) async def permit(self, time_s: int = 60, node: t.EmberNodeId = None) -> None: """Permit joining.""" From ad0ce5b7a485beebe24bd94aceff15238172951d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:12:12 -0500 Subject: [PATCH 2/8] Allow routing errors to cancel pending requests --- bellows/zigbee/application.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 5d6054d5..428e6399 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -81,7 +81,7 @@ class ControllerApplication(zigpy.application.ControllerApplication): {zigpy.config.CONF_DEVICE_BAUDRATE: 57600}, ] - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: super().__init__(config) self._ctrl_event = asyncio.Event() self._created_device_endpoints: list[zdo_t.SimpleDescriptor] = [] @@ -1029,3 +1029,25 @@ def handle_route_record( def handle_route_error(self, status: t.sl_Status, nwk: t.EmberNodeId) -> None: LOGGER.debug("Processing route error: status=%s, nwk=%s", status, nwk) + + try: + device = self.get_device(nwk=nwk) + except KeyError: + return + + # XXX: We cannot handle routing errors directly if there is more than a single + # pending request. Should we delay this matching for 500ms to be able to fix + # this? + if len(device._pending) != 1: + LOGGER.debug( + "Device has %d pending requests, cannot uniquely assign error", + len(device._pending), + ) + return + + key = list(device._pending.keys())[0] + exc = zigpy.exceptions.DeliveryError( + f"Received a routing error: {status!r}", status + ) + + device._pending[key].result.set_exception(exc) From f8feaea1c4ec50595b874787589b0500a657ca07 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:24:41 -0500 Subject: [PATCH 3/8] Properly handle routing status callbacks without digging into devices --- bellows/zigbee/application.py | 80 +++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 428e6399..28cdc1a6 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from collections import defaultdict, deque from datetime import datetime, timezone import logging import os @@ -43,7 +44,11 @@ from bellows.zigbee.device import EZSPEndpoint, EZSPGroupEndpoint import bellows.zigbee.util as util -APS_ACK_TIMEOUT = 120 +APS_ACK_TIMEOUT = 8 + +ROUTE_STATUS_TIMEOUT_MAINS = 0.5 +ROUTE_STATUS_TIMEOUT_BATTERY = 8 + COUNTER_EZSP_BUFFERS = "EZSP_FREE_BUFFERS" COUNTER_NWK_CONFLICTS = "nwk_conflicts" COUNTER_RESET_REQ = "reset_requests" @@ -94,6 +99,9 @@ def __init__(self, config: dict) -> None: self._req_lock = asyncio.Lock() self._packet_capture_channel: int | None = None + self._request_status_handlers: defaultdict[ + t.EmberNodeId, deque[asyncio.Future] + ] = defaultdict(deque) @property def controller_event(self): @@ -837,6 +845,8 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: # Source routing uses address discovery to discover routes aps_frame.options |= t.EmberApsOption.APS_OPTION_ENABLE_ADDRESS_DISCOVERY + route_status_handler_future: asyncio.Future | None = None + async with self._limit_concurrency(priority=packet.priority): message_tag = self.get_sequence() pending_tag = (packet.dst.address, message_tag) @@ -874,6 +884,13 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: message_tag=message_tag, data=packet.data.serialize(), ) + + route_status_handler_future = ( + asyncio.get_running_loop().create_future() + ) + self._request_status_handlers[packet.dst.address].append( + route_status_handler_future + ) elif packet.dst.addr_mode == zigpy.types.AddrMode.Group: status, _ = await self._ezsp.send_multicast( aps_frame=aps_frame, @@ -892,15 +909,18 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: data=packet.data.serialize(), ) - if status != t.sl_Status.OK: - raise zigpy.exceptions.DeliveryError( - f"Failed to enqueue message: {status!r}", status - ) + try: + if status != t.sl_Status.OK: + raise zigpy.exceptions.DeliveryError( + f"Failed to enqueue message: {status!r}", status + ) + + # Only throw a delivery exception for packets sent with NWK addressing. + # https://github.com/home-assistant/core/issues/79832 + # Broadcasts/multicasts don't have ACKs or confirmations either. + if packet.dst.addr_mode != zigpy.types.AddrMode.NWK: + return - # Only throw a delivery exception for packets sent with NWK addressing. - # https://github.com/home-assistant/core/issues/79832 - # Broadcasts/multicasts don't have ACKs or confirmations either. - if packet.dst.addr_mode == zigpy.types.AddrMode.NWK: # Wait for `messageSentHandler` message async with asyncio_timeout(APS_ACK_TIMEOUT): send_status, _ = await req.result @@ -910,6 +930,26 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: f"Failed to deliver message: {send_status!r}", send_status ) + try: + async with asyncio_timeout( + ROUTE_STATUS_TIMEOUT_BATTERY + if packet.extended_timeout + else ROUTE_STATUS_TIMEOUT_MAINS + ): + route_status = await route_status_handler_future + except asyncio.TimeoutError: + route_status = None + + if route_status is not None: + raise zigpy.exceptions.DeliveryError( + f"Received a routing error: {route_status!r}", route_status + ) + finally: + if route_status_handler_future is not None: + self._request_status_handlers[packet.dst.address].remove( + route_status_handler_future + ) + async def permit(self, time_s: int = 60, node: t.EmberNodeId = None) -> None: """Permit joining.""" self.create_task(self._ezsp.pre_permit(time_s), "pre_permit") @@ -1030,24 +1070,8 @@ def handle_route_record( def handle_route_error(self, status: t.sl_Status, nwk: t.EmberNodeId) -> None: LOGGER.debug("Processing route error: status=%s, nwk=%s", status, nwk) - try: - device = self.get_device(nwk=nwk) - except KeyError: + handlers = self._request_status_handlers[nwk] + if not handlers: return - # XXX: We cannot handle routing errors directly if there is more than a single - # pending request. Should we delay this matching for 500ms to be able to fix - # this? - if len(device._pending) != 1: - LOGGER.debug( - "Device has %d pending requests, cannot uniquely assign error", - len(device._pending), - ) - return - - key = list(device._pending.keys())[0] - exc = zigpy.exceptions.DeliveryError( - f"Received a routing error: {status!r}", status - ) - - device._pending[key].result.set_exception(exc) + handlers.popleft().set_result(status) From 7f9497026db84d81e957b9c50f15b57e493b104f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:34:35 -0500 Subject: [PATCH 4/8] Handle relays too --- bellows/zigbee/application.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 28cdc1a6..e3d4e651 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -2,6 +2,7 @@ import asyncio from collections import defaultdict, deque +import contextlib from datetime import datetime, timezone import logging import os @@ -946,9 +947,10 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: ) finally: if route_status_handler_future is not None: - self._request_status_handlers[packet.dst.address].remove( - route_status_handler_future - ) + with contextlib.suppress(ValueError): + self._request_status_handlers[packet.dst.address].remove( + route_status_handler_future + ) async def permit(self, time_s: int = 60, node: t.EmberNodeId = None) -> None: """Permit joining.""" @@ -1067,6 +1069,12 @@ def handle_route_record( ) self.handle_relays(nwk=nwk, relays=relays) + handlers = self._request_status_handlers[nwk] + if not handlers: + return + + handlers.popleft().set_result(None) + def handle_route_error(self, status: t.sl_Status, nwk: t.EmberNodeId) -> None: LOGGER.debug("Processing route error: status=%s, nwk=%s", status, nwk) From 8286570d3a80a3ea56b5da7b077290e0e6f3401c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Feb 2025 19:48:23 -0500 Subject: [PATCH 5/8] Only wait for status notifications for packets with extended timeout --- bellows/zigbee/application.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index e3d4e651..c55e6dd2 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -886,12 +886,13 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: data=packet.data.serialize(), ) - route_status_handler_future = ( - asyncio.get_running_loop().create_future() - ) - self._request_status_handlers[packet.dst.address].append( - route_status_handler_future - ) + if packet.extended_timeout: + route_status_handler_future = ( + asyncio.get_running_loop().create_future() + ) + self._request_status_handlers[packet.dst.address].append( + route_status_handler_future + ) elif packet.dst.addr_mode == zigpy.types.AddrMode.Group: status, _ = await self._ezsp.send_multicast( aps_frame=aps_frame, @@ -931,12 +932,13 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: f"Failed to deliver message: {send_status!r}", send_status ) + # Only wait for routing status notifications for messages sent + # indirectly + if not packet.extended_timeout: + return + try: - async with asyncio_timeout( - ROUTE_STATUS_TIMEOUT_BATTERY - if packet.extended_timeout - else ROUTE_STATUS_TIMEOUT_MAINS - ): + async with asyncio_timeout(ROUTE_STATUS_TIMEOUT_BATTERY): route_status = await route_status_handler_future except asyncio.TimeoutError: route_status = None From a0ab8c2b9cc8ef9affcb4934f75182f5377d5c07 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:10:38 -0500 Subject: [PATCH 6/8] Fix unit tests --- tests/test_application.py | 63 +++++++++++++-------------------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index c0baa95d..98b1d660 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -739,10 +739,7 @@ async def _test_send_packet_unicast( packet, *, statuses=(bellows.types.sl_Status.OK,), - options=( - t.EmberApsOption.APS_OPTION_RETRY - | t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY - ), + options=t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY, ): def send_unicast(*args, **kwargs): nonlocal statuses @@ -841,10 +838,7 @@ async def test_send_packet_unicast_source_route(make_app, packet): await _test_send_packet_unicast( app, packet, - options=( - t.EmberApsOption.APS_OPTION_RETRY - | t.EmberApsOption.APS_OPTION_ENABLE_ADDRESS_DISCOVERY - ), + options=t.EmberApsOption.APS_OPTION_ENABLE_ADDRESS_DISCOVERY, ) app._ezsp._protocol.set_source_route.assert_called_once_with( @@ -871,10 +865,7 @@ async def test_send_packet_unicast_manual_source_route(make_app, packet): await _test_send_packet_unicast( app, packet, - options=( - t.EmberApsOption.APS_OPTION_RETRY - | t.EmberApsOption.APS_OPTION_ENABLE_ADDRESS_DISCOVERY - ), + options=t.EmberApsOption.APS_OPTION_ENABLE_ADDRESS_DISCOVERY, ) app._ezsp.xncp_set_manual_source_route.assert_called_once_with( @@ -886,6 +877,19 @@ async def test_send_packet_unicast_manual_source_route(make_app, packet): async def test_send_packet_unicast_extended_timeout(app, ieee, packet): app.add_device(nwk=packet.dst.address, ieee=ieee) + asyncio.get_running_loop().call_later( + 0.1, + app.ezsp_callback_handler, + "incomingRouteRecordHandler", + { + "source": packet.dst.address, + "sourceEui": ieee, + "lastHopLqi": 123, + "lastHopRssi": -60, + "relayList": [0x1234], + }.values(), + ) + await _test_send_packet_unicast( app, packet.replace(extended_timeout=True), @@ -896,19 +900,6 @@ async def test_send_packet_unicast_extended_timeout(app, ieee, packet): ] -@patch("bellows.zigbee.application.RETRY_DELAYS", [0.01, 0.01, 0.01]) -async def test_send_packet_unicast_retries_success(app, packet): - await _test_send_packet_unicast( - app, - packet, - statuses=( - bellows.types.sl_Status.ALLOCATION_FAILED, - bellows.types.sl_Status.ALLOCATION_FAILED, - bellows.types.sl_Status.OK, - ), - ) - - async def test_send_packet_unicast_unexpected_failure(app, packet): with pytest.raises(zigpy.exceptions.DeliveryError): await _test_send_packet_unicast( @@ -916,17 +907,12 @@ async def test_send_packet_unicast_unexpected_failure(app, packet): ) -@patch("bellows.zigbee.application.RETRY_DELAYS", [0.01, 0.01, 0.01]) async def test_send_packet_unicast_retries_failure(app, packet): with pytest.raises(zigpy.exceptions.DeliveryError): await _test_send_packet_unicast( app, packet, - statuses=( - bellows.types.sl_Status.ALLOCATION_FAILED, - bellows.types.sl_Status.ALLOCATION_FAILED, - bellows.types.sl_Status.ALLOCATION_FAILED, - ), + statuses=(bellows.types.sl_Status.ALLOCATION_FAILED,) * 3, ) @@ -1023,10 +1009,7 @@ async def test_send_packet_broadcast(app, packet): clusterId=packet.cluster_id, sourceEndpoint=packet.src_ep, destinationEndpoint=packet.dst_ep, - options=( - t.EmberApsOption.APS_OPTION_RETRY - | t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY - ), + options=t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY, groupId=0x0000, sequence=packet.tsn, ), @@ -1074,10 +1057,7 @@ async def test_send_packet_broadcast_ignored_delivery_failure(app, packet): clusterId=packet.cluster_id, sourceEndpoint=packet.src_ep, destinationEndpoint=packet.dst_ep, - options=( - t.EmberApsOption.APS_OPTION_RETRY - | t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY - ), + options=t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY, groupId=0x0000, sequence=packet.tsn, ), @@ -1127,10 +1107,7 @@ async def test_send_packet_multicast(app, packet): clusterId=packet.cluster_id, sourceEndpoint=packet.src_ep, destinationEndpoint=packet.dst_ep, - options=( - t.EmberApsOption.APS_OPTION_RETRY - | t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY - ), + options=t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY, groupId=0x1234, sequence=packet.tsn, ), From e3163afb763a1d3a913c36064dc726e9c17fb8eb Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:24:19 -0500 Subject: [PATCH 7/8] Simplify send unit tests --- tests/test_application.py | 53 +++++++++++++++------------------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 98b1d660..de71e869 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -738,31 +738,25 @@ async def _test_send_packet_unicast( app, packet, *, - statuses=(bellows.types.sl_Status.OK,), + status=bellows.types.sl_Status.OK, options=t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY, ): def send_unicast(*args, **kwargs): - nonlocal statuses - - status = statuses[0] - statuses = statuses[1:] - - if not statuses: - asyncio.get_running_loop().call_later( - 0.01, - app.ezsp_callback_handler, - "messageSentHandler", - list( - dict( - type=t.EmberOutgoingMessageType.OUTGOING_DIRECT, - indexOrDestination=0x1234, - apsFrame=sentinel.aps, - messageTag=sentinel.msg_tag, - status=status, - message=b"", - ).values() - ), - ) + asyncio.get_running_loop().call_later( + 0.01, + app.ezsp_callback_handler, + "messageSentHandler", + list( + dict( + type=t.EmberOutgoingMessageType.OUTGOING_DIRECT, + indexOrDestination=0x1234, + apsFrame=sentinel.aps, + messageTag=sentinel.msg_tag, + status=status, + message=b"", + ).values() + ), + ) return [status, 0x12] @@ -771,12 +765,9 @@ def send_unicast(*args, **kwargs): ) app.get_sequence = MagicMock(return_value=sentinel.msg_tag) - expected_unicast_calls = len(statuses) - await app.send_packet(packet) - assert app._ezsp.send_unicast.call_count == expected_unicast_calls - assert app._ezsp.send_unicast.mock_calls[-1] == ( + assert app._ezsp.send_unicast.mock_calls == [ call( nwk=t.EmberNodeId(0x1234), aps_frame=t.EmberApsFrame( @@ -791,7 +782,7 @@ def send_unicast(*args, **kwargs): message_tag=sentinel.msg_tag, data=b"some data", ) - ) + ] assert len(app._pending) == 0 @@ -902,17 +893,13 @@ async def test_send_packet_unicast_extended_timeout(app, ieee, packet): async def test_send_packet_unicast_unexpected_failure(app, packet): with pytest.raises(zigpy.exceptions.DeliveryError): - await _test_send_packet_unicast( - app, packet, statuses=(t.EmberStatus.ERR_FATAL,) - ) + await _test_send_packet_unicast(app, packet, status=t.EmberStatus.ERR_FATAL) async def test_send_packet_unicast_retries_failure(app, packet): with pytest.raises(zigpy.exceptions.DeliveryError): await _test_send_packet_unicast( - app, - packet, - statuses=(bellows.types.sl_Status.ALLOCATION_FAILED,) * 3, + app, packet, status=bellows.types.sl_Status.ALLOCATION_FAILED ) From d2dc6240f409b481bd3361eadea6e2b76da370a3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:53:48 -0500 Subject: [PATCH 8/8] Ignore the coordinator itself --- bellows/zigbee/application.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index c55e6dd2..7013d075 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -933,8 +933,11 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: ) # Only wait for routing status notifications for messages sent - # indirectly - if not packet.extended_timeout: + # indirectly, ignoring the coordinator + if ( + not packet.extended_timeout + or packet.dst.address == self.state.node_info.nwk + ): return try: