55from asyncio import timeout as asyncio_timeout
66import binascii
77from collections .abc import AsyncGenerator , Callable , Iterable
8- from dataclasses import dataclass
98import functools
109import logging
1110import time
12- from typing import TYPE_CHECKING , Any , Final
11+ from typing import TYPE_CHECKING , Any
1312
1413from zigpy .datastructures import PriorityDynamicBoundedSemaphore
15- from zigpy .event .event_base import EventBase
1614import zigpy .state
17- import zigpy .types
1815
1916from bellows .config import CONF_EZSP_POLICIES
2017from bellows .exception import InvalidCommandError
3027MAX_COMMAND_CONCURRENCY = 1
3128
3229
33- @dataclass (frozen = True , kw_only = True )
34- class MessageSentEvent :
35- event_type : Final [str ] = "message_sent"
36-
37- status : t .sl_Status
38- message_type : t .EmberOutgoingMessageType
39- destination : t .uint16_t
40- aps_frame : t .EmberApsFrame
41- message_tag : t .uint8_t
42- message_contents : t .LVBytes
43-
44-
45- @dataclass (frozen = True , kw_only = True )
46- class PacketReceivedEvent :
47- event_type : Final [str ] = "packet_received"
48-
49- packet : zigpy .types .ZigbeePacket
50-
51-
52- @dataclass (frozen = True , kw_only = True )
53- class TrustCenterJoinEvent :
54- event_type : Final [str ] = "trust_center_join"
55-
56- nwk : t .EmberNodeId
57- ieee : t .EUI64
58- device_update_status : t .EmberDeviceUpdate
59- decision : t .EmberJoinDecision
60- parent_nwk : t .EmberNodeId
61-
62-
63- @dataclass (frozen = True , kw_only = True )
64- class RouteRecordEvent :
65- event_type : Final [str ] = "route_record"
66-
67- nwk : t .EmberNodeId
68- ieee : t .EUI64
69- lqi : t .uint8_t
70- rssi : t .int8s
71- relays : t .LVList [t .EmberNodeId ]
72-
73-
74- @dataclass (frozen = True , kw_only = True )
75- class IdConflictEvent :
76- event_type : Final [str ] = "id_conflict"
77-
78- nwk : t .EmberNodeId
79-
80-
81- class ProtocolHandler (EventBase , abc .ABC ):
30+ class ProtocolHandler (abc .ABC ):
8231 """EZSP protocol specific handler."""
8332
8433 COMMANDS = {}
8534 VERSION = None
8635
8736 def __init__ (self , cb_handler : Callable , gateway : Gateway ) -> None :
88- super ().__init__ ()
8937 self ._handle_callback = cb_handler
9038 self ._awaiting = {}
9139 self ._gw = gateway
@@ -231,6 +179,52 @@ def __call__(self, data: bytes) -> None:
231179 if data :
232180 LOGGER .debug ("Frame contains trailing data: %s" , data )
233181
182+ if (
183+ frame_name == "incomingMessageHandler"
184+ and result [1 ].options & t .EmberApsOption .APS_OPTION_FRAGMENT
185+ ):
186+ # Extract received APS frame and sender
187+ aps_frame = result [1 ]
188+ sender = result [4 ]
189+
190+ # The fragment count and index are encoded in the groupId field
191+ fragment_count = (aps_frame .groupId >> 8 ) & 0xFF
192+ fragment_index = aps_frame .groupId & 0xFF
193+
194+ (
195+ complete ,
196+ reassembled ,
197+ frag_count ,
198+ frag_index ,
199+ ) = self ._fragment_manager .handle_incoming_fragment (
200+ sender_nwk = sender ,
201+ aps_sequence = aps_frame .sequence ,
202+ profile_id = aps_frame .profileId ,
203+ cluster_id = aps_frame .clusterId ,
204+ fragment_count = fragment_count ,
205+ fragment_index = fragment_index ,
206+ payload = result [7 ],
207+ )
208+
209+ ack_task = asyncio .create_task (
210+ self ._send_fragment_ack (sender , aps_frame , frag_count , frag_index )
211+ ) # APS Ack
212+
213+ self ._fragment_ack_tasks .add (ack_task )
214+ ack_task .add_done_callback (lambda t : self ._fragment_ack_tasks .discard (t ))
215+
216+ if not complete :
217+ # Do not pass partial data up the stack
218+ LOGGER .debug ("Fragment reassembly not complete. waiting for more data." )
219+ return
220+
221+ # Replace partial data with fully reassembled data
222+ result [7 ] = reassembled
223+
224+ LOGGER .debug (
225+ "Reassembled fragmented message. Proceeding with normal handling."
226+ )
227+
234228 if sequence in self ._awaiting :
235229 expected_id , schema , future = self ._awaiting .pop (sequence )
236230 try :
@@ -252,20 +246,8 @@ def __call__(self, data: bytes) -> None:
252246 sequence ,
253247 self .COMMANDS_BY_ID .get (expected_id , [expected_id ])[0 ],
254248 )
255-
256- return
257-
258- self .handle_parsed_callback (frame_name , result )
259-
260- # Legacy callback system for CLI tools
261- self ._handle_callback (frame_name , result )
262-
263- def handle_parsed_callback (self , frame_name : str , args : list [Any ]) -> None :
264- """Dispatch a callback frame to the appropriate handler method."""
265- handler = getattr (self , f"_handle_{ frame_name } " , None )
266-
267- if handler is not None :
268- handler (* args )
249+ else :
250+ self ._handle_callback (frame_name , result )
269251
270252 async def _send_fragment_ack (
271253 self ,
@@ -293,135 +275,6 @@ async def _send_fragment_ack(
293275 status = await self .sendReply (sender , ackFrame , b"" )
294276 return status [0 ]
295277
296- def _handle_incoming_message (
297- self ,
298- message_type : t .EmberIncomingMessageType ,
299- aps_frame : t .EmberApsFrame ,
300- sender : zigpy .types .NWK ,
301- eui64 : zigpy .types .EUI64 | None ,
302- binding_index : t .uint8_t ,
303- address_index : t .uint8_t ,
304- lqi : t .uint8_t ,
305- rssi : t .int8s ,
306- timestamp : t .uint32_t | None ,
307- message : t .LVBytes ,
308- ) -> None :
309- """Handle incomingMessageHandler callback and maybe return a packet."""
310-
311- if aps_frame .options & t .EmberApsOption .APS_OPTION_FRAGMENT :
312- fragment_count = (aps_frame .groupId >> 8 ) & 0xFF
313- fragment_index = aps_frame .groupId & 0xFF
314-
315- (
316- complete ,
317- reassembled ,
318- frag_count ,
319- frag_index ,
320- ) = self ._fragment_manager .handle_incoming_fragment (
321- sender_nwk = sender ,
322- aps_sequence = aps_frame .sequence ,
323- profile_id = aps_frame .profileId ,
324- cluster_id = aps_frame .clusterId ,
325- fragment_count = fragment_count ,
326- fragment_index = fragment_index ,
327- payload = message ,
328- )
329-
330- ack_task = asyncio .create_task (
331- self ._send_fragment_ack (sender , aps_frame , frag_count , frag_index )
332- )
333- self ._fragment_ack_tasks .add (ack_task )
334- ack_task .add_done_callback (lambda t : self ._fragment_ack_tasks .discard (t ))
335-
336- if not complete :
337- LOGGER .debug ("Fragment reassembly not complete, waiting for more data" )
338- return
339-
340- LOGGER .debug ("Reassembled fragmented message, proceeding with handling" )
341- message = reassembled
342-
343- # Determine destination address based on message type
344- if message_type == t .EmberIncomingMessageType .INCOMING_BROADCAST :
345- dst = zigpy .types .AddrModeAddress (
346- addr_mode = zigpy .types .AddrMode .Broadcast ,
347- address = zigpy .types .BroadcastAddress .ALL_ROUTERS_AND_COORDINATOR ,
348- )
349- elif message_type == t .EmberIncomingMessageType .INCOMING_MULTICAST :
350- dst = zigpy .types .AddrModeAddress (
351- addr_mode = zigpy .types .AddrMode .Group ,
352- address = aps_frame .groupId ,
353- )
354- elif message_type == t .EmberIncomingMessageType .INCOMING_UNICAST :
355- dst = None # We don't know the current NWK here
356- else :
357- LOGGER .debug ("Ignoring message type: %r" , message_type )
358- return
359-
360- self .emit (
361- PacketReceivedEvent .event_type ,
362- PacketReceivedEvent (
363- packet = zigpy .types .ZigbeePacket (
364- src = zigpy .types .AddrModeAddress (
365- addr_mode = zigpy .types .AddrMode .NWK ,
366- address = zigpy .types .NWK (sender ),
367- ),
368- src_ep = aps_frame .sourceEndpoint ,
369- dst = dst ,
370- dst_ep = aps_frame .destinationEndpoint ,
371- tsn = aps_frame .sequence ,
372- profile_id = aps_frame .profileId ,
373- cluster_id = aps_frame .clusterId ,
374- data = zigpy .types .SerializableBytes (message ),
375- lqi = lqi ,
376- rssi = rssi ,
377- )
378- ),
379- )
380-
381- def _handle_trustCenterJoinHandler (
382- self ,
383- nwk : t .EmberNodeId ,
384- ieee : t .EUI64 ,
385- device_update_status : t .EmberDeviceUpdate ,
386- decision : t .EmberJoinDecision ,
387- parent_nwk : t .EmberNodeId ,
388- ) -> None :
389- self .emit (
390- TrustCenterJoinEvent .event_type ,
391- TrustCenterJoinEvent (
392- nwk = nwk ,
393- ieee = ieee ,
394- device_update_status = device_update_status ,
395- decision = decision ,
396- parent_nwk = parent_nwk ,
397- ),
398- )
399-
400- def _handle_incomingRouteRecordHandler (
401- self ,
402- nwk : t .EmberNodeId ,
403- ieee : t .EUI64 ,
404- lqi : t .uint8_t ,
405- rssi : t .int8s ,
406- relays : t .LVList [t .EmberNodeId ],
407- ) -> None :
408- self .emit (
409- RouteRecordEvent .event_type ,
410- RouteRecordEvent (
411- nwk = nwk ,
412- ieee = ieee ,
413- lqi = lqi ,
414- rssi = rssi ,
415- relays = relays ,
416- ),
417- )
418-
419- def _handle_idConflictHandler (self , nwk : t .EmberNodeId ) -> None :
420- self .emit (
421- IdConflictEvent .event_type ,
422- IdConflictEvent (nwk = nwk ),
423- )
424-
425278 def __getattr__ (self , name : str ) -> Callable :
426279 if name not in self .COMMANDS :
427280 raise AttributeError (f"{ name } not found in COMMANDS" )
0 commit comments