diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index 599b8a1..12b57ee 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 @@ -30,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.""" @@ -52,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.""" @@ -87,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. @@ -101,7 +102,11 @@ 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) + 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: """Clean up resources, namely the listener queues. @@ -109,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() @@ -152,11 +158,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 @@ -181,6 +187,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() @@ -199,13 +207,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: @@ -229,7 +238,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) @@ -258,7 +267,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: @@ -267,7 +276,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] @@ -278,7 +287,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" ) @@ -291,7 +300,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) @@ -324,20 +333,45 @@ 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.NoOptions, + wait_for_reset: bool = True, + ): """Reset the NCP module (see ResetOptions).""" - if self._app is not None: - tsn = self._app.get_sequence() - else: - tsn = 0 + LOGGER.debug("Sending a reset: %s", option) + + 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 - 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") + + async with self._reset_uart_reconnect: + await self._send_to_uart(req.to_frame()) + + 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/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"), ) 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) diff --git a/zigpy_zboss/uart.py b/zigpy_zboss/uart.py index 66e30db..a6daf75 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 @@ -82,48 +81,17 @@ 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) - 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" @@ -275,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), diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 4be867c..73bdf4f 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -1,12 +1,13 @@ """ControllerApplication for ZBOSS NCP protocol based adapters.""" -import asyncio +from __future__ import annotations + import logging +import asyncio 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 @@ -20,7 +21,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__) @@ -41,27 +41,36 @@ 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): """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() 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: - await self._api.reset() + try: + await self._api.reset(wait_for_reset=False) + except Exception: + LOGGER.debug( + "Failed to reset API during disconnect", exc_info=True + ) + self._api.close() self._api = None @@ -72,16 +81,17 @@ 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( 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 @@ -117,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): @@ -133,9 +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() - network_info.stack_specific.update( - self.get_default_stack_specific_formation_settings() + # 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 + }) + if node_info.ieee != t.EUI64.UNKNOWN: await self._api.request( c.NcpConfig.SetLocalIEEE.Req( @@ -179,101 +193,81 @@ 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=network_info.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"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetMaxChildren.Req( - TSN=self.get_sequence(), - ChildrenNbr=network_info.stack_specific[ - "max_children"] - ) - ) - - await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.TC_Link_Keys_Required, - PolicyValue=network_info.stack_specific[ - "tc_policy"]["unique_tclk_required"] + 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._api.request( - request=c.NcpConfig.SetTCPolicy.Req( - TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.IC_Required, - PolicyValue=network_info.stack_specific[ - "tc_policy"]["ic_required"] - ) - ) + await self._form_network(network_info, node_info) + # We have to set the PANID after formation for some reason await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( + request=c.NcpConfig.SetShortPANID.Req( TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.TC_Rejoin_Enabled, - PolicyValue=network_info.stack_specific[ - "tc_policy"]["tc_rejoin_enabled"] + PANID=network_info.pan_id ) ) + # Write stack-specific parameters. await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( + request=c.NcpConfig.SetRxOnWhenIdle.Req( TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.Ignore_TC_Rejoin, - PolicyValue=network_info.stack_specific[ - "tc_policy"]["tc_rejoin_ignored"] + RxOnWhenIdle=zboss_stack_specific["rx_on_when_idle"] ) ) await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( + request=c.NcpConfig.SetEDTimeout.Req( TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.APS_Insecure_Join, - PolicyValue=network_info.stack_specific[ - "tc_policy"]["aps_insecure_join_enabled"] + Timeout=t_zboss.TimeoutIndex( + zboss_stack_specific["end_device_timeout"] + ) ) ) await self._api.request( - request=c.NcpConfig.SetTCPolicy.Req( + request=c.NcpConfig.SetMaxChildren.Req( TSN=self.get_sequence(), - PolicyType=t_zboss.PolicyType.Disable_NWK_MGMT_Channel_Update, - PolicyValue=network_info.stack_specific[ - "tc_policy"]["mgmt_channel_update_disabled"] + ChildrenNbr=zboss_stack_specific["max_children"] ) ) - await self._form_network(network_info, node_info) - - # We have to set the PANID after formation for some reason - await self._api.request( - request=c.NcpConfig.SetShortPANID.Req( - TSN=self.get_sequence(), - PANID=network_info.pan_id - ) - ) + # 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]) + 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, @@ -300,10 +294,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() )) @@ -318,6 +317,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. @@ -349,10 +362,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, @@ -383,39 +402,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 @@ -424,7 +431,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: @@ -435,7 +442,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 @@ -465,10 +472,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 @@ -478,28 +481,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( @@ -618,18 +599,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.""" - await self.disconnect() - await self.startup() + + self.connection_lost(RuntimeError(msg)) async def send_packet(self, packet: t.ZigbeePacket) -> None: """Send packets."""