diff --git a/tests/test_api.py b/tests/test_api.py index 62bbbee..02f6218 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -488,12 +488,37 @@ async def withhold(request: p.Request, request_id: int) -> None: assert api.disconnects == [None] +async def test_startup_hello_ignored( + api: RecordingApi, transport: SyntheticBinaryTransport +) -> None: + async def start_then_reply(request: p.Request, request_id: int) -> None: + transport.notify( + p.NotificationCommand.HELLO, + 0, + p.Hello( + protocol_version=t.uint8_t(p.PROTOCOL_VERSION), + configured=t.Bool(False), + ), + ) + transport.ok(request.command, request_id) + + # Embedded firmware starts its stack on the first host frame and announces it + # with a `hello` before the reply: that opens the session, it is not a reboot + transport.handlers[p.RequestCommand.RESET] = start_then_reply + assert await api.request(p.Reset(hard=t.Bool(False))) is None + + assert api.disconnects == [] + assert api.notifications == [] + + async def test_hello_reported_as_disconnect( api: RecordingApi, transport: SyntheticBinaryTransport ) -> None: async def withhold(request: p.Request, request_id: int) -> None: return None + assert await api.request(p.Shutdown()) is None + transport.handlers[p.RequestCommand.SHUTDOWN] = withhold request = asyncio.ensure_future(api.request(p.Shutdown())) await asyncio.sleep(0) diff --git a/zigpy_ziggurat/zigbee/api.py b/zigpy_ziggurat/zigbee/api.py index 5d80fc1..d2c4102 100644 --- a/zigpy_ziggurat/zigbee/api.py +++ b/zigpy_ziggurat/zigbee/api.py @@ -56,6 +56,7 @@ def __init__( ] = {} self._awaiting_aps_ack: set[int] = set() self._transport: Transport | None = None + self._received_frame = False async def connect(self) -> None: self._transport = await connect_transport( @@ -201,6 +202,8 @@ def _handle_frame(self, frame: bytes) -> None: _LOGGER.debug("Received notification (id=%d): %r", request_id, notification) self._handle_notification(request_id, notification) + self._received_frame = True + def _handle_response(self, request_id: int, body: bytes) -> None: pending = self._pending.get(request_id) if pending is None or pending.response.done(): @@ -261,7 +264,11 @@ def _handle_notification( if confirm is not None and not confirm.done(): confirm.set_result(notification) elif isinstance(notification, p.Hello): - # The firmware only sends `hello` when it reboots + # Embedded firmware starts its stack on the first frame the host sends, and + # the stack announces itself before it answers. So a `hello` that opens the + # session is that start. Any later `hello` means the firmware rebooted. + if not self._received_frame: + return self._connection_lost(ConnectionError("Ziggurat firmware reset")) elif isinstance(notification, p.LastReset): logging.getLogger("ziggurat.fw").warning(