diff --git a/tests/test_application.py b/tests/test_application.py index 708c7f4..38a2aa6 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -72,7 +72,6 @@ def zdo_packet(cluster_id: int, data: bytes, src: t.NWK = DEVICE_NWK) -> t.Zigbe src_ep=t.uint8_t(0), dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=t.NWK(0x0000)), dst_ep=t.uint8_t(0), - tsn=t.uint8_t(data[0]), profile_id=t.uint16_t(0x0000), cluster_id=t.uint16_t(cluster_id), data=t.SerializableBytes(data), @@ -84,7 +83,6 @@ def zdo_packet(cluster_id: int, data: bytes, src: t.NWK = DEVICE_NWK) -> t.Zigbe def aps_packet( dst: t.AddrModeAddress, *, - tsn: int = 33, src_ep: int = 1, dst_ep: int = 1, tx_options: t.TransmitOptions = t.TransmitOptions.NONE, @@ -95,7 +93,6 @@ def aps_packet( src_ep=t.uint8_t(src_ep), dst=dst, dst_ep=t.uint8_t(dst_ep), - tsn=t.uint8_t(tsn), profile_id=t.uint16_t(0x0104), cluster_id=t.uint16_t(0x0006), data=t.SerializableBytes(data), @@ -668,7 +665,6 @@ async def test_send_packet_unicast( assert not send.aps_encryption assert not send.sleepy_destination assert send.profile_id == 0x0104 - assert send.aps_seq == 33 assert send.radius == 30 assert send.priority == 0 assert bytes(send.asdu) == b"\x01\x02\x03" @@ -750,7 +746,6 @@ async def test_send_packet_broadcast( send = server.sent(p.SendBroadcast)[-1] assert send.destination == t.NWK(0xFFFC) assert send.dst_ep == 255 - assert send.aps_seq == 33 assert bytes(send.asdu) == b"\x01\x02\x03" @@ -766,7 +761,6 @@ async def test_send_packet_groupcast( send = server.sent(p.SendGroupcast)[-1] assert send.group_id == 0x0002 - assert send.aps_seq == 33 assert bytes(send.asdu) == b"\x01\x02\x03" diff --git a/tests/test_legacy.py b/tests/test_legacy.py index 9212e81..6719e15 100644 --- a/tests/test_legacy.py +++ b/tests/test_legacy.py @@ -577,7 +577,6 @@ async def test_legacy_send_packet( src_ep=t.uint8_t(1), dst=dst, dst_ep=t.uint8_t(1), - tsn=t.uint8_t(33), profile_id=t.uint16_t(0x0104), cluster_id=t.uint16_t(0x0006), data=t.SerializableBytes(b"\x01\x02\x03"), @@ -587,7 +586,6 @@ async def test_legacy_send_packet( request = legacy_server.sent(commands.SendAps)[-1] assert request.data == b"\x01\x02\x03" - assert request.aps_seq == 33 assert request.radius == 30 for field, value in expected.items(): assert getattr(request, field) == value @@ -609,7 +607,6 @@ async def fail(command: commands.SendAps, request_id: int) -> commands.Status: src_ep=t.uint8_t(1), dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=DEVICE_NWK), dst_ep=t.uint8_t(1), - tsn=t.uint8_t(34), profile_id=t.uint16_t(0x0104), cluster_id=t.uint16_t(0x0006), data=t.SerializableBytes(b"\x04"), @@ -638,7 +635,6 @@ async def fail_confirm( src_ep=t.uint8_t(1), dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=DEVICE_NWK), dst_ep=t.uint8_t(1), - tsn=t.uint8_t(35), profile_id=t.uint16_t(0x0104), cluster_id=t.uint16_t(0x0006), data=t.SerializableBytes(b"\x05"), @@ -663,7 +659,6 @@ async def fail_ack(command: commands.SendAps, request_id: int) -> commands.Statu src_ep=t.uint8_t(1), dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=DEVICE_NWK), dst_ep=t.uint8_t(1), - tsn=t.uint8_t(36), profile_id=t.uint16_t(0x0104), cluster_id=t.uint16_t(0x0006), data=t.SerializableBytes(b"\x06"), diff --git a/zigpy_ziggurat/zigbee/application.py b/zigpy_ziggurat/zigbee/application.py index 8bb7989..7a6feac 100644 --- a/zigpy_ziggurat/zigbee/application.py +++ b/zigpy_ziggurat/zigbee/application.py @@ -6,6 +6,7 @@ import logging import math import os +import random import statistics from typing import Any, cast @@ -107,6 +108,10 @@ def __init__(self, config: dict[str, Any]) -> None: self._api: ZigguratApi | None = None self._start_time: datetime | None = None + # Randomized so a reconnect does not replay counters a device still remembers + # for duplicate rejection, which it would ack but not deliver + self._aps_counter = random.randint(0x00, 0xFF) + async def connect(self) -> None: # The device path is either the WebSocket URL of a ziggurat server or the # serial port of a ziggurat firmware (e.g. an ESP32-C6 over USB-Serial-JTAG). @@ -798,6 +803,11 @@ def _handle_received_aps_command(self, command: p.ReceivedAps) -> None: ) self.packet_received(packet) + def _next_aps_counter(self) -> int: + """Allocate the APS counter for the next outgoing frame.""" + self._aps_counter = (self._aps_counter + 1) % 256 + return self._aps_counter + async def send_packet(self, packet: t.ZigbeePacket) -> None: dst = packet.dst assert dst is not None and dst.address is not None @@ -834,6 +844,9 @@ async def send_packet(self, packet: t.ZigbeePacket) -> None: send: p.SendUnicast | p.SendBroadcast | p.SendGroupcast async with self._limit_concurrency(priority=packet.priority): + # Allocated inside the semaphore so counters advance in send order + aps_seq = self._next_aps_counter() + if dst.addr_mode == t.AddrMode.Group: assert destination is not None send = p.SendGroupcast.build( @@ -841,7 +854,7 @@ async def send_packet(self, packet: t.ZigbeePacket) -> None: profile_id=packet.profile_id, cluster_id=packet.cluster_id or 0x0000, src_ep=packet.src_ep or 0, - aps_seq=packet.tsn, + aps_seq=aps_seq, radius=radius, priority=priority, asdu=asdu, @@ -854,7 +867,7 @@ async def send_packet(self, packet: t.ZigbeePacket) -> None: cluster_id=packet.cluster_id or 0x0000, src_ep=packet.src_ep or 0, dst_ep=packet.dst_ep or 0, - aps_seq=packet.tsn, + aps_seq=aps_seq, radius=radius, priority=priority, asdu=asdu, @@ -899,7 +912,7 @@ async def send_packet(self, packet: t.ZigbeePacket) -> None: cluster_id=packet.cluster_id or 0x0000, src_ep=packet.src_ep or 0, dst_ep=packet.dst_ep or 0, - aps_seq=packet.tsn, + aps_seq=aps_seq, radius=radius, priority=priority, route_control=route_control,