From 362380101bb88c922fd1e5be6c74fa2ffc15f3f0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 11:50:52 -0400 Subject: [PATCH 01/23] Remove unnecessary `probe` command --- zigpy_zboss/zigbee/application.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index d3dd31b..2a9c0e6 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -478,28 +478,6 @@ def zboss_config(self) -> conf.ConfigType: """Shortcut property to access the ZBOSS radio config.""" return self.config[conf.CONF_ZBOSS_CONFIG] - @classmethod - async def probe( - cls, device_config: dict[str, Any]) -> bool | dict[str, Any]: - """Probe the NCP. - - Checks whether the NCP device is responding to requests. - """ - config = cls.SCHEMA( - {conf.CONF_DEVICE: cls.SCHEMA_DEVICE(device_config)}) - zboss = ZBOSS(config) - try: - await zboss.connect() - async with async_timeout.timeout(PROBE_TIMEOUT): - await zboss.request( - c.NcpConfig.GetZigbeeRole.Req(TSN=1), timeout=1) - except asyncio.TimeoutError: - return False - else: - return device_config - finally: - zboss.close() - async def _watchdog_feed(self): """Watchdog loop to periodically test if ZBOSS is still running.""" await self._api.request( From 8866e119ad915239d910436f19dfc9d8f95bab9f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 11:54:12 -0400 Subject: [PATCH 02/23] Remove reconnection from the radio library --- zigpy_zboss/uart.py | 36 +------------------------------ zigpy_zboss/zigbee/application.py | 16 +++----------- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/zigpy_zboss/uart.py b/zigpy_zboss/uart.py index 66e30db..81d4353 100644 --- a/zigpy_zboss/uart.py +++ b/zigpy_zboss/uart.py @@ -4,7 +4,6 @@ import logging import zigpy.serial import async_timeout -import serial # type: ignore import zigpy_zboss.config as conf from zigpy_zboss import types as t from zigpy_zboss.frames import Frame @@ -84,46 +83,13 @@ def connection_lost(self, exc: typing.Optional[Exception]) -> None: """Lost connection.""" if self._api is not None: self._api.connection_lost(exc) - self.close() - - # Do not try to reconnect if no exception occured. - if exc is None: - return - - if not self._reset_flag: - SERIAL_LOGGER.warning( - f"Unexpected connection lost... {exc}") - self._reconnect_task = asyncio.create_task(self._reconnect()) - - async def _reconnect(self, timeout=RECONNECT_TIMEOUT): - """Try to reconnect the disconnected serial port.""" - SERIAL_LOGGER.info("Trying to reconnect to the NCP module!") - assert self._api is not None - loop = asyncio.get_running_loop() - async with async_timeout.timeout(timeout): - while True: - try: - _, proto = await zigpy.serial.create_serial_connection( - loop=loop, - protocol_factory=lambda: self, - url=self._port, - baudrate=self._baudrate, - xonxoff=(self._flow_control == "software"), - rtscts=(self._flow_control == "hardware"), - ) - self._api._uart = proto - break - except serial.serialutil.SerialException: - await asyncio.sleep(0.1) def close(self) -> None: """Close serial connection.""" self._buffer.clear() self._ack_seq = 0 self._pack_seq = 0 - if self._reconnect_task is not None: - self._reconnect_task.cancel() - self._reconnect_task = None + # Reset transport if self._transport: message = "Closing serial port" diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 2a9c0e6..f9f33b3 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -1,12 +1,12 @@ """ControllerApplication for ZBOSS NCP protocol based adapters.""" -import asyncio +from __future__ import annotations + import logging import zigpy.util import zigpy.state import zigpy.appdb import zigpy.config import zigpy.device -import async_timeout import zigpy.endpoint import zigpy.exceptions import zigpy.types as t @@ -596,18 +596,8 @@ def on_ncp_reset(self, msg): """NCP_RESET.indication handler.""" if msg.ResetSrc == t_zboss.ResetSource.RESET_SRC_POWER_ON: return - LOGGER.debug( - f"Resetting ControllerApplication. Source: {msg.ResetSrc}") - if self._reset_task: - LOGGER.debug("Preempting ControllerApplication reset") - self._reset_task.cancel() - - self._reset_task = asyncio.create_task(self._reset_controller()) - async def _reset_controller(self): - """Restart the application controller.""" - self.disconnect() - await self.startup() + self.connection_lost(RuntimeError(msg)) async def send_packet(self, packet: t.ZigbeePacket) -> None: """Send packets.""" From d453fcaff86fdda12c212fa76e6d3aa981f8661a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 11:54:35 -0400 Subject: [PATCH 03/23] Propagate `connection_lost` --- zigpy_zboss/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index 599b8a1..cdb17e4 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -101,7 +101,8 @@ def connection_lost(self, exc) -> None: Propagates up to the `ControllerApplication` that owns this ZBOSS instance. """ - LOGGER.debug("We were disconnected from %s: %s", self._port_path, exc) + if self._app is not None: + self._app.connection_lost(exc) def close(self) -> None: """Clean up resources, namely the listener queues. From 11c0cf4701cd4d71c3ac318af3bd13f26158442f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 11:54:40 -0400 Subject: [PATCH 04/23] Log when sending a request --- zigpy_zboss/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index cdb17e4..6ce31ee 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -182,6 +182,8 @@ async def request( raise ValueError( f"Cannot send a command that isn't a request: {request!r}") + LOGGER.debug("Sending request: %s", request) + frame = request.to_frame() # If the frame is too long, it needs fragmentation. fragments = frame.handle_tx_fragmentation() From 2324e187cd2370048b4e6af90fef2f039a4b626a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 11:55:24 -0400 Subject: [PATCH 05/23] Clean up `permit_with_key` and `connect` --- zigpy_zboss/zigbee/application.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index f9f33b3..f16a6e6 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -20,7 +20,6 @@ from zigpy_zboss import commands as c from zigpy.exceptions import DeliveryError from .device import ZbossCoordinator, ZbossDevice -from zigpy_zboss.exceptions import ZbossResponseError from zigpy_zboss.config import CONFIG_SCHEMA, SCHEMA_DEVICE LOGGER = logging.getLogger(__name__) @@ -47,11 +46,18 @@ def __init__(self, config: Dict[str, Any]): async def connect(self): """Connect to the zigbee module.""" assert self._api is None - is_responsive = await self.probe(self.config.get(conf.CONF_DEVICE, {})) - if not is_responsive: - raise ZbossResponseError + zboss = ZBOSS(self.config) - await zboss.connect() + + try: + await zboss.connect() + await zboss.request( + c.NcpConfig.GetZigbeeRole.Req(TSN=1), timeout=1 + ) + except Exception: + zboss.close() + raise + self._api = zboss self._api.set_application(self) self._bind_callbacks() @@ -465,10 +471,6 @@ async def permit_ncp(self, time_s=60): ) ) - def permit_with_key(self, node, code, time_s=60): - """Permit with key.""" - raise NotImplementedError - def permit_with_link_key(self, node, link_key, time_s=60): """Permit with link key.""" raise NotImplementedError From d49eef005ac03ad49eb5e9e483674cb182f61612 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 11:57:00 -0400 Subject: [PATCH 06/23] Log when reset on disconnect fails --- zigpy_zboss/zigbee/application.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index f16a6e6..40215ea 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -67,7 +67,13 @@ async def disconnect(self): if self._reset_task and not self._reset_task.done(): self._reset_task.cancel() if self._api is not None: - await self._api.reset() + try: + await self._api.reset() + except Exception: + LOGGER.debug( + "Failed to reset API during disconnect", exc_info=True + ) + self._api.close() self._api = None From 9813bda8d51c3ac8725bc68e747fa765e6a72da6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 12:01:28 -0400 Subject: [PATCH 07/23] Keep stack-specific settings under the `zboss` key --- zigpy_zboss/zigbee/application.py | 54 ++++++++++++++----------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 40215ea..7147c04 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -145,7 +145,10 @@ async def write_network_info(self, *, network_info, node_info): if not network_info.stack_specific.get("form_quickly", False): await self.reset_network_info() - network_info.stack_specific.update( + zboss_stack_specific = network_info.stack_specific.setdefault( + "zboss", {} + ) + zboss_stack_specific.update( self.get_default_stack_specific_formation_settings() ) if node_info.ieee != t.EUI64.UNKNOWN: @@ -195,21 +198,21 @@ async def write_network_info(self, *, network_info, node_info): await self._api.request( request=c.NcpConfig.SetRxOnWhenIdle.Req( TSN=self.get_sequence(), - RxOnWhenIdle=network_info.stack_specific["rx_on_when_idle"] + RxOnWhenIdle=zboss_stack_specific["rx_on_when_idle"] ) ) await self._api.request( request=c.NcpConfig.SetEDTimeout.Req( TSN=self.get_sequence(), - Timeout=network_info.stack_specific["end_device_timeout"] + Timeout=zboss_stack_specific["end_device_timeout"] ) ) await self._api.request( request=c.NcpConfig.SetMaxChildren.Req( TSN=self.get_sequence(), - ChildrenNbr=network_info.stack_specific[ + ChildrenNbr=zboss_stack_specific[ "max_children"] ) ) @@ -218,7 +221,7 @@ async def write_network_info(self, *, network_info, node_info): request=c.NcpConfig.SetTCPolicy.Req( TSN=self.get_sequence(), PolicyType=t_zboss.PolicyType.TC_Link_Keys_Required, - PolicyValue=network_info.stack_specific[ + PolicyValue=zboss_stack_specific[ "tc_policy"]["unique_tclk_required"] ) ) @@ -227,7 +230,7 @@ async def write_network_info(self, *, network_info, node_info): request=c.NcpConfig.SetTCPolicy.Req( TSN=self.get_sequence(), PolicyType=t_zboss.PolicyType.IC_Required, - PolicyValue=network_info.stack_specific[ + PolicyValue=zboss_stack_specific[ "tc_policy"]["ic_required"] ) ) @@ -236,7 +239,7 @@ async def write_network_info(self, *, network_info, node_info): request=c.NcpConfig.SetTCPolicy.Req( TSN=self.get_sequence(), PolicyType=t_zboss.PolicyType.TC_Rejoin_Enabled, - PolicyValue=network_info.stack_specific[ + PolicyValue=zboss_stack_specific[ "tc_policy"]["tc_rejoin_enabled"] ) ) @@ -245,7 +248,7 @@ async def write_network_info(self, *, network_info, node_info): request=c.NcpConfig.SetTCPolicy.Req( TSN=self.get_sequence(), PolicyType=t_zboss.PolicyType.Ignore_TC_Rejoin, - PolicyValue=network_info.stack_specific[ + PolicyValue=zboss_stack_specific[ "tc_policy"]["tc_rejoin_ignored"] ) ) @@ -254,7 +257,7 @@ async def write_network_info(self, *, network_info, node_info): request=c.NcpConfig.SetTCPolicy.Req( TSN=self.get_sequence(), PolicyType=t_zboss.PolicyType.APS_Insecure_Join, - PolicyValue=network_info.stack_specific[ + PolicyValue=zboss_stack_specific[ "tc_policy"]["aps_insecure_join_enabled"] ) ) @@ -263,7 +266,7 @@ async def write_network_info(self, *, network_info, node_info): request=c.NcpConfig.SetTCPolicy.Req( TSN=self.get_sequence(), PolicyType=t_zboss.PolicyType.Disable_NWK_MGMT_Channel_Update, - PolicyValue=network_info.stack_specific[ + PolicyValue=zboss_stack_specific[ "tc_policy"]["mgmt_channel_update_disabled"] ) ) @@ -312,10 +315,15 @@ async def load_network_info(self, *, load_devices=False): """Populate state.node_info and state.network_info.""" res = await self._api.request( c.NcpConfig.GetJoinStatus.Req(TSN=self.get_sequence())) - self.state.network_info.stack_specific["joined"] = res.Joined + if not res.Joined & 0x01: raise zigpy.exceptions.NetworkNotFormed + zboss_stack_specific = ( + self.state.network_info.stack_specific.setdefault("zboss", {}) + ) + zboss_stack_specific["joined"] = res.Joined + res = await self._api.request(c.NcpConfig.GetShortAddr.Req( TSN=self.get_sequence() )) @@ -395,39 +403,27 @@ async def load_network_info(self, *, load_devices=False): res = await self._api.request( c.NcpConfig.GetRxOnWhenIdle.Req(TSN=self.get_sequence())) - self.state.network_info.stack_specific[ - "rx_on_when_idle" - ] = res.RxOnWhenIdle + zboss_stack_specific["rx_on_when_idle"] = res.RxOnWhenIdle res = await self._api.request( c.NcpConfig.GetEDTimeout.Req(TSN=self.get_sequence())) - self.state.network_info.stack_specific[ - "end_device_timeout" - ] = res.Timeout + zboss_stack_specific["end_device_timeout"] = res.Timeout res = await self._api.request( c.NcpConfig.GetMaxChildren.Req(TSN=self.get_sequence())) - self.state.network_info.stack_specific[ - "max_children" - ] = res.ChildrenNbr + zboss_stack_specific["max_children"] = res.ChildrenNbr res = await self._api.request( c.NcpConfig.GetAuthenticationStatus.Req(TSN=self.get_sequence())) - self.state.network_info.stack_specific[ - "authenticated" - ] = res.Authenticated + zboss_stack_specific["authenticated"] = res.Authenticated res = await self._api.request( c.NcpConfig.GetParentAddr.Req(TSN=self.get_sequence())) - self.state.network_info.stack_specific[ - "parent_nwk" - ] = res.NWKParentAddr + zboss_stack_specific["parent_nwk"] = res.NWKParentAddr res = await self._api.request( c.NcpConfig.GetCoordinatorVersion.Req(TSN=self.get_sequence())) - self.state.network_info.stack_specific[ - "coordinator_version" - ] = res.CoordinatorVersion + zboss_stack_specific["coordinator_version"] = res.CoordinatorVersion if not load_devices: return From f55aa976636c0009810464ad8e9adeee7b3928b7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 12:03:16 -0400 Subject: [PATCH 08/23] Remove remnants of `_reset_task` --- zigpy_zboss/zigbee/application.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 7147c04..7f91ba0 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -40,7 +40,6 @@ def __init__(self, config: Dict[str, Any]): """Initialize instance.""" super().__init__(config=zigpy.config.ZIGPY_SCHEMA(config)) self._api: ZBOSS | None = None - self._reset_task = None self.version = None async def connect(self): @@ -64,8 +63,6 @@ async def connect(self): async def disconnect(self): """Disconnect from the zigbee module.""" - if self._reset_task and not self._reset_task.done(): - self._reset_task.cancel() if self._api is not None: try: await self._api.reset() From bbb370feb49b3f4cbaf59f6f3909acfd67f70718 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 12:07:49 -0400 Subject: [PATCH 09/23] Expose coordinator device info --- zigpy_zboss/zigbee/application.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 7f91ba0..44f2dc8 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -40,7 +40,6 @@ def __init__(self, config: Dict[str, Any]): """Initialize instance.""" super().__init__(config=zigpy.config.ZIGPY_SCHEMA(config)) self._api: ZBOSS | None = None - self.version = None async def connect(self): """Connect to the zigbee module.""" @@ -81,8 +80,6 @@ async def start_network(self): await self.start_without_formation() - self.version = await self._api.version() - await self.register_endpoints() self.devices[self.state.node_info.ieee] = ZbossCoordinator( @@ -326,6 +323,13 @@ async def load_network_info(self, *, load_devices=False): )) self.state.node_info.nwk = res.NWKAddr + fw_ver, stack_ver, proto_ver = await self._api.version() + + # TODO: is there a way to read the coordinator model and manufacturer? + self.state.node_info.model = "ZBOSS" + self.state.node_info.manufacturer = "DSR" + self.state.node_info.version = f"{fw_ver} (stack {stack_ver})" + res = await self._api.request( c.NcpConfig.GetLocalIEEE.Req( TSN=self.get_sequence(), MacInterfaceNum=0)) From 9425a4981d10da19ee3818ae342aa8febe1e054a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 12:22:16 -0400 Subject: [PATCH 10/23] Allow resetting without waiting for a reply --- zigpy_zboss/api.py | 16 +++++++++++++--- zigpy_zboss/zigbee/application.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index 6ce31ee..4383fa1 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -202,13 +202,14 @@ async def _send_frags(self, fragments, response_future, timeout): await self._send_to_uart(frag, None) async def _send_to_uart( - self, frame, response_future, timeout=DEFAULT_TIMEOUT): + self, frame, response_future=None, timeout=DEFAULT_TIMEOUT): """Send the frame and waits for the response.""" if self._uart is None: return + try: await self._uart.send(frame) - if response_future: + if response_future is not None: async with async_timeout.timeout(timeout): return await response_future except asyncio.TimeoutError: @@ -327,7 +328,11 @@ async def version(self): version[idx] = ".".join([major, minor, revision, commit]) return tuple(version) - async def reset(self, option=t.ResetOptions(0)): + async def reset( + self, + option: t.ResetOptions = t.ResetOptions(0), + wait_for_reset: bool = True + ): """Reset the NCP module (see ResetOptions).""" if self._app is not None: tsn = self._app.get_sequence() @@ -335,6 +340,11 @@ async def reset(self, option=t.ResetOptions(0)): tsn = 0 req = c.NcpConfig.NCPModuleReset.Req(TSN=tsn, Option=option) self._uart.reset_flag = True + + if not wait_for_reset: + await self._send_to_uart(req.to_frame()) + return + res = await self._send_to_uart( req.to_frame(), self.wait_for_response( diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 44f2dc8..0082a95 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -64,7 +64,7 @@ async def disconnect(self): """Disconnect from the zigbee module.""" if self._api is not None: try: - await self._api.reset() + await self._api.reset(wait_for_reset=False) except Exception: LOGGER.debug( "Failed to reset API during disconnect", exc_info=True From 2e8816b7291596a05ce873b049cd4287e7543525 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 12:56:01 -0400 Subject: [PATCH 11/23] Move all listener logging to a child logger --- zigpy_zboss/api.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index 4383fa1..7aaf0bc 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -20,6 +20,8 @@ from zigpy_zboss.utils import OneShotResponseListener LOGGER = logging.getLogger(__name__) +LISTENER_LOGGER = LOGGER.getChild("listener") +LISTENER_LOGGER.propagate = False # All of these are in seconds AFTER_BOOTLOADER_SKIP_BYTE_DELAY = 2.5 @@ -153,11 +155,11 @@ def frame_received(self, frame: Frame) -> bool: continue if not listener.resolve(command): - LOGGER.debug(f"{command} does not match {listener}") + LISTENER_LOGGER.debug(f"{command} does not match {listener}") continue matched = True - LOGGER.debug(f"{command} matches {listener}") + LISTENER_LOGGER.debug(f"{command} matches {listener}") if isinstance(listener, OneShotResponseListener): one_shot_matched = True @@ -233,7 +235,7 @@ def wait_for_responses( """ listener = OneShotResponseListener(responses) - LOGGER.debug("Creating one-shot listener %s", listener) + LISTENER_LOGGER.debug("Creating one-shot listener %s", listener) for header in listener.matching_headers(): self._listeners[header].append(listener) @@ -262,7 +264,7 @@ def remove_listener(self, listener: BaseResponseListener) -> None: if not self._listeners: return - LOGGER.debug("Removing listener %s", listener) + LISTENER_LOGGER.debug("Removing listener %s", listener) for header in listener.matching_headers(): try: @@ -271,7 +273,7 @@ def remove_listener(self, listener: BaseResponseListener) -> None: pass if not self._listeners[header]: - LOGGER.debug( + LISTENER_LOGGER.debug( "Cleaning up empty listener list for header %s", header ) del self._listeners[header] @@ -282,7 +284,7 @@ def remove_listener(self, listener: BaseResponseListener) -> None: self._listeners.values()): counts[type(listener)] += 1 - LOGGER.debug( + LISTENER_LOGGER.debug( f"There are {counts[IndicationListener]} callbacks and" f" {counts[OneShotResponseListener]} one-shot listeners remaining" ) @@ -330,10 +332,12 @@ async def version(self): async def reset( self, - option: t.ResetOptions = t.ResetOptions(0), + option: t.ResetOptions = t.ResetOptions.NoOptions, wait_for_reset: bool = True ): """Reset the NCP module (see ResetOptions).""" + LOGGER.debug("Sending a reset: %s", option) + if self._app is not None: tsn = self._app.get_sequence() else: From 7981b6bbf1f8e9a76685752034941ba59baa4abd Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 12:56:13 -0400 Subject: [PATCH 12/23] Migrate `ChannelEntry` to a struct --- zigpy_zboss/types/named.py | 37 +++---------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/zigpy_zboss/types/named.py b/zigpy_zboss/types/named.py index 67da9b3..f6adf6a 100644 --- a/zigpy_zboss/types/named.py +++ b/zigpy_zboss/types/named.py @@ -36,42 +36,11 @@ class BindAddrMode(basic.enum8): IEEE = 0x03 -class ChannelEntry: +class ChannelEntry(Struct): """Class representing a channel entry.""" - def __new__(cls, page=None, channel_mask=None): - """Create a channel entry instance.""" - instance = super().__new__(cls) - - instance.page = basic.uint8_t(page) - instance.channel_mask = channel_mask - - return instance - - @classmethod - def deserialize(cls, data: bytes) -> "ChannelEntry": - """Deserialize the object.""" - page, data = basic.uint8_t.deserialize(data) - channel_mask, data = Channels.deserialize(data) - - return cls(page=page, channel_mask=channel_mask), data - - def serialize(self) -> bytes: - """Serialize the object.""" - return self.page.serialize() + self.channel_mask.serialize() - - def __eq__(self, other): - """Return True if channel_masks and pages are equal.""" - if not isinstance(other, type(self)): - return NotImplemented - - return self.page == other.page and \ - self.channel_mask == other.channel_mask - - def __repr__(self) -> str: - """Return a representation of a channel entry.""" - return f"{type(self).__name__}(page={self.page!r}," \ - f" channels={self.channel_mask!r})" + page: basic.uint8_t + channel_mask: Channels @dataclasses.dataclass(frozen=True) From 03692019df0bda3721c1b3688f659485daa7776e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 12:57:25 -0400 Subject: [PATCH 13/23] Log callbacks under the listener logger --- zigpy_zboss/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index 7aaf0bc..f64d517 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -297,7 +297,7 @@ def register_indication_listeners( """ listener = IndicationListener(responses, callback=callback) - LOGGER.debug(f"Creating callback {listener}") + LISTENER_LOGGER.debug(f"Creating callback {listener}") for header in listener.matching_headers(): self._listeners[header].append(listener) From 566c4a991d5220c4cc7109890024f08e3a170c00 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:13:09 -0400 Subject: [PATCH 14/23] Use generic status code with responses --- zigpy_zboss/types/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_zboss/types/commands.py b/zigpy_zboss/types/commands.py index 613f749..7406afc 100644 --- a/zigpy_zboss/types/commands.py +++ b/zigpy_zboss/types/commands.py @@ -717,5 +717,5 @@ class Relationship(t.enum8): STATUS_SCHEMA = ( t.Param("TSN", t.uint8_t, "Transmit Sequence Number"), t.Param("StatusCat", StatusCategory, "Status category code"), - t.Param("StatusCode", t.uint8_t, "Status code inside category"), + t.Param("StatusCode", StatusCodeGeneric, "Status code inside category"), ) From ff8e1c8f9deb3b13f382c93f640229e59970b082 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:13:48 -0400 Subject: [PATCH 15/23] Set TC policy settings using a dictionary for clarity --- zigpy_zboss/zigbee/application.py | 80 +++++++++++-------------------- 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 0082a95..54e8753 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -211,59 +211,35 @@ async def write_network_info(self, *, network_info, node_info): ) ) - await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.TC_Link_Keys_Required, - PolicyValue=zboss_stack_specific[ - "tc_policy"]["unique_tclk_required"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.IC_Required, - PolicyValue=zboss_stack_specific[ - "tc_policy"]["ic_required"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.TC_Rejoin_Enabled, - PolicyValue=zboss_stack_specific[ - "tc_policy"]["tc_rejoin_enabled"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.Ignore_TC_Rejoin, - PolicyValue=zboss_stack_specific[ - "tc_policy"]["tc_rejoin_ignored"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.APS_Insecure_Join, - PolicyValue=zboss_stack_specific[ - "tc_policy"]["aps_insecure_join_enabled"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.Disable_NWK_MGMT_Channel_Update, - PolicyValue=zboss_stack_specific[ - "tc_policy"]["mgmt_channel_update_disabled"] + for policy_type, policy_value in { + t_zboss.PolicyType.TC_Link_Keys_Required: ( + zboss_stack_specific["tc_policy"]["unique_tclk_required"] + ), + t_zboss.PolicyType.IC_Required: ( + zboss_stack_specific["tc_policy"]["ic_required"] + ), + t_zboss.PolicyType.TC_Rejoin_Enabled: ( + zboss_stack_specific["tc_policy"]["tc_rejoin_enabled"] + ), + t_zboss.PolicyType.Ignore_TC_Rejoin: ( + zboss_stack_specific["tc_policy"]["tc_rejoin_ignored"] + ), + t_zboss.PolicyType.APS_Insecure_Join: ( + zboss_stack_specific["tc_policy"]["aps_insecure_join_enabled"] + ), + t_zboss.PolicyType.Disable_NWK_MGMT_Channel_Update: ( + zboss_stack_specific["tc_policy"][ + "mgmt_channel_update_disabled" + ] + ), + }.items(): + await self._api.request( + request=c.NcpConfig.SetTCPolicy.Req( + TSN=self.get_sequence(), + PolicyType=policy_type, + PolicyValue=policy_value, + ) ) - ) await self._form_network(network_info, node_info) From b340b39cda8e62770c96e7c690a46e55559ae06f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:14:34 -0400 Subject: [PATCH 16/23] Handle empty NVRAM entries for keys and the address map --- zigpy_zboss/zigbee/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 54e8753..3e0a1cf 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -409,7 +409,7 @@ async def load_network_info(self, *, load_devices=False): t_zboss.DatasetId.ZB_NVRAM_ADDR_MAP, t_zboss.DSNwkAddrMap ) - for rec in map: + for rec in (map or []): if rec.nwk_addr == 0x0000: continue if rec.ieee_addr not in self.state.network_info.children: @@ -420,7 +420,7 @@ async def load_network_info(self, *, load_devices=False): t_zboss.DatasetId.ZB_NVRAM_APS_SECURE_DATA, t_zboss.DSApsSecureKeys ) - for key_entry in keys: + for key_entry in (keys or []): zigpy_key = zigpy.state.Key( key=t.KeyData(key_entry.key), partner_ieee=key_entry.ieee_addr From 6a82be47d238aae61f8d5caf7b532e5f9cc34aaa Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:15:19 -0400 Subject: [PATCH 17/23] Loading the device information requires a formed network, unfortunately --- zigpy_zboss/zigbee/application.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 3e0a1cf..b053564 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -85,9 +85,12 @@ async def start_network(self): self.devices[self.state.node_info.ieee] = ZbossCoordinator( self, self.state.node_info.ieee, self.state.node_info.nwk ) - await self._device.schedule_initialize() + # We can only read the coordinator info after the network is formed + self.state.node_info.model = self._device.model + self.state.node_info.manufacturer = self._device.manufacturer + async def force_remove(self, dev: zigpy.device.Device) -> None: """Send a lower-level leave command to the device.""" # ZBOSS NCP does not have any way to do this @@ -299,13 +302,6 @@ async def load_network_info(self, *, load_devices=False): )) self.state.node_info.nwk = res.NWKAddr - fw_ver, stack_ver, proto_ver = await self._api.version() - - # TODO: is there a way to read the coordinator model and manufacturer? - self.state.node_info.model = "ZBOSS" - self.state.node_info.manufacturer = "DSR" - self.state.node_info.version = f"{fw_ver} (stack {stack_ver})" - res = await self._api.request( c.NcpConfig.GetLocalIEEE.Req( TSN=self.get_sequence(), MacInterfaceNum=0)) @@ -315,6 +311,20 @@ async def load_network_info(self, *, load_devices=False): c.NcpConfig.GetZigbeeRole.Req(TSN=self.get_sequence())) self.state.node_info.logical_type = zdo_t.LogicalType(res.DeviceRole) + # TODO: it looks like we can't load the device info unless a network is + # running, as it is only accessible via ZCL + try: + self._device + except KeyError: + self.state.node_info.model = "ZBOSS" + self.state.node_info.manufacturer = "DSR" + else: + self.state.node_info.model = self._device.model + self.state.node_info.manufacturer = self._device.manufacturer + + fw_ver, stack_ver, proto_ver = await self._api.version() + self.state.node_info.version = f"{fw_ver} (stack {stack_ver})" + res = await self._api.request( c.NcpConfig.GetExtendedPANID.Req(TSN=self.get_sequence())) # FIX! Swaping bytes because of module sending IEEE the wrong way. From d61a42b2d26ed3695622bc0c8b0f8bb231a222c3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:16:47 -0400 Subject: [PATCH 18/23] Clear NVRAM instead of sending a factory reset to keep the serial port open --- zigpy_zboss/zigbee/application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index b053564..ce6a796 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -439,8 +439,9 @@ async def load_network_info(self, *, load_devices=False): async def reset_network_info(self) -> None: """Reset node network information and leaves the current network.""" - assert self._api is not None - await self._api.reset(option=t_zboss.ResetOptions.FactoryReset) + await self._api.request( + c.NcpConfig.EraseNVRAM.Req(TSN=self.get_sequence()) + ) async def start_without_formation(self): """Start the network with settings currently stored on the module.""" From 71b18855154973fb5eeba568e70155072ecc5629 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:17:02 -0400 Subject: [PATCH 19/23] Build the channel mask from the expected channel --- zigpy_zboss/zigbee/application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index ce6a796..daaa480 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -256,12 +256,13 @@ async def write_network_info(self, *, network_info, node_info): async def _form_network(self, network_info, node_info): """Clear the current config and forms a new network.""" + channel_mask = t.Channels.from_channel_list([network_info.channel]) + await self._api.request( request=c.NWK.Formation.Req( TSN=self.get_sequence(), ChannelList=t_zboss.ChannelEntryList([ - t_zboss.ChannelEntry( - page=0, channel_mask=network_info.channel_mask) + t_zboss.ChannelEntry(page=0, channel_mask=channel_mask) ]), ScanDuration=0x05, DistributedNetFlag=0x00, From 91e4fbf2e62f393c0514929bd0c05badf7b03275 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:18:35 -0400 Subject: [PATCH 20/23] Revert "Clear NVRAM instead of sending a factory reset to keep the serial port open" This reverts commit d61a42b2d26ed3695622bc0c8b0f8bb231a222c3. --- zigpy_zboss/zigbee/application.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index daaa480..2b8efc9 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -440,9 +440,8 @@ async def load_network_info(self, *, load_devices=False): async def reset_network_info(self) -> None: """Reset node network information and leaves the current network.""" - await self._api.request( - c.NcpConfig.EraseNVRAM.Req(TSN=self.get_sequence()) - ) + assert self._api is not None + await self._api.reset(option=t_zboss.ResetOptions.FactoryReset) async def start_without_formation(self): """Start the network with settings currently stored on the module.""" From 907c3e53fe660f9e726bdfc7978334862e249c31 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:39:06 -0400 Subject: [PATCH 21/23] Hide serial reconnects during reset from the application --- zigpy_zboss/api.py | 69 ++++++++++++++++++++++++++++----------------- zigpy_zboss/uart.py | 4 +-- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index f64d517..12b57ee 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -32,6 +32,10 @@ DEFAULT_TIMEOUT = 5 +EXPECTED_DISCONNECT_TIMEOUT = 5.0 +MAX_RESET_RECONNECT_ATTEMPTS = 5 +RESET_RECONNECT_DELAY = 1.0 + class ZBOSS: """Class linking zigpy with ZBOSS running on nRF SoC.""" @@ -54,6 +58,8 @@ def __init__(self, config: conf.ConfigType): self._rx_fragments = [] self._ncp_debug = None + self._reset_uart_reconnect = asyncio.Lock() + self._disconnected_event = asyncio.Event() def set_application(self, app): """Set the application using the ZBOSS class.""" @@ -89,13 +95,6 @@ async def connect(self) -> None: LOGGER.debug( "Connected to %s at %s baud", self._uart.name, self._uart.baudrate) - def connection_made(self) -> None: - """Notify that connection has been made. - - Called by the UART object when a connection has been made. - """ - pass - def connection_lost(self, exc) -> None: """Port has been closed. @@ -103,7 +102,10 @@ def connection_lost(self, exc) -> None: Propagates up to the `ControllerApplication` that owns this ZBOSS instance. """ - if self._app is not None: + self._uart = None + self._disconnected_event.set() + + if self._app is not None and not self._reset_uart_reconnect.locked(): self._app.connection_lost(exc) def close(self) -> None: @@ -112,8 +114,9 @@ def close(self) -> None: Calling this will reset ZBOSS to the same internal state as a fresh ZBOSS instance. """ - self._app = None - self.version = None + if not self._reset_uart_reconnect.locked(): + self._app = None + self.version = None if self._uart is not None: self._uart.close() @@ -333,28 +336,42 @@ async def version(self): async def reset( self, option: t.ResetOptions = t.ResetOptions.NoOptions, - wait_for_reset: bool = True + wait_for_reset: bool = True, ): """Reset the NCP module (see ResetOptions).""" LOGGER.debug("Sending a reset: %s", option) - if self._app is not None: - tsn = self._app.get_sequence() - else: - tsn = 0 + tsn = self._app.get_sequence() if self._app is not None else 0 req = c.NcpConfig.NCPModuleReset.Req(TSN=tsn, Option=option) self._uart.reset_flag = True - if not wait_for_reset: + async with self._reset_uart_reconnect: await self._send_to_uart(req.to_frame()) - return - res = await self._send_to_uart( - req.to_frame(), - self.wait_for_response( - c.NcpConfig.NCPModuleReset.Rsp(partial=True) - ), - timeout=10 - ) - if not res.TSN == 0xFF: - raise ValueError("Should get TSN 0xFF") + if not wait_for_reset: + return + + LOGGER.debug("Waiting for radio to disconnect") + + try: + async with async_timeout.timeout(EXPECTED_DISCONNECT_TIMEOUT): + await self._disconnected_event.wait() + except asyncio.TimeoutError: + LOGGER.debug( + "Radio did not disconnect, must be using external UART" + ) + return + + LOGGER.debug("Radio has disconnected, reconnecting") + + for attempt in range(MAX_RESET_RECONNECT_ATTEMPTS): + await asyncio.sleep(RESET_RECONNECT_DELAY) + + try: + await self.connect() + break + except Exception as exc: + if attempt == MAX_RESET_RECONNECT_ATTEMPTS - 1: + raise + + LOGGER.debug("Failed to reconnect, retrying: %r", exc) diff --git a/zigpy_zboss/uart.py b/zigpy_zboss/uart.py index 81d4353..a6daf75 100644 --- a/zigpy_zboss/uart.py +++ b/zigpy_zboss/uart.py @@ -81,6 +81,8 @@ def connection_made( def connection_lost(self, exc: typing.Optional[Exception]) -> None: """Lost connection.""" + LOGGER.debug("Connection has been lost: %r", exc) + if self._api is not None: self._api.connection_lost(exc) @@ -241,8 +243,6 @@ async def connect(config: conf.ConfigType, api) -> ZbossNcpProtocol: baudrate = config[conf.CONF_DEVICE_BAUDRATE] flow_control = config[conf.CONF_DEVICE_FLOW_CONTROL] - LOGGER.debug("Connecting to %s at %s baud", port, baudrate) - _, protocol = await zigpy.serial.create_serial_connection( loop=loop, protocol_factory=lambda: ZbossNcpProtocol(config, api), From 3ca3e058892109e4249855358240a9d43e19052f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:46:29 -0400 Subject: [PATCH 22/23] Properly handle missing counters dataset --- zigpy_zboss/zigbee/application.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 2b8efc9..587fb96 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -357,10 +357,16 @@ async def load_network_info(self, *, load_devices=False): t_zboss.DatasetId.ZB_IB_COUNTERS, t_zboss.DSIbCounters ) - if common and counters: + + # Counters NVRAM dataset can be missing if we don't use the network + tx_counter = 0 + if counters is not None: + tx_counter = counters.nib_counter + + if common is not None: self.state.network_info.network_key = zigpy.state.Key( key=common.nwk_key, - tx_counter=counters.nib_counter, + tx_counter=tx_counter, rx_counter=0, seq=common.nwk_key_seq, partner_ieee=self.state.node_info.ieee, From eb4170017c6cb5cfaace04a56dc66d6708ddcf4a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 May 2024 13:54:38 -0400 Subject: [PATCH 23/23] Make backup/restore fully round-trip --- zigpy_zboss/zigbee/application.py | 91 ++++++++++++++++--------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 587fb96..73bdf4f 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import asyncio import zigpy.util import zigpy.state import zigpy.appdb @@ -126,15 +127,13 @@ def get_default_stack_specific_formation_settings(self): "authenticated": t.Bool.false, "parent_nwk": None, "coordinator_version": None, - "tc_policy": { - "unique_tclk_required": t.Bool.false, - "ic_required": t.Bool.false, - "tc_rejoin_enabled": t.Bool.true, - "unsecured_tc_rejoin_enabled": t.Bool.false, - "tc_rejoin_ignored": t.Bool.false, - "aps_insecure_join_enabled": t.Bool.false, - "mgmt_channel_update_disabled": t.Bool.false, - }, + "tc_policy_unique_tclk_required": t.Bool.false, + "tc_policy_ic_required": t.Bool.false, + "tc_policy_tc_rejoin_enabled": t.Bool.true, + "tc_policy_unsecured_tc_rejoin_enabled": t.Bool.false, + "tc_policy_tc_rejoin_ignored": t.Bool.false, + "tc_policy_aps_insecure_join_enabled": t.Bool.false, + "tc_policy_mgmt_channel_update_disabled": t.Bool.false, } async def write_network_info(self, *, network_info, node_info): @@ -142,12 +141,15 @@ async def write_network_info(self, *, network_info, node_info): if not network_info.stack_specific.get("form_quickly", False): await self.reset_network_info() + # Prefer the existing stack-specific settings to the defaults zboss_stack_specific = network_info.stack_specific.setdefault( "zboss", {} ) - zboss_stack_specific.update( - self.get_default_stack_specific_formation_settings() - ) + zboss_stack_specific.update({ + **self.get_default_stack_specific_formation_settings(), + **zboss_stack_specific + }) + if node_info.ieee != t.EUI64.UNKNOWN: await self._api.request( c.NcpConfig.SetLocalIEEE.Req( @@ -191,49 +193,24 @@ async def write_network_info(self, *, network_info, node_info): ) ) - # Write stack-specific parameters. - await self._api.request( - request=c.NcpConfig.SetRxOnWhenIdle.Req( - TSN=self.get_sequence(), - RxOnWhenIdle=zboss_stack_specific["rx_on_when_idle"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetEDTimeout.Req( - TSN=self.get_sequence(), - Timeout=zboss_stack_specific["end_device_timeout"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetMaxChildren.Req( - TSN=self.get_sequence(), - ChildrenNbr=zboss_stack_specific[ - "max_children"] - ) - ) - for policy_type, policy_value in { t_zboss.PolicyType.TC_Link_Keys_Required: ( - zboss_stack_specific["tc_policy"]["unique_tclk_required"] + zboss_stack_specific["tc_policy_unique_tclk_required"] ), t_zboss.PolicyType.IC_Required: ( - zboss_stack_specific["tc_policy"]["ic_required"] + zboss_stack_specific["tc_policy_ic_required"] ), t_zboss.PolicyType.TC_Rejoin_Enabled: ( - zboss_stack_specific["tc_policy"]["tc_rejoin_enabled"] + zboss_stack_specific["tc_policy_tc_rejoin_enabled"] ), t_zboss.PolicyType.Ignore_TC_Rejoin: ( - zboss_stack_specific["tc_policy"]["tc_rejoin_ignored"] + zboss_stack_specific["tc_policy_tc_rejoin_ignored"] ), t_zboss.PolicyType.APS_Insecure_Join: ( - zboss_stack_specific["tc_policy"]["aps_insecure_join_enabled"] + zboss_stack_specific["tc_policy_aps_insecure_join_enabled"] ), t_zboss.PolicyType.Disable_NWK_MGMT_Channel_Update: ( - zboss_stack_specific["tc_policy"][ - "mgmt_channel_update_disabled" - ] + zboss_stack_specific["tc_policy_mgmt_channel_update_disabled"] ), }.items(): await self._api.request( @@ -254,6 +231,34 @@ async def write_network_info(self, *, network_info, node_info): ) ) + # Write stack-specific parameters. + await self._api.request( + request=c.NcpConfig.SetRxOnWhenIdle.Req( + TSN=self.get_sequence(), + RxOnWhenIdle=zboss_stack_specific["rx_on_when_idle"] + ) + ) + + await self._api.request( + request=c.NcpConfig.SetEDTimeout.Req( + TSN=self.get_sequence(), + Timeout=t_zboss.TimeoutIndex( + zboss_stack_specific["end_device_timeout"] + ) + ) + ) + + await self._api.request( + request=c.NcpConfig.SetMaxChildren.Req( + TSN=self.get_sequence(), + ChildrenNbr=zboss_stack_specific["max_children"] + ) + ) + + # XXX: We must wait a moment after setting the PAN ID, otherwise the + # setting does not persist + await asyncio.sleep(1) + async def _form_network(self, network_info, node_info): """Clear the current config and forms a new network.""" channel_mask = t.Channels.from_channel_list([network_info.channel])