diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index 8394a86..8211bc9 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -313,3 +313,42 @@ async def test_concurrency_auto_config(device, concurrency, make_application): await app.start_network() assert app._concurrent_requests_semaphore.max_concurrency == concurrency + + +@pytest.mark.parametrize( + ("code_revision", "expected"), + [ + (None, False), + (20190608, False), + (20221226, False), + (20230506, False), + (20230507, True), + (20250321, True), + ], +) +async def test_child_aging_by_build(code_revision, expected, make_application): + app, znp_server = make_application(FormedLaunchpadCC26X2R1) + + znp_server.reply_to( + c.SYS.Version.Req(), + responses=c.SYS.Version.Rsp( + TransportRev=2, + ProductId=1, + MajorRel=2, + MinorRel=7, + MaintRel=1, + CodeRevision=code_revision, + BootloaderBuildType=None, + BootloaderRevision=None, + ), + override=True, + ) + + await app.startup(auto_form=False) + + value = await app._znp.nvram.osal_read( + OsalNvIds.NWK_CHILD_AGE_ENABLE, item_type=t.Bool + ) + assert value == expected + + await app.shutdown() diff --git a/zigpy_znp/zigbee/application.py b/zigpy_znp/zigbee/application.py index be95a9a..30f33e4 100644 --- a/zigpy_znp/zigbee/application.py +++ b/zigpy_znp/zigbee/application.py @@ -36,6 +36,8 @@ DATA_CONFIRM_TIMEOUT = 8 EXTENDED_DATA_CONFIRM_TIMEOUT = 30 DEVICE_JOIN_MAX_DELAY = 5 +# First Koenkk Z-Stack 3.x.0 build with child aging enabled (and Xiaomi exempted) +CHILD_AGING_MIN_BUILD_ID = 20230507 LOGGER = logging.getLogger(__name__) @@ -139,6 +141,9 @@ async def start_network(self, *, read_only=False): if self.state.node_info == zigpy.state.NodeInfo(): await self.load_network_info() + # Needed by the stack settings below + self._version_rsp = await self._znp.request(c.SYS.Version.Req()) + if not read_only: await self._znp.migrate_nvram() await self._write_stack_settings() @@ -150,8 +155,6 @@ async def start_network(self, *, read_only=False): await self._znp.start_network() - self._version_rsp = await self._znp.request(c.SYS.Version.Req()) - # The CC2531 running Z-Stack Home 1.2 overrides the LED setting if it is changed # before the coordinator has started. if self.znp_config[conf.CONF_LED_MODE] is not None: @@ -624,7 +627,13 @@ async def _write_stack_settings(self) -> bool: OsalNvIds.CONCENTRATOR_DISCOVERY: t.uint8_t(120), OsalNvIds.CONCENTRATOR_RC: t.Bool(True), OsalNvIds.SRC_RTG_EXPIRY_TIME: t.uint8_t(255), - OsalNvIds.NWK_CHILD_AGE_ENABLE: t.Bool(False), + # Koenkk's builds enable child aging from 20230507 on and skip the leave + # for Xiaomi devices, which do not implement it. Older builds have no such + # exception, so aging stays off there. The value is written either way + # because an earlier zigpy-znp may have left `False` in NVRAM. + OsalNvIds.NWK_CHILD_AGE_ENABLE: t.Bool( + self._zstack_build_id >= CHILD_AGING_MIN_BUILD_ID + ), # Default is 20 in Z-Stack 3.0.1, 30 in Z-Stack 3/4 OsalNvIds.BCAST_DELIVERY_TIME: t.uint8_t(30), # We want to receive all ZDO callbacks to proxy them back to zigpy @@ -936,10 +945,10 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: except InvalidCommandResponse as e: last_error = e - # Child aging is disabled so if a child switches parents from - # the coordinator to another router, we will not be able to - # re-discover a route to it. We have to manually drop the child - # to do this. + # Child aging is off on older builds, so if a child switches + # parents from the coordinator to another router, we will not be + # able to re-discover a route to it. We have to manually drop + # the child to do this. if ( e.response.Status == t.Status.MAC_TRANSACTION_EXPIRED and device is not None