Difference in behaviour between bellows and zigpy-znp while processing frame
We've had some customers reporting issues with one of our products ( https://candeo.io/store/zigbee-5-button-remote-with-rotary-dial ) under ZHA.
Further investigation has narrowed the problem down to customers using TI based Zigbee radios, which use the zigpy-znp library.
Customers using Ember based Zigbee radios, using the bellows library, don't experience the problem.
bellows:
2025-10-17 11:55:46.266 DEBUG (bellows.thread_0) [bellows.ash] Received data 6408b1a97d312a15b65897b524ab1593499ce08f6de49498befac70b88fd7d5e 2025-10-17 11:55:46.270 DEBUG (bellows.thread_0) [bellows.ash] Received data 2fa4e997757e 2025-10-17 11:55:46.271 DEBUG (bellows.thread_0) [bellows.ash] Received frame DataFrame(frm_num=6, re_tx=0, ack_num=4, ezsp_frame=b'J\x90\x01E\x00\x00\x04\x01\x03\xff\x01\x01@\x01\x00\x00\xae\xa8\xc6\tZ\xff5\x07\x01h\x01\x01\x00\x10\x03\x02') 2025-10-17 11:55:46.272 DEBUG (bellows.thread_0) [bellows.ash] Sending frame AckFrame(res=0, ncp_ready=0, ack_num=7) + FLAG 2025-10-17 11:55:46.273 DEBUG (bellows.thread_0) [bellows.ash] Sending data 87009f7e 2025-10-17 11:55:46.280 DEBUG (MainThread) [bellows.ezsp.protocol] Received command incomingMessageHandler: {'type': <EmberIncomingMessageType.INCOMING_UNICAST: 0>, 'apsFrame': EmberApsFrame(profileId=260, clusterId=65283, sourceEndpoint=1, destinationEndpoint=1, options=<EmberApsOption.APS_OPTION_RETRY|APS_OPTION_ENABLE_ROUTE_DISCOVERY: 320>, groupId=0, sequence=174), 'lastHopLqi': 168, 'lastHopRssi': -58, 'sender': 0x5A09, 'bindingIndex': 255, 'addressIndex': 53, 'messageContents': b'\x01h\x01\x01\x00\x10\x03'} 2025-10-17 11:55:46.281 DEBUG (MainThread) [bellows.ezsp.protocol] Frame contains trailing data: b'\x02' 2025-10-17 11:55:46.282 DEBUG (MainThread) [bellows.zigbee.application] Received incomingMessageHandler frame with [<EmberIncomingMessageType.INCOMING_UNICAST: 0>, EmberApsFrame(profileId=260, clusterId=65283, sourceEndpoint=1, destinationEndpoint=1, options=<EmberApsOption.APS_OPTION_RETRY|APS_OPTION_ENABLE_ROUTE_DISCOVERY: 320>, groupId=0, sequence=174), 168, -58, 0x5A09, 255, 53, b'\x01h\x01\x01\x00\x10\x03']
zigpy-znp:
2025-10-17 00:28:20.559 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2025, 10, 16, 20, 28, 20, 557536, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xE132), src_ep=0, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=1, profile_id=0, cluster_id=65283, data=Serialized[b'\x01t\x01\x03\x01\x01\x19'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=None, rssi=None) 2025-10-17 00:28:20.559 DEBUG (MainThread) [zigpy.application] Could not parse ZDO message from packet 2025-10-17 00:28:20.560 DEBUG (MainThread) [zigpy.device] [0xe132] Failed to parse packet ZigbeePacket(timestamp=datetime.datetime(2025, 10, 16, 20, 28, 20, 557536, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xE132), src_ep=0, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=1, profile_id=0, cluster_id=65283, data=Serialized[b'\x01t\x01\x03\x01\x01\x19'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=None, rssi=None) Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy/device.py", line 824, in packet_received cmd = self._parse_packet_command(packet, endpoint, zcl_cluster) File "/usr/local/lib/python3.13/site-packages/zigpy/device.py", line 768, in _parse_packet_command _, cmd = endpoint.deserialize(packet.cluster_id, data) ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.13/site-packages/zigpy/zdo/init.py", line 42, in deserialize raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") ValueError: Invalid ZDO cluster ID: 0xFF03
The above exception was the direct cause of the following exception:
zigpy.exceptions.ParsingError 2025-10-17 00:28:20.866 ERROR (MainThread) [zigpy_znp.uart] Received an exception while passing frame to API: TransportFrame(payload=GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\x32\xE1\x75\x01\x03\x01\x01\x01')) Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/uart.py", line 56, in data_received self._api.frame_received(frame.payload) ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.13/site-packages/zigpy_znp/api.py", line 860, in frame_received command = command_cls.from_frame(frame, align=self.nvram.align_structs) File "/usr/local/lib/python3.13/site-packages/zigpy_znp/types/commands.py", line 433, in from_frame raise ValueError( f"Frame {frame} contains trailing data after parsing: {data}" ) ValueError: Frame GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\x32\xE1\x75\x01\x03\x01\x01\x01') contains trailing data after parsing: b'\x01' 2025-10-17 00:28:20.868 WARNING (MainThread) [zigpy_znp.zigbee.application] Failed to deserialize ZDO packet Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/zigbee/application.py", line 443, in on_zdo_message zdo_hdr, zdo_args = self._device.zdo.deserialize( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ cluster_id=packet.cluster_id, data=packet.data.serialize() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/usr/local/lib/python3.13/site-packages/zigpy/zdo/init.py", line 42, in deserialize raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") ValueError: Invalid ZDO cluster ID: 0xFF03
I guess that ultimately the device may have a bug if it's sending spurious data in the frame, but it does work OK across all our other supported platforms (Z2M, Hubitat, SmartThings, etc).
It does seem that there is a difference in behaviour between how bellows and zigpy-znp are handling frames that contain trailing data?
bellows logs an error if the frame contains trailing data, but otherwise moves on and processes the data:
https://github.com/zigpy/bellows/blob/cdd12173f044da35962bc433dc4f7b250e0fba52/bellows/ezsp/protocol.py#L184
zigpy-znp actually raises an error, so further processing of the data stops:
https://github.com/zigpy/zigpy-znp/blob/fd9f7eb65b7d3262c739d864a7ea483234996999/zigpy_znp/types/commands.py#L433
Should the behaviour between bellows & zigpy-znp be consistent?
Can you attach the complete logs from both radio libraries and a packet capture, if possible? The two logs are saying very different things.
$ pip install zigpy-cli
$ zigpy radio ezsp /dev/cu.SLAB_USBtoUART packet-capture -c 15 -o capture.pcap
The bellows library is logging this:
2025-10-17 11:55:46.282 DEBUG (MainThread) [bellows.zigbee.application] Received incomingMessageHandler frame with [<EmberIncomingMessageType.INCOMING_UNICAST: 0>, EmberApsFrame(profileId=260, clusterId=65283, sourceEndpoint=1, destinationEndpoint=1, options=<EmberApsOption.APS_OPTION_RETRY|APS_OPTION_ENABLE_ROUTE_DISCOVERY: 320>, groupId=0, sequence=174), 168, -58, 0x5A09, 255, 53, b'\x01h\x01\x01\x00\x10\x03']
So the firmware is saying it received Zigbee packet coming in with src_ep=1 and dst_ep=1, with cluster_id=0xFF03.
But zigpy-znp is reporting a ZDO parsing failure, which means that it was sent a packet with src_ep=0 and dst_ep=0, also with cluster_id=0xFF03. That doesn't match up and is the source of the ZDO parsing failure.
@puddly thanks for the fast reply!
Unfortunately I've only got the zigpy-znp log snippet from the customer for their TI based radio, so can't see the raw packet contents.
There's another few customers with similar log snippets:
2025-09-28 17:37:46.574 ERROR (MainThread) [zigpy_znp.uart] Received an exception while passing frame to API: TransportFrame(payload=GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xE8\x08\x48\x01\x01\x00\x10\x01')) Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/uart.py", line 56, in data_received self._api.frame_received(frame.payload)
File "/usr/local/lib/python3.13/site-packages/zigpy_znp/api.py", line 860, in frame_received command = command_cls.from_frame(frame, align=self.nvram.align_structs) File "/usr/local/lib/python3.13/site-packages/zigpy_znp/types/commands.py", line 433, in from_frame raise ValueError( f"Frame {frame} contains trailing data after parsing: {data}" ) ValueError: Frame GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xE8\x08\x48\x01\x01\x00\x10\x01') contains trailing data after parsing: b'\x01' 2025-09-28 17:37:46.577 WARNING (MainThread) [zigpy_znp.zigbee.application] Failed to deserialize ZDO packet Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/zigbee/application.py", line 443, in on_zdo_message zdo_hdr, zdo_args = self._device.zdo.deserialize(
cluster_id=packet.cluster_id, data=packet.data.serialize() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/usr/local/lib/python3.13/site-packages/zigpy/zdo/init.py", line 42, in deserialize raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") ValueError: Invalid ZDO cluster ID: 0xFF03
2025-10-07 20:10:42.890 ERROR (MainThread) [zigpy_znp.uart] Received an exception while passing frame to API: TransportFrame(payload=GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xF1\xF4\x4C\x01\x03\x01\x01\x03')) Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/uart.py", line 56, in data_received self._api.frame_received(frame.payload) ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.13/site-packages/zigpy_znp/api.py", line 860, in frame_received command = command_cls.from_frame(frame, align=self.nvram.align_structs) File "/usr/local/lib/python3.13/site-packages/zigpy_znp/types/commands.py", line 433, in from_frame raise ValueError( f"Frame {frame} contains trailing data after parsing: {data}" ) ValueError: Frame GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xF1\xF4\x4C\x01\x03\x01\x01\x03') contains trailing data after parsing: b'\x03' 2025-10-07 20:10:42.891 DEBUG (MainThread) [zigpy_znp.api] Received command: ZDO.MsgCbIncoming.Callback(Src=0xF4F1, IsBroadcast=<Bool.false: 0>, ClusterId=65283, SecurityUse=0, TSN=1, MacDst=0x0000, Data=b'\x4C\x01\x03\x01\x01\x03') 2025-10-07 20:10:43.011 WARNING (MainThread) [zigpy_znp.zigbee.application] Failed to deserialize ZDO packet Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/zigbee/application.py", line 443, in on_zdo_message zdo_hdr, zdo_args = self._device.zdo.deserialize( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ cluster_id=packet.cluster_id, data=packet.data.serialize() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/usr/local/lib/python3.13/site-packages/zigpy/zdo/init.py", line 42, in deserialize raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") ValueError: Invalid ZDO cluster ID: 0xFF03 2025-10-07 20:10:43.056 ERROR (MainThread) [zigpy_znp.uart] Received an exception while passing frame to API: TransportFrame(payload=GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xF1\xF4\x4D\x01\x03\x01\x01\x01')) Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/uart.py", line 56, in data_received self._api.frame_received(frame.payload) ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.13/site-packages/zigpy_znp/api.py", line 860, in frame_received command = command_cls.from_frame(frame, align=self.nvram.align_structs) File "/usr/local/lib/python3.13/site-packages/zigpy_znp/types/commands.py", line 433, in from_frame raise ValueError( f"Frame {frame} contains trailing data after parsing: {data}" ) ValueError: Frame GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xF1\xF4\x4D\x01\x03\x01\x01\x01') contains trailing data after parsing: b'\x01' 2025-10-07 20:10:43.057 WARNING (MainThread) [zigpy_znp.zigbee.application] Failed to deserialize ZDO packet Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/zigbee/application.py", line 443, in on_zdo_message zdo_hdr, zdo_args = self._device.zdo.deserialize( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ cluster_id=packet.cluster_id, data=packet.data.serialize() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/usr/local/lib/python3.13/site-packages/zigpy/zdo/init.py", line 42, in deserialize raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") ValueError: Invalid ZDO cluster ID: 0xFF03
So is the trailing data error not the cause? It seemed like the trailing data error was occuring first, from the log snippets.
The bellows log is from my own test system, as I'm using an ember based radio.
As far as I can see, the only difference is the radio being used.
It sounds like I need to replicate the issue myself with the device and a TI radio, along with a full log & packet capture, so I'll see what I can do!
@puddly I've gathered some more information now.
I'm testing in two separate HA instances in different docker containers, both with ZHA, one with a TI radio and one with an ember radio.
The device is un-paired and then re-paired between each HA instance.
Hopefully this is enough information for you to see what's causing the issue, but please let me know if you need anything further.
Here's a WireShark capture of the command packet from the device when using a TI radio & zigpy-znp:
Frame:
48 02 00 00 b4 71 1e 54 28 2e 70 01 00 f4 3a 36 fe ff 27 71 84 00 5b 9d 23 fd 3b c3 7d c3 96 a3 30 99 75 b2 ac e6 a0 8c a0
Decrypted Zigbee payload:
40 00 03 ff 04 01 01 ae 01 27 01 01 00 10 01
HA log with ZHA debugging enabled:
2025-10-21 15:08:21.422 ERROR (MainThread) [zigpy_znp.uart] Received an exception while passing frame to API: TransportFrame(payload=GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xB4\x71\x27\x01\x01\x00\x10\x01')) Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/uart.py", line 56, in data_received self._api.frame_received(frame.payload) ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.13/site-packages/zigpy_znp/api.py", line 860, in frame_received command = command_cls.from_frame(frame, align=self.nvram.align_structs) File "/usr/local/lib/python3.13/site-packages/zigpy_znp/types/commands.py", line 433, in from_frame raise ValueError( f"Frame {frame} contains trailing data after parsing: {data}" ) ValueError: Frame GeneralFrame(header=CommandHeader(id=0x83, subsystem=Subsystem.ZDO, type=CommandType.AREQ), data=b'\xB4\x71\x27\x01\x01\x00\x10\x01') contains trailing data after parsing: b'\x01' 2025-10-21 15:08:21.435 DEBUG (MainThread) [zigpy_znp.api] Received command: ZDO.MsgCbIncoming.Callback(Src=0x71B4, IsBroadcast=<Bool.false: 0>, ClusterId=65283, SecurityUse=0, TSN=1, MacDst=0x0000, Data=b'\x27\x01\x01\x00\x10\x01') 2025-10-21 15:08:21.440 WARNING (MainThread) [zigpy_znp.zigbee.application] Failed to deserialize ZDO packet Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy_znp/zigbee/application.py", line 443, in on_zdo_message zdo_hdr, zdo_args = self._device.zdo.deserialize( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ cluster_id=packet.cluster_id, data=packet.data.serialize() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/usr/local/lib/python3.13/site-packages/zigpy/zdo/init.py", line 42, in deserialize raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") ValueError: Invalid ZDO cluster ID: 0xFF03 2025-10-21 15:08:21.452 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2025, 10, 21, 14, 8, 21, 440531, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x71B4), src_ep=0, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=1, profile_id=0, cluster_id=65283, data=Serialized[b"\x01'\x01\x01\x00\x10\x01"], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=None, rssi=None) 2025-10-21 15:08:21.454 DEBUG (MainThread) [zigpy.application] Could not parse ZDO message from packet 2025-10-21 15:08:21.458 DEBUG (MainThread) [zigpy.device] [0x71b4] Failed to parse packet ZigbeePacket(timestamp=datetime.datetime(2025, 10, 21, 14, 8, 21, 440531, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x71B4), src_ep=0, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=1, profile_id=0, cluster_id=65283, data=Serialized[b"\x01'\x01\x01\x00\x10\x01"], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=None, rssi=None) Traceback (most recent call last): File "/usr/local/lib/python3.13/site-packages/zigpy/device.py", line 824, in packet_received cmd = self._parse_packet_command(packet, endpoint, zcl_cluster) File "/usr/local/lib/python3.13/site-packages/zigpy/device.py", line 768, in _parse_packet_command _, cmd = endpoint.deserialize(packet.cluster_id, data) ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.13/site-packages/zigpy/zdo/init.py", line 42, in deserialize raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") ValueError: Invalid ZDO cluster ID: 0xFF03
The above exception was the direct cause of the following exception:
zigpy.exceptions.ParsingError
Here's a WireShark capture of the command packet from the device when using an ember radio & bellows:
Frame:
48 22 00 00 44 30 1e fa 28 31 60 01 00 f4 3a 36 fe ff 27 71 84 00 c2 70 3a e3 a0 a6 37 47 8a d1 26 0a 25 d2 0b 40 5d 0b f9
Decrypted Zigbee payload:
40 00 03 ff 04 01 01 85 01 11 01 01 00 10 01
HA log with ZHA debugging enabled:
2025-10-21 12:43:59.993 DEBUG (bellows.thread_0) [bellows.ash] Received data 1468b1a97d312a15b65897b524aa1593499ccbe366a9fe9874fac77288fd7d5e 2025-10-21 12:43:59.997 DEBUG (bellows.thread_0) [bellows.ash] Received data 2fa6e9d83e7e 2025-10-21 12:43:59.998 DEBUG (bellows.thread_0) [bellows.ash] Received frame DataFrame(frm_num=1, re_tx=0, ack_num=4, ezsp_frame=b'*\x90\x01E\x00\x00\x04\x01\x03\xff\x01\x00@\x01\x00\x00\x85\xc4\xcdD0\xff\xff\x07\x01\x11\x01\x01\x00\x10\x01\x02') 2025-10-21 12:43:59.999 DEBUG (bellows.thread_0) [bellows.ash] Sending frame AckFrame(res=0, ncp_ready=0, ack_num=2) + FLAG 2025-10-21 12:44:00.000 DEBUG (bellows.thread_0) [bellows.ash] Sending data 82503a7e 2025-10-21 12:44:00.009 DEBUG (MainThread) [bellows.ezsp.protocol] Received command incomingMessageHandler: {'type': <EmberIncomingMessageType.INCOMING_UNICAST: 0>, 'apsFrame': EmberApsFrame(profileId=260, clusterId=65283, sourceEndpoint=1, destinationEndpoint=0, options=<EmberApsOption.APS_OPTION_RETRY|APS_OPTION_ENABLE_ROUTE_DISCOVERY: 320>, groupId=0, sequence=133), 'lastHopLqi': 196, 'lastHopRssi': -51, 'sender': 0x3044, 'bindingIndex': 255, 'addressIndex': 255, 'messageContents': b'\x01\x11\x01\x01\x00\x10\x01'} 2025-10-21 12:44:00.011 DEBUG (MainThread) [bellows.ezsp.protocol] Frame contains trailing data: b'\x02' 2025-10-21 12:44:00.012 DEBUG (MainThread) [bellows.zigbee.application] Received incomingMessageHandler frame with [<EmberIncomingMessageType.INCOMING_UNICAST: 0>, EmberApsFrame(profileId=260, clusterId=65283, sourceEndpoint=1, destinationEndpoint=0, options=<EmberApsOption.APS_OPTION_RETRY|APS_OPTION_ENABLE_ROUTE_DISCOVERY: 320>, groupId=0, sequence=133), 196, -51, 0x3044, 255, 255, b'\x01\x11\x01\x01\x00\x10\x01'] 2025-10-21 12:44:00.014 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2025, 10, 21, 11, 44, 0, 14486, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x3044), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=133, profile_id=260, cluster_id=65283, data=Serialized[b'\x01\x11\x01\x01\x00\x10\x01'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=196, rssi=-51) 2025-10-21 12:44:00.016 DEBUG (MainThread) [zigpy.application] Could not parse ZDO message from packet 2025-10-21 12:44:00.028 WARNING (MainThread) [zigpy.device] Cluster 0xff03 on has incorrect direction (got <Direction.Client_to_Server: 0> for <ClusterType.Server: 0> cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640 2025-10-21 12:44:00.032 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] Received ZCL frame: b'\x01\x11\x01\x01\x00\x10\x01' 2025-10-21 12:44:00.036 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x01>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=17, command_id=1, *direction=<Direction.Client_to_Server: 0>) 2025-10-21 12:44:00.043 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] Decoded ZCL frame: CandeoCZBSR5BRSceneSwitchRemoteCluster:candeo_scene_switch_remote(field_1=1, field_2=0, field_3=16, field_4=1) 2025-10-21 12:44:00.046 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] CandeoCZBSR5BRSceneSwitchRemote: Received command 0x01 (TSN 17): candeo_scene_switch_remote(field_1=1, field_2=0, field_3=16, field_4=1) 2025-10-21 12:44:00.048 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] CandeoCZBSR5BRSceneSwitchRemote: handle_cluster_request called 2025-10-21 12:44:00.049 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] CandeoCZBSR5BRSceneSwitchRemote: sending default response 2025-10-21 12:44:00.053 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] CandeoCZBSR5BRSceneSwitchRemote: received field_1 - [1] field_2 - [0] field_3 - [16] field_4 - [1] 2025-10-21 12:44:00.054 DEBUG (MainThread) [zigpy.zcl] [0x3044:1:0xff03] CandeoCZBSR5BRSceneSwitchRemote: button_number - [centre_button_] button_action - [click] 2025-10-21 12:44:00.056 DEBUG (MainThread) [zha] Emitting event zha_event with data ZHAEvent(device_ieee=84:71:27:ff:fe:36:3a:f4, unique_id='84:71:27:ff:fe:36:3a:f4', data={'unique_id': '84:71:27:ff:fe:36:3a:f4:1:0xff03', 'endpoint_id': 1, 'cluster_id': 65283, 'command': 'centre_button_click', 'args': [], 'params': {}}, event_type='zha_event', event='zha_event') (1 listeners) 2025-10-21 12:44:00.057 DEBUG (MainThread) [zha] (ZHADeviceProxy) handling event protocol for event: ZHAEvent(device_ieee=84:71:27:ff:fe:36:3a:f4, unique_id='84:71:27:ff:fe:36:3a:f4', data={'unique_id': '84:71:27:ff:fe:36:3a:f4:1:0xff03', 'endpoint_id': 1, 'cluster_id': 65283, 'command': 'centre_button_click', 'args': [], 'params': {}}, event_type='zha_event', event='zha_event')
Thanks! So the destination endpoint is 0 in both cases.
This really isn't a supported configuration and I think very much against what the spec allows. Endpoint 0 is only for ZDO commands.
The reason we can't easily work around this is because Z-Stack firmware intercepts and rewrites almost all ZDO commands. There is a global ZDO.MsgCbIncoming callback that we can subscribe to to get access to the raw ZDO packet but it contains no endpoint information: ZDO is assumed to be from endpoint 0 to endpoint 0 and that's the only thing we have access to in zigpy-znp. Your device sending its packet with a source endpoint of 1 is just stripped out by the firmware and lost.
Indeed, it's another somewhat buggy device :-(
Unfortunately we missed the issue during testing as we're only using ember radios internally so the issue did not present itself.
I wasn't expecting there to be differences in how the underlying radio libraries worked, we'll bear that in mind for the future and make sure we test accordingly.
We'll reach out to the manufacturer to see if they can update the device to correct the problem, in the meantime please let us know if you can think of any way to work around it!
Hi again @puddly!
We may have a workaround from our side, it transpires that if you bind the Cluster to the cordinator with a valid endpoint then it sends an additional correct command packet. So the buggy one is still received and discarded, but a valid one follows shortly after.
Bad packet:
Good packet:
In order to use this though, we need to be able to bind to the manufacturer specific cluster that's being used.
Are you able to point me to any examples of how to do this from a quirk?
I had a look but there didn't seem to be an easy way as it doesn't look like bind() is called for a custom cluster that doesn't inherit from something else.
For testing we came up with a hacky way to do it, by piggy-backing off a custom power configuration cluster like so:
class CandeoCustomPower(CustomCluster, PowerConfiguration):
"""custom power"""
async def bind(self):
"""Bind cluster."""
self.debug("CandeoCZBSR5BRSceneSwitchRemote: bind called")
result = await super().bind()
self.debug("CandeoCZBSR5BRSceneSwitchRemote: horrible hack to bind the manfacturer cluster")
application = self._endpoint.device.application
dstaddr = zdotypes.MultiAddress()
dstaddr.addrmode = 3
dstaddr.ieee = application.state.node_info.ieee
dstaddr.endpoint = self._endpoint.endpoint_id
await self._endpoint.device.zdo.Bind_req(
self._endpoint.device.ieee,
self._endpoint.endpoint_id,
0xFF03,
dstaddr,
)
return result
CC @TheJulianJES. Do you have any suggestions for quirks APIs to support this use case (if it isn't already)?
You should be able to just call .bind() on your manufacturer cluster. ZHA calls bind() on some clusters automatically, so you can do it there, like in your example, but it depends on the cluster_id. You should be able to just use await self.endpoint.in_clusters[0xFF03].bind() in your example.
I'd recommend to use apply_custom_configuration. It's a method first called in the CustomDevice object, then in all(!) individual clusters. So, if you add your a cluster class for your manufacturer cluster, implement apply_custom_configuration, and you should be able to just call await self.bind() in that method IIRC.
For some references, see:
- https://github.com/search?q=repo%3Azigpy%2Fzha-device-handlers%20apply_custom_configuration&type=code
- https://github.com/search?q=repo%3Azigpy%2Fzha-device-handlers%20bind()&type=code
So, something like this might work:
class CandeoCluster(CustomCluster):
"""Custom manufacturer specific cluster to bind on configuration."""
cluster_id = 0xFF03
async def apply_custom_configuration(self, *args, **kwargs):
"""Apply custom configuration to bind cluster."""
await self.bind()
and in your v2 quirk, add:
.replaces(CandeoCluster)
But right now, we don't have an easy quirk v2 method yet to notify ZHA to bind a certain cluster. This is something I wanna do in the future though. The only caveat the method above currently has is that it won't show the cluster is bound in the reconfiguration UI, but that's purely visual and shouldn't really matter. This would be avoided with a proper quirks v2 method.
Thanks for the pointers @TheJulianJES!
You should be able to just use
await self.endpoint.in_clusters[0xFF03].bind()in your example.
Ah, I thought there'd probably be a way to do it without having to build the bind packet manually but I'd tried a few variations on the above but didn't hit the right combination.
I'd recommend to use
apply_custom_configuration.
I'd completely forgotten about that method, that will be perfect.
Our workaround means under bellows we'd get duplicate packets coming through (one for the dodgy ZDO one and one for the valid ZCL one), but we can filter those out in the quirk as they both have the same seqence number.
Thanks again to both of you for your help!