22import logging
33import os
44
5+ from bellows .exception import ControllerError , EzspError
6+ import bellows .multicast
7+ import bellows .types as t
8+ import bellows .zigbee .util
59from serial import SerialException
6- from zigpy .quirks import CustomDevice , CustomEndpoint
7- from zigpy .types import BroadcastAddress
10+ import voluptuous as vol
811import zigpy .application
912import zigpy .device
13+ from zigpy .quirks import CustomDevice , CustomEndpoint
14+ from zigpy .types import BroadcastAddress
1015import zigpy .util
1116import zigpy .zdo
1217import zigpy .zdo .types as zdo_t
1318
14- import bellows .types as t
15- import bellows .zigbee .util
16- from bellows .exception import ControllerError , EzspError
17- import bellows .multicast
18-
1919APS_ACK_TIMEOUT = 120
20+ CONF_PARAM_SRC_RTG = "source_routing"
21+ CONFIG_SCHEMA = zigpy .application .CONFIG_SCHEMA .extend (
22+ {vol .Optional (CONF_PARAM_SRC_RTG , default = False ): bellows .zigbee .util .cv_boolean }
23+ )
2024EZSP_DEFAULT_RADIUS = 0
2125EZSP_MULTICAST_NON_MEMBER_RADIUS = 3
2226MAX_WATCHDOG_FAILURES = 4
27+ MTOR_MIN_INTERVAL = 600
28+ MTOR_MAX_INTERVAL = 1800
2329RESET_ATTEMPT_BACKOFF_TIME = 5
2430WATCHDOG_WAKE_PERIOD = 10
2531
2935class ControllerApplication (zigpy .application .ControllerApplication ):
3036 direct = t .EmberOutgoingMessageType .OUTGOING_DIRECT
3137
32- def __init__ (self , ezsp , database_file = None ):
33- super ().__init__ (database_file = database_file )
38+ def __init__ (self , ezsp , database_file = None , config = {} ):
39+ super ().__init__ (database_file = database_file , config = CONFIG_SCHEMA ( config ) )
3440 self ._ctrl_event = asyncio .Event ()
3541 self ._ezsp = ezsp
3642 self ._multicast = bellows .multicast .Multicast (ezsp )
3743 self ._pending = zigpy .util .Requests ()
3844 self ._watchdog_task = None
3945 self ._reset_task = None
4046 self ._in_flight_msg = None
47+ self ._tx_options = t .EmberApsOption (
48+ t .EmberApsOption .APS_OPTION_RETRY
49+ | t .EmberApsOption .APS_OPTION_ENABLE_ROUTE_DISCOVERY
50+ )
51+ self .use_source_routing = self .config [CONF_PARAM_SRC_RTG ]
4152
4253 @property
4354 def controller_event (self ):
@@ -124,6 +135,7 @@ async def startup(self, auto_form=False):
124135 await self .initialize ()
125136 e = self ._ezsp
126137
138+ await self .set_source_routing ()
127139 v = await e .networkInit ()
128140 if v [0 ] != t .EmberStatus .SUCCESS :
129141 if not auto_form :
@@ -153,8 +165,25 @@ async def startup(self, auto_form=False):
153165
154166 self .handle_join (self .nwk , self .ieee , 0 )
155167 LOGGER .debug ("EZSP nwk=0x%04x, IEEE=%s" , self ._nwk , str (self ._ieee ))
168+
156169 await self .multicast .startup (self .get_device (self .ieee ))
157170
171+ async def set_source_routing (self ) -> None :
172+ res = await self ._ezsp .setConcentrator (
173+ self .use_source_routing ,
174+ t .EmberConcentratorType .HIGH_RAM_CONCENTRATOR ,
175+ MTOR_MIN_INTERVAL ,
176+ MTOR_MAX_INTERVAL ,
177+ 2 ,
178+ 5 ,
179+ 0 ,
180+ )
181+ LOGGER .debug ("Set concentrator type: %s" , res )
182+ if res [0 ] != t .EmberStatus .SUCCESS :
183+ LOGGER .warning (
184+ "Couldn't set concentrator type %s: %s" , self .use_source_routing , res
185+ )
186+
158187 async def shutdown (self ):
159188 """Shutdown and cleanup ControllerApplication."""
160189 LOGGER .info ("Shutting down ControllerApplication" )
@@ -235,6 +264,10 @@ def ezsp_callback_handler(self, frame_name, args):
235264 self .handle_leave (args [0 ], args [1 ])
236265 else :
237266 self .handle_join (args [0 ], args [1 ], args [4 ])
267+ elif frame_name == "incomingRouteRecordHandler" :
268+ self .handle_route_record (* args )
269+ elif frame_name == "incomingRouteErrorHandler" :
270+ self .handle_route_error (* args )
238271 elif frame_name == "_reset_controller_application" :
239272 self ._handle_reset_request (* args )
240273
@@ -432,10 +465,7 @@ async def request(
432465 aps_frame .clusterId = t .uint16_t (cluster )
433466 aps_frame .sourceEndpoint = t .uint8_t (src_ep )
434467 aps_frame .destinationEndpoint = t .uint8_t (dst_ep )
435- aps_frame .options = t .EmberApsOption (
436- t .EmberApsOption .APS_OPTION_RETRY
437- | t .EmberApsOption .APS_OPTION_ENABLE_ROUTE_DISCOVERY
438- )
468+ aps_frame .options = self ._tx_options
439469 aps_frame .groupId = t .uint16_t (0 )
440470 aps_frame .sequence = t .uint8_t (sequence )
441471 message_tag = self .get_sequence ()
@@ -444,11 +474,30 @@ async def request(
444474 LOGGER .warning (
445475 ("EUI64 addressing is not currently supported, " "reverting to NWK" )
446476 )
447- if expect_reply and device .node_desc .is_end_device in (True , None ):
448- LOGGER .debug ("Extending timeout for %s/0x%04x" , device .ieee , device .nwk )
449- await self ._ezsp .setExtendedTimeout (device .ieee , True )
450477 with self ._pending .new (message_tag ) as req :
451478 async with self ._in_flight_msg :
479+ if expect_reply and device .node_desc .is_end_device in (True , None ):
480+ LOGGER .debug (
481+ "Extending timeout for %s/0x%04x" , device .ieee , device .nwk
482+ )
483+ await self ._ezsp .setExtendedTimeout (device .ieee , True )
484+ if self .use_source_routing and device .relays is not None :
485+ res = await self ._ezsp .setSourceRoute (device .nwk , device .relays )
486+ if res [0 ] != t .EmberStatus .SUCCESS :
487+ LOGGER .warning (
488+ "Couldn't set source route for %s: %s" , device .nwk , res
489+ )
490+ else :
491+ aps_frame .options = t .EmberApsOption (
492+ aps_frame .options
493+ ^ t .EmberApsOption .APS_OPTION_ENABLE_ROUTE_DISCOVERY
494+ )
495+ LOGGER .debug (
496+ "Set source route for %s to %s: %s" ,
497+ device .nwk ,
498+ device .relays ,
499+ res ,
500+ )
452501 res = await self ._ezsp .sendUnicast (
453502 self .direct , device .nwk , aps_frame , message_tag , data
454503 )
@@ -563,6 +612,34 @@ async def _watchdog(self):
563612 "Watchdog timeout. Heartbeat timeouts: {}" .format (failures )
564613 )
565614
615+ def handle_route_record (
616+ self ,
617+ nwk : t .EmberNodeId ,
618+ ieee : t .EmberEUI64 ,
619+ lqi : t .uint8_t ,
620+ rssi : t .int8s ,
621+ relays : t .LVList (t .EmberNodeId ),
622+ ) -> None :
623+ LOGGER .debug (
624+ "Processing route record request: %s" , (nwk , ieee , lqi , rssi , relays )
625+ )
626+ try :
627+ dev = self .get_device (ieee = ieee )
628+ except KeyError :
629+ LOGGER .debug ("Why we don't have a device for %s ieee and %s NWK" , ieee , nwk )
630+ self .handle_join (nwk , ieee , 0 )
631+ return
632+ dev .relays = relays
633+
634+ def handle_route_error (self , status : t .EmberStatus , nwk : t .EmberNodeId ) -> None :
635+ LOGGER .debug ("Processing route error: status=%s, nwk=%s" , status , nwk )
636+ try :
637+ dev = self .get_device (nwk = nwk )
638+ except KeyError :
639+ LOGGER .debug ("No %s device found" , nwk )
640+ return
641+ dev .relays = None
642+
566643
567644class EZSPCoordinator (CustomDevice ):
568645 """Zigpy Device representing Coordinator."""
0 commit comments