diff --git a/zigpy_znp/api.py b/zigpy_znp/api.py index 0dfd260b..0585e8e0 100644 --- a/zigpy_znp/api.py +++ b/zigpy_znp/api.py @@ -31,7 +31,11 @@ CallbackResponseListener, ) from zigpy_znp.frames import GeneralFrame -from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse +from zigpy_znp.exceptions import ( + ControllerResetting, + CommandNotRecognized, + InvalidCommandResponse, +) from zigpy_znp.types.nvids import ExNvIds, OsalNvIds if typing.TYPE_CHECKING: @@ -991,15 +995,27 @@ async def request( if self._uart is None: raise RuntimeError("Coordinator is disconnected, cannot send request") - # Immediately send reset requests - ctx = ( - contextlib.AsyncExitStack() - if isinstance(request, c.SYS.ResetReq.Req) - else self._sync_request_lock - ) + if not isinstance(request, c.SYS.ResetReq.Req): + # We should only be sending one SREQ at a time, according to the spec + send_ctx = self._sync_request_lock + else: + # Immediately send reset requests + send_ctx = contextlib.AsyncExitStack() + + # Fail all one-shot listeners on a reset + for header, listeners in self._listeners.items(): + # Allow any listeners for a reset indication + if header == c.SYS.ResetInd.Callback.header: + continue + + for listener in listeners: + if not isinstance(listener, OneShotResponseListener): + continue + + LOGGER.log(log.TRACE, "Failing listener %s on reset", listener) + listener.failure(ControllerResetting()) - # We should only be sending one SREQ at a time, according to the spec - async with ctx: + async with send_ctx: LOGGER.debug("Sending request: %s", request) # If our request has no response, we cannot wait for one diff --git a/zigpy_znp/exceptions.py b/zigpy_znp/exceptions.py index 2c61502f..631318be 100644 --- a/zigpy_znp/exceptions.py +++ b/zigpy_znp/exceptions.py @@ -13,6 +13,10 @@ class CommandNotRecognized(Exception): pass +class ControllerResetting(Exception): + pass + + class InvalidCommandResponse(DeliveryError): def __init__(self, message, response): super().__init__(message) diff --git a/zigpy_znp/utils.py b/zigpy_znp/utils.py index 04d05ef1..c169151e 100644 --- a/zigpy_znp/utils.py +++ b/zigpy_znp/utils.py @@ -76,6 +76,14 @@ def resolve(self, response: t.CommandBase) -> bool: return self._resolve(response) + def failure(self, exception: BaseException) -> bool: + """ + Implement by subclasses to have the listener enter into an error state. + + Return value indicates whether or not the listener is failable. + """ + raise NotImplementedError() # pragma: no cover + def _resolve(self, response: t.CommandBase) -> bool: """ Implemented by subclasses to handle matched commands. @@ -118,6 +126,13 @@ def _resolve(self, response: t.CommandBase) -> bool: self.future.set_result(response) return True + def failure(self, exception: BaseException) -> bool: + if self.future.done(): + return False + + self.future.set_exception(exception) + return True + def cancel(self): if not self.future.done(): self.future.cancel() @@ -149,6 +164,10 @@ def _resolve(self, response: t.CommandBase) -> bool: # Callbacks are always resolved return True + def failure(self, exception: BaseException) -> bool: + # You can't fail a callback + return False + def cancel(self): # You can't cancel a callback return False @@ -161,6 +180,9 @@ class CatchAllResponse: header = object() # sentinel + def failure(self, exception: BaseException) -> bool: + return False + def matches(self, other) -> bool: return True diff --git a/zigpy_znp/zigbee/application.py b/zigpy_znp/zigbee/application.py index 46ef99ed..25c42b5e 100644 --- a/zigpy_znp/zigbee/application.py +++ b/zigpy_znp/zigbee/application.py @@ -25,7 +25,11 @@ import zigpy_znp.commands as c from zigpy_znp.api import ZNP from zigpy_znp.utils import combine_concurrent_calls -from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse +from zigpy_znp.exceptions import ( + ControllerResetting, + CommandNotRecognized, + InvalidCommandResponse, +) from zigpy_znp.types.nvids import OsalNvIds from zigpy_znp.zigbee.device import ZNPCoordinator @@ -687,6 +691,8 @@ async def _watchdog_loop(self): try: await self._znp.request(c.SYS.Ping.Req()) + except ControllerResetting: + LOGGER.debug("Controller is resetting, ignoring watchdog failure") except Exception as e: LOGGER.error( "Watchdog check failed",