From a2b2706e105721c90ae1e1579fc594125231a16a Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 17:42:39 +1300 Subject: [PATCH 01/12] use consistent logger --- custom_components/intesisbox/intesisbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 46e0524..2557774 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -8,7 +8,7 @@ from asyncio import ensure_future from time import sleep -_LOGGER = logging.getLogger('pyintesisbox') +_LOGGER = logging.getLogger(__name__) API_DISCONNECTED = "Disconnected" API_CONNECTING = "Connecting" From b388eae2fcd1791a25a54e1c08d4f995c0cf7a08 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 17:46:05 +1300 Subject: [PATCH 02/12] more consistent handling of edge cases in connect() --- custom_components/intesisbox/intesisbox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 2557774..1b12f13 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -172,10 +172,13 @@ def connect(self): ensure_future(coro, loop=self._eventLoop) else: _LOGGER.debug("Missing IP address or port.") + self._connectionStatus = API_DISCONNECTED except Exception as e: _LOGGER.error('%s Exception. %s / %s', type(e), repr(e.args), e) self._connectionStatus = API_DISCONNECTED + else: + _LOGGER.debug('connect() called but already connecting') def stop(self): """Public method for shutting down connectivity with the envisalink.""" From e551ca956d0427b134a74ab5fc8137cc6b4ef487 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 18:25:34 +1300 Subject: [PATCH 03/12] actually sleep for 1 minute before trying to reconnect --- custom_components/intesisbox/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/intesisbox/climate.py b/custom_components/intesisbox/climate.py index 1e7bd88..dc4b872 100644 --- a/custom_components/intesisbox/climate.py +++ b/custom_components/intesisbox/climate.py @@ -250,7 +250,7 @@ def set_swing_mode(self, swing_mode): async def async_update(self): """Copy values from controller dictionary to climate device.""" if not self._controller.is_connected: - await asyncio.sleep(1) # per device specs, wait min 1 sec before re-connecting + await asyncio.sleep(60) # per device specs, wait min 1 sec before re-connecting await self.hass.async_add_executor_job(self._controller.connect) self._connection_retries += 1 else: From 808e30810474d6a2223a525ef5611fe460c1d066 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 18:25:52 +1300 Subject: [PATCH 04/12] more logging cleanup --- custom_components/intesisbox/intesisbox.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 1b12f13..bd82c60 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -95,7 +95,8 @@ async def query_initial_state(self): def data_received(self, data): """asyncio callback when data is received on the socket""" decoded_data = data.decode('ascii') - _LOGGER.debug("Data received: {}".format(decoded_data)) + _LOGGER.debug(f"Data received: {decoded_data}") + linesReceived = decoded_data.splitlines() for line in linesReceived: cmdList = line.split(':', 1) @@ -148,6 +149,16 @@ def _parse_limits_received(self, args): self._vertical_vane_list = values elif function == FUNCTION_VANELR: self._horizontal_vane_list = values + + _LOGGER.debug( + "Updated limits: ", + f"{self._setpoint_minimum=}", + f"{self._setpoint_maximum=}", + f"{self._fan_speed_list=}", + f"{self._operation_list=}", + f"{self._vertical_vane_list=}", + f"{self._horizontal_vane_list=}", + ) return def connection_lost(self, exc): From 8481143203c23ad38aba7065084e9af91e42a948 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 18:26:22 +1300 Subject: [PATCH 05/12] don't run update callback unless something happens ie skip keepalive --- custom_components/intesisbox/intesisbox.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index bd82c60..28082e4 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -98,6 +98,8 @@ def data_received(self, data): _LOGGER.debug(f"Data received: {decoded_data}") linesReceived = decoded_data.splitlines() + statusChanged = False + for line in linesReceived: cmdList = line.split(':', 1) cmd = cmdList[0] @@ -108,12 +110,16 @@ def data_received(self, data): self._parse_id_received(args) self._connectionStatus = API_AUTHENTICATED asyncio.ensure_future(self.keep_alive()) + statusChanged = True elif cmd == 'CHN,1': self._parse_change_received(args) + statusChanged = True elif cmd == 'LIMITS': self._parse_limits_received(args) + statusChanged = True - self._send_update_callback() + if statusChanged: + self._send_update_callback() def _parse_id_received(self, args): # ID:Model,MAC,IP,Protocol,Version,RSSI From 6903bcd7cc5f0631d77634757aace19cb78dd390 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 18:32:04 +1300 Subject: [PATCH 06/12] actively poll for status on reconnect --- custom_components/intesisbox/intesisbox.py | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 28082e4..1e46ace 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -80,17 +80,18 @@ async def keep_alive(self): _LOGGER.debug("Not connected, skipping keepalive") async def query_initial_state(self): - self._transport.write("ID\r".encode('ascii')) - await asyncio.sleep(1) - self._transport.write("LIMITS:SETPTEMP\r".encode('ascii')) - await asyncio.sleep(1) - self._transport.write("LIMITS:FANSP\r".encode('ascii')) - await asyncio.sleep(1) - self._transport.write("LIMITS:MODE\r".encode('ascii')) - await asyncio.sleep(1) - self._transport.write("LIMITS:VANEUD\r".encode('ascii')) - await asyncio.sleep(1) - self._transport.write("LIMITS:VANELR\r".encode('ascii')) + cmds = [ + "ID", + "LIMITS:SETPTEMP", + "LIMITS:FANSP", + "LIMITS:MODE", + "LIMITS:VANEUD", + "LIMITS:VANELR", + "GET,1:*", + ] + for cmd in cmds: + self._transport.write(f"{cmd}\r".encode('ascii')) + await asyncio.sleep(1) def data_received(self, data): """asyncio callback when data is received on the socket""" From c936d0a0953329f16b4ff81f6bddae7dc858cad3 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 18:38:47 +1300 Subject: [PATCH 07/12] refactor write into a method with consistent logging --- custom_components/intesisbox/intesisbox.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 1e46ace..28b4828 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -74,7 +74,7 @@ async def keep_alive(self): """Send a keepalive command to reset it's watchdog timer.""" while self.is_connected: _LOGGER.debug("Sending keepalive") - self._transport.write("PING\r".encode('ascii')) + self._write("PING") await asyncio.sleep(45) else: _LOGGER.debug("Not connected, skipping keepalive") @@ -90,9 +90,13 @@ async def query_initial_state(self): "GET,1:*", ] for cmd in cmds: - self._transport.write(f"{cmd}\r".encode('ascii')) + self._write(cmd) await asyncio.sleep(1) + def _write(self, cmd): + self._transport.write(f"{cmd}\r".encode('ascii')) + _LOGGER.debug(f"Data sent: {cmd!r}") + def data_received(self, data): """asyncio callback when data is received on the socket""" decoded_data = data.decode('ascii') @@ -204,7 +208,7 @@ def stop(self): self._transport.close() def poll_status(self, sendcallback=False): - self._transport.write("GET,1:*\r".encode('ascii')) + self._write("GET,1:*") def set_temperature(self, setpoint): """Public method for setting the temperature""" @@ -225,10 +229,8 @@ def set_horizontal_vane(self, vane: str): def _set_value(self, uid, value): """Internal method to send a command to the API""" - message = "SET,{}:{},{}\r".format(1, uid, value) try: - self._transport.write(message.encode('ascii')) - _LOGGER.debug("Data sent: {!r}".format(message)) + self._write(f"SET,1:{uid},{value}") except Exception as e: _LOGGER.error('%s Exception. %s / %s', type(e), e.args, e) From 982f7971bd548dc9ac888f751494462d7ecf62ee Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 18:53:15 +1300 Subject: [PATCH 08/12] receive logging improvements --- custom_components/intesisbox/intesisbox.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 28b4828..9885a0c 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -99,10 +99,9 @@ def _write(self, cmd): def data_received(self, data): """asyncio callback when data is received on the socket""" - decoded_data = data.decode('ascii') - _LOGGER.debug(f"Data received: {decoded_data}") + _LOGGER.debug(f"Data received: {data!r}") - linesReceived = decoded_data.splitlines() + linesReceived = data.decode('ascii').splitlines() statusChanged = False for line in linesReceived: From b9a2ae8391b40edc8469017a9d56df438fc1c00a Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 19:06:51 +1300 Subject: [PATCH 09/12] log per line received --- custom_components/intesisbox/intesisbox.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 9885a0c..0629bbc 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -99,12 +99,11 @@ def _write(self, cmd): def data_received(self, data): """asyncio callback when data is received on the socket""" - _LOGGER.debug(f"Data received: {data!r}") - linesReceived = data.decode('ascii').splitlines() statusChanged = False for line in linesReceived: + _LOGGER.debug(f"Data received: {line!r}") cmdList = line.split(':', 1) cmd = cmdList[0] args = None From 265ce329b4edb733363ddd0eb464963814db5018 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 19:07:05 +1300 Subject: [PATCH 10/12] clean up fetching initial state --- custom_components/intesisbox/climate.py | 1 - custom_components/intesisbox/intesisbox.py | 13 +++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/custom_components/intesisbox/climate.py b/custom_components/intesisbox/climate.py index dc4b872..796763f 100644 --- a/custom_components/intesisbox/climate.py +++ b/custom_components/intesisbox/climate.py @@ -91,7 +91,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): controller = hass.data[DOMAIN][entry.entry_id] - controller.poll_status() async_add_entities([IntesisBoxAC(controller)], True) class IntesisBoxAC(ClimateEntity): diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 0629bbc..966939d 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -87,7 +87,6 @@ async def query_initial_state(self): "LIMITS:MODE", "LIMITS:VANEUD", "LIMITS:VANELR", - "GET,1:*", ] for cmd in cmds: self._write(cmd) @@ -113,7 +112,7 @@ def data_received(self, data): self._parse_id_received(args) self._connectionStatus = API_AUTHENTICATED asyncio.ensure_future(self.keep_alive()) - statusChanged = True + asyncio.ensure_future(self.poll_status()) elif cmd == 'CHN,1': self._parse_change_received(args) statusChanged = True @@ -205,8 +204,14 @@ def stop(self): self._connectionStatus = API_DISCONNECTED self._transport.close() - def poll_status(self, sendcallback=False): - self._write("GET,1:*") + async def poll_status(self, sendcallback=False): + """Periodically poll for updates since the controllers don't always update reliably""" + while self.is_connected: + _LOGGER.debug("Polling for update") + self._write("GET,1:*") + await asyncio.sleep(60*5) # 5 minutes + else: + _LOGGER.debug("Not connected, skipping poll_status()") def set_temperature(self, setpoint): """Public method for setting the temperature""" From 5b51ae441fa8aa24338a287b4addd5d714dab843 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 19:12:20 +1300 Subject: [PATCH 11/12] more debug logging --- custom_components/intesisbox/intesisbox.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index 966939d..c5c4098 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -132,6 +132,15 @@ def _parse_id_received(self, args): self._firmversion = info[4] self._rssi = info[5] + _LOGGER.debug( + "Updated info:", + f"model:{self._model}", + f"mac:{self._mac}", + f"version:{self._firmversion}", + f"rssi:{self._rssi}", + ) + + def _parse_change_received(self, args): function = args.split(',')[0] value = args.split(',')[1] @@ -139,6 +148,8 @@ def _parse_change_received(self, args): value = None self._device[function] = value + _LOGGER.debug(f"Updated state: {self._device!r}") + def _parse_limits_received(self, args): split_args = args.split(',', 1) From eb71f1c5a61c45ae19121eadd3b4be2e17b074f5 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Sun, 26 Mar 2023 19:12:56 +1300 Subject: [PATCH 12/12] fix null value --- custom_components/intesisbox/intesisbox.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/intesisbox/intesisbox.py b/custom_components/intesisbox/intesisbox.py index c5c4098..f5ad59a 100644 --- a/custom_components/intesisbox/intesisbox.py +++ b/custom_components/intesisbox/intesisbox.py @@ -35,7 +35,7 @@ FUNCTION_ERRSTATUS = 'ERRSTATUS' FUNCTION_ERRCODE = 'ERRCODE' -NULL_VALUE = '32768' +NULL_VALUE = '-32768' class IntesisBox(asyncio.Protocol): @@ -329,9 +329,6 @@ def ambient_temperature(self) -> float: temperature = self._device.get(FUNCTION_AMBTEMP) if temperature: temperature = int(temperature) / 10 - # When unsupported, -32768 is reported - if temperature == -3276.8: - temperature = None return temperature @property