From e96f08785a04846e18ea6369f244fba1c99fa8d7 Mon Sep 17 00:00:00 2001 From: Peter Dolkens Date: Wed, 5 Nov 2025 13:32:59 +1100 Subject: [PATCH 1/2] Improve keepalive recovery handling --- pyintesishome2/intesisbase.py | 25 ++++++++++++++++++++++--- pyintesishome2/intesisbox.py | 8 +++++++- pyintesishome2/intesishome.py | 19 ++++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/pyintesishome2/intesisbase.py b/pyintesishome2/intesisbase.py index 7aed3d0..9e916a6 100644 --- a/pyintesishome2/intesisbase.py +++ b/pyintesishome2/intesisbase.py @@ -73,18 +73,23 @@ async def _set_value(self, device_id, uid, value): """Internal method to send a value to the device.""" raise NotImplementedError() - async def _send_command(self, command: str): + async def _send_command(self, command: str, *, wait_for_response: bool = True): try: _LOGGER.debug("Preparing to send command: %s", command) - self._received_response.clear() - if not self._writer: + if wait_for_response: + self._received_response.clear() + if not self._writer or self._writer.is_closing(): _LOGGER.error("No writer available. Cannot send command.") + self._connected = False return _LOGGER.debug("Writer state: %r", self._writer) encoded_command = command.encode("ascii") _LOGGER.debug("Encoded command: %r (length: %d)", encoded_command, len(encoded_command)) self._writer.write(encoded_command) await self._writer.drain() + if not wait_for_response: + _LOGGER.debug("Command sent without waiting for response.") + return _LOGGER.debug("Command sent and drained. Waiting for response event.") timeout = 15.0 start_time = asyncio.get_event_loop().time() @@ -161,6 +166,18 @@ async def _data_received(self): finally: self._connected = False self._connecting = False + if self._keepalive_task: + await self._cancel_task_if_exists(self._keepalive_task) + self._keepalive_task = None + if self._writer: + self._writer.close() + try: + await self._writer.wait_closed() + except Exception as exc: # pylint: disable=broad-except + _LOGGER.debug("Error while waiting for writer to close: %s", exc) + self._writer = None + self._reader = None + self._receive_task = None await self._send_update_callback() def _update_device_state(self, device_id, uid, value): @@ -197,7 +214,9 @@ async def stop(self): """Public method for shutting down connectivity.""" self._connected = False await self._cancel_task_if_exists(self._receive_task) + self._receive_task = None await self._cancel_task_if_exists(self._keepalive_task) + self._keepalive_task = None if self._writer: self._writer.close() await self._writer.wait_closed() diff --git a/pyintesishome2/intesisbox.py b/pyintesishome2/intesisbox.py index 84d62be..522fb22 100644 --- a/pyintesishome2/intesisbox.py +++ b/pyintesishome2/intesisbox.py @@ -167,7 +167,13 @@ async def _send_keepalive(self): try: while True: await asyncio.sleep(30) - await self._send_command("GET,1:AMBTEMP") + if not self._connected: + _LOGGER.debug("Stopping keepalive task because connection is inactive") + break + if not self._writer or self._writer.is_closing(): + _LOGGER.warning("Keepalive aborted because writer is not available") + break + await self._send_command("GET,1:AMBTEMP", wait_for_response=False) except asyncio.CancelledError: _LOGGER.debug("Cancelled the keepalive task") diff --git a/pyintesishome2/intesishome.py b/pyintesishome2/intesishome.py index b52248e..9a354ba 100644 --- a/pyintesishome2/intesishome.py +++ b/pyintesishome2/intesishome.py @@ -78,12 +78,25 @@ async def _send_keepalive(self): try: while True: await asyncio.sleep(120) - _LOGGER.debug("sending keepalive to {self._device_type}") - device_id = str(next(iter(self._devices))) + if not self._connected: + _LOGGER.debug("Stopping keepalive task because connection is inactive") + break + if not self._writer or self._writer.is_closing(): + _LOGGER.warning("Keepalive aborted because writer is not available") + break + if not self._devices: + _LOGGER.debug("No devices registered; skipping keepalive ping") + continue + _LOGGER.debug("sending keepalive to %s", self._device_type) + try: + device_id = str(next(iter(self._devices))) + except StopIteration: + _LOGGER.debug("No device id available for keepalive") + continue message = ( f'{{"command":"get","data":{{"deviceId":{device_id},"uid":10}}}}' ) - await self._send_command(message) + await self._send_command(message, wait_for_response=False) except asyncio.CancelledError: _LOGGER.debug("Cancelled the keepalive task") From 5fe5a0e3d319aabfe79a79df5521bdad2220ecfa Mon Sep 17 00:00:00 2001 From: Peter Dolkens Date: Wed, 5 Nov 2025 13:39:28 +1100 Subject: [PATCH 2/2] Reset connection state when keepalives fail --- pyintesishome2/intesisbase.py | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyintesishome2/intesisbase.py b/pyintesishome2/intesisbase.py index 9e916a6..146bbc0 100644 --- a/pyintesishome2/intesisbase.py +++ b/pyintesishome2/intesisbase.py @@ -81,6 +81,9 @@ async def _send_command(self, command: str, *, wait_for_response: bool = True): if not self._writer or self._writer.is_closing(): _LOGGER.error("No writer available. Cannot send command.") self._connected = False + await self.stop() + if wait_for_response: + self._received_response.set() return _LOGGER.debug("Writer state: %r", self._writer) encoded_command = command.encode("ascii") @@ -105,6 +108,8 @@ async def _send_command(self, command: str, *, wait_for_response: bool = True): _LOGGER.debug("Response event set! Command succeeded.") else: _LOGGER.error("Response event was never set. Command failed.") + except asyncio.CancelledError: + raise except OSError as exc: _LOGGER.error("%s Exception. %s / %s", type(exc), exc.args, exc) except Exception as exc: @@ -213,6 +218,7 @@ async def connect(self): async def stop(self): """Public method for shutting down connectivity.""" self._connected = False + self._connecting = False await self._cancel_task_if_exists(self._receive_task) self._receive_task = None await self._cancel_task_if_exists(self._keepalive_task) diff --git a/setup.py b/setup.py index c5d7a68..cd7be32 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name="pyintesishome2", - version="1.8.7", + version="1.8.8", description="A python3 library for running asynchronus communications with IntesisHome Smart AC Controllers", long_description=long_description, long_description_content_type="text/markdown",