From cc86eb8a747aadf306cb553f7aaa4c175e363479 Mon Sep 17 00:00:00 2001 From: Heng-Leung Chau Date: Sat, 14 Mar 2026 22:53:18 +0100 Subject: [PATCH] proxy: Add detection for stale/broken TCP connections Consider TCP connections as broken after user-specified time of no transmissions (currently set to 5 minutes) and closes the respective proxy socket pair from inverter to the growatt server. This is required for some environments (such as docker), where the grott proxy will simply not receive any data from the growatt inverter anymore. --- grottconf.py | 3 +++ grottproxy.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/grottconf.py b/grottconf.py index 62ce0d1f..abf29f64 100644 --- a/grottconf.py +++ b/grottconf.py @@ -34,6 +34,9 @@ def __init__(self, vrm): self.grottip = "default" #connect to server IP adress self.outfile ="sys.stdout" self.tmzone = "local" #set timezone (at this moment only used for influxdb) + + self.select_timeout_s = 60 * 1 # Timeout for select() + self.client_timeout_s = 60 * 5 # Time without any received packets from growatt device until it will be considered as disconnected #Growatt server default #self.growattip = "47.91.67.66" diff --git a/grottproxy.py b/grottproxy.py index 2b032904..a4b528a0 100644 --- a/grottproxy.py +++ b/grottproxy.py @@ -86,6 +86,7 @@ def start(self, host, port): return False class Proxy: + client_timeouts_s = {} input_list = [] channel = {} @@ -126,13 +127,32 @@ def main(self,conf): while 1: time.sleep(delay) ss = select.select - inputready, outputready, exceptready = ss(self.input_list, [], []) + inputready, outputready, exceptready = ss(self.input_list, [], [], conf.select_timeout_s) + if (not inputready): + for key in list(self.client_timeouts_s.keys()): + # Skip sockets, which have been removed in the meantime. + if (key not in self.client_timeouts_s): + continue + # Increase timeout counter. + self.client_timeouts_s[key] += conf.select_timeout_s + # If the timeout limit was reached, consider the socket to be broken or already + # closed by the remote. + if (self.client_timeouts_s[key] >= conf.client_timeout_s): + try: + print("\t - Grott connection closed (timeout): remote={}".format(key.getpeername())) + except: + print("\t - Grott connection closed (timeout)") + self.s = key + self.on_close(conf) + continue for self.s in inputready: if self.s == self.server: self.on_accept(conf) break try: self.data, self.addr = self.s.recvfrom(buffer_size) + # Reset timeout counter for the socket we just received from. + self.client_timeouts_s[self.s] = 0 except: if conf.verbose : print("\t - Grott connection error") self.on_close(conf) @@ -152,6 +172,9 @@ def on_accept(self,conf): self.input_list.append(forward) self.channel[clientsock] = forward self.channel[forward] = clientsock + # Initialize timeout counters for the new proxy and forwarder socket. + self.client_timeouts_s[clientsock] = 0 + self.client_timeouts_s[forward] = 0 else: if conf.verbose: print("\t - Can't establish connection with remote server."), @@ -177,6 +200,10 @@ def on_close(self,conf): # delete both objects from channel dict del self.channel[out] del self.channel[self.s] + # Delete timeout counter for proxy/grott socket. + del self.client_timeouts_s[self.s] + # Delete timeout counter for forwarder/growatt socket. + del self.client_timeouts_s[out] def on_recv(self,conf): data = self.data @@ -233,6 +260,8 @@ def on_recv(self,conf): # send data to destination self.channel[self.s].send(data) + # Reset timeout counter for the socket we just sent data to. + self.client_timeouts_s[self.channel[self.s]] = 0 if len(data) > conf.minrecl : #process received data procdata(conf,data)