From 3cebab40c8ce475e2ddb59222e6e6c28ded3d465 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 13:26:05 +0200 Subject: [PATCH 01/16] add heaithy handler --- app/src/async_stream.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 3deeb43..bd2352d 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -1,5 +1,6 @@ import logging import traceback +import time from asyncio import StreamReader, StreamWriter from messages import hex_dump_memory from typing import Self @@ -117,6 +118,15 @@ class AsyncStream(): logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}') self.writer.close() + def healthy(self) -> bool: + elapsed = 0 + if self.proc_start is not None: + elapsed = time.time() - self.proc_start + logging.info('async_stream healthy() elapsed: ' + f'{round(1000*elapsed)}ms' + f' max:{round(1000*self.proc_max)}ms') + return elapsed < 5 + ''' Our private methods ''' From fa7bfe9e16baecd2e9254fa3417754298fd09f8a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 13:29:43 +0200 Subject: [PATCH 02/16] log unrelease references --- app/src/async_stream.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index bd2352d..ba0204c 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -5,6 +5,7 @@ from asyncio import StreamReader, StreamWriter from messages import hex_dump_memory from typing import Self +import gc logger = logging.getLogger('conn') @@ -125,6 +126,7 @@ class AsyncStream(): logging.info('async_stream healthy() elapsed: ' f'{round(1000*elapsed)}ms' f' max:{round(1000*self.proc_max)}ms') + logging.info(f'Healthy()) refs: {gc.get_referrers(self)}') return elapsed < 5 ''' From 4600fc9577d768e80a9a228ae0f5d3f428a93161 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 17:46:51 +0200 Subject: [PATCH 03/16] add healtcheck --- docker-compose.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4566a80..9b4f9e8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -77,10 +77,15 @@ services: - ${DNS2:-4.4.4.4} ports: - 5005:5005 + - 8127:8127 - 10000:10000 volumes: - ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log - ${PROJECT_DIR:-./}tsun-proxy/config:/home/tsun-proxy/config + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1 + interval: 10s + timeout: 3s networks: - outside From 94f7f5faa247b027c097d1d3e1804b3e5acf5e2f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 17:47:13 +0200 Subject: [PATCH 04/16] complete exposed port list --- app/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Dockerfile b/app/Dockerfile index 0ef685e..8b2d706 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -64,7 +64,7 @@ COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh COPY config . COPY src . RUN date > /build-date.txt -EXPOSE 5005 +EXPOSE 5005 8127 10000 # command to run on container start ENTRYPOINT ["/root/entrypoint.sh"] From 5e360e1139e59e215bbe8f68052f9e5e58171df7 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 17:47:46 +0200 Subject: [PATCH 05/16] add wget for healtcheck --- app/hardening_final.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/app/hardening_final.sh b/app/hardening_final.sh index c6896bb..279e1b6 100644 --- a/app/hardening_final.sh +++ b/app/hardening_final.sh @@ -17,6 +17,5 @@ if [ "$environment" = "production" ] ; then \ -name od -o \ -name strings -o \ -name su -o \ - -name wget -o \ \) -delete \ ; fi From f5e7aa4292bb0e7f2042468c3741b20db736ea8e Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 17:48:17 +0200 Subject: [PATCH 06/16] add aiohttp --- app/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index b151101..ed9dcb0 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,3 +1,4 @@ aiomqtt==2.0.1 schema==0.7.5 - aiocron==1.8 \ No newline at end of file + aiocron==1.8 + aiohttp==3.9.5 \ No newline at end of file From e0568291f673a43df51a5ce92205968e644b2e81 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 17:50:09 +0200 Subject: [PATCH 07/16] use Enum class for State --- app/src/gen3/talent.py | 16 ++++++++-------- app/src/gen3plus/solarman_v5.py | 16 ++++++++-------- app/src/messages.py | 12 ++++++++---- app/tests/test_solarman.py | 24 ++++++++++++------------ app/tests/test_talent.py | 29 +++++++++++++++-------------- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 57ac794..fa3fa8e 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -4,12 +4,12 @@ import time from datetime import datetime if __name__ == "app.src.gen3.talent": - from app.src.messages import hex_dump_memory, Message + from app.src.messages import hex_dump_memory, Message, State from app.src.modbus import Modbus from app.src.config import Config from app.src.gen3.infos_g3 import InfosG3 else: # pragma: no cover - from messages import hex_dump_memory, Message + from messages import hex_dump_memory, Message, State from modbus import Modbus from config import Config from gen3.infos_g3 import InfosG3 @@ -71,13 +71,13 @@ class Talent(Message): Our puplic methods ''' def close(self) -> None: - logging.debug('Talent.close()') + logging.info('Talent.close()') # we have refernces to methods of this class in self.switch # so we have to erase self.switch, otherwise this instance can't be # deallocated by the garbage collector ==> we get a memory leak self.switch.clear() self.log_lvl.clear() - self.state = self.STATE_CLOSED + self.state = State.closed super().close() def __set_serial_no(self, serial_no: str): @@ -141,7 +141,7 @@ class Talent(Message): return def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str): - if self.state != self.STATE_UP: + if self.state != State.up: logger.warning(f'[{self.node_id}] ignore MODBUS cmd,' ' cause the state is not UP anymore') return @@ -158,7 +158,7 @@ class Talent(Message): self._send_buffer = bytearray(0) # self._send_buffer[sent:] async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None: - if self.state != self.STATE_UP: + if self.state != State.up: logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,' ' as the state is not UP') return @@ -371,7 +371,7 @@ class Talent(Message): self._send_buffer += b'\x01' self.__finish_send_msg() self.__process_data() - self.state = self.STATE_UP + self.state = State.up elif self.ctrl.is_resp(): return # ignore received response @@ -387,7 +387,7 @@ class Talent(Message): self._send_buffer += b'\x01' self.__finish_send_msg() self.__process_data() - self.state = self.STATE_UP + self.state = State.up elif self.ctrl.is_resp(): return # ignore received response diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index dd209b0..f8e2c8a 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -6,13 +6,13 @@ import asyncio from datetime import datetime if __name__ == "app.src.gen3plus.solarman_v5": - from app.src.messages import hex_dump_memory, Message + from app.src.messages import hex_dump_memory, Message, State from app.src.modbus import Modbus from app.src.config import Config from app.src.gen3plus.infos_g3p import InfosG3P from app.src.infos import Register else: # pragma: no cover - from messages import hex_dump_memory, Message + from messages import hex_dump_memory, Message, State from config import Config from modbus import Modbus from gen3plus.infos_g3p import InfosG3P @@ -135,7 +135,7 @@ class SolarmanV5(Message): # deallocated by the garbage collector ==> we get a memory leak self.switch.clear() self.log_lvl.clear() - self.state = self.STATE_CLOSED + self.state = State.closed super().close() def __set_serial_no(self, snr: int): @@ -345,7 +345,7 @@ class SolarmanV5(Message): self.__finish_send_msg() def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str): - if self.state != self.STATE_UP: + if self.state != State.up: logger.warning(f'[{self.node_id}] ignore MODBUS cmd,' ' cause the state is not UP anymore') return @@ -360,7 +360,7 @@ class SolarmanV5(Message): self._send_buffer = bytearray(0) # self._send_buffer[sent:] async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None: - if self.state != self.STATE_UP: + if self.state != State.up: logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,' ' as the state is not UP') return @@ -371,7 +371,7 @@ class SolarmanV5(Message): cmd.startswith(tuple(self.at_acl[connection]['block'])) async def send_at_cmd(self, AT_cmd: str) -> None: - if self.state != self.STATE_UP: + if self.state != State.up: logger.warning(f'[{self.node_id}] ignore AT+ cmd,' ' as the state is not UP') return @@ -471,7 +471,7 @@ class SolarmanV5(Message): self.__process_data(ftype) self.__forward_msg() self.__send_ack_rsp(0x1210, ftype) - self.state = self.STATE_UP + self.state = State.up def msg_sync_start(self): data = self._recv_buffer[self.header_len:] @@ -567,7 +567,7 @@ class SolarmanV5(Message): self.__forward_msg() self.__send_ack_rsp(0x1710, ftype) - self.state = self.STATE_UP + self.state = State.up def msg_sync_end(self): data = self._recv_buffer[self.header_len:] diff --git a/app/src/messages.py b/app/src/messages.py index 6553f36..4743b1f 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -1,6 +1,7 @@ import logging import weakref from typing import Callable, Generator +from enum import Enum if __name__ == "app.src.messages": @@ -52,11 +53,14 @@ class IterRegistry(type): yield obj +class State(Enum): + init = 0 + up = 2 + closed = 3 + + class Message(metaclass=IterRegistry): _registry = [] - STATE_INIT = 0 - STATE_UP = 2 - STATE_CLOSED = 3 def __init__(self, server_side: bool, send_modbus_cb: Callable[[bytes, int, str], None], mb_timeout: int): @@ -78,7 +82,7 @@ class Message(metaclass=IterRegistry): self._send_buffer = bytearray(0) self._forward_buffer = bytearray(0) self.new_data = {} - self.state = self.STATE_INIT + self.state = State.init ''' Empty methods, that have to be implemented in any child class which diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index ef29998..dbbaa8d 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -1682,21 +1682,21 @@ def test_zombie_conn(ConfigTsunInv1, MsgInverterInd): m1 = MemoryStream(MsgInverterInd, (0,)) m2 = MemoryStream(MsgInverterInd, (0,)) m3 = MemoryStream(MsgInverterInd, (0,)) - assert m1.state == m1.STATE_INIT - assert m2.state == m2.STATE_INIT - assert m3.state == m3.STATE_INIT + assert m1.state == m1.State.init + assert m2.state == m2.State.init + assert m3.state == m3.State.init m1.read() # read complete msg, and set unique_id - assert m1.state == m1.STATE_INIT - assert m2.state == m2.STATE_INIT - assert m3.state == m3.STATE_INIT + assert m1.state == m1.State.init + assert m2.state == m2.State.init + assert m3.state == m3.State.init m2.read() # read complete msg, and set unique_id - assert m1.state == m1.STATE_CLOSED - assert m2.state == m2.STATE_INIT - assert m3.state == m3.STATE_INIT + assert m1.state == m1.State.closed + assert m2.state == m2.State.init + assert m3.state == m3.State.init m3.read() # read complete msg, and set unique_id - assert m1.state == m1.STATE_CLOSED - assert m2.state == m2.STATE_CLOSED - assert m3.state == m3.STATE_INIT + assert m1.state == m1.State.closed + assert m2.state == m2.State.closed + assert m3.state == m3.State.init m1.close() m2.close() m3.close() diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 7632a09..6256811 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -4,6 +4,7 @@ from app.src.gen3.talent import Talent, Control from app.src.config import Config from app.src.infos import Infos, Register from app.src.modbus import Modbus +from app.src.messages import State pytest_plugins = ('pytest_asyncio',) @@ -909,7 +910,7 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd): ConfigTsunInv1 m = MemoryStream(b'') m.id_str = b"R170000000000001" - m.state = m.STATE_UP + m.state = State.up c = m.createClientStream(MsgModbusCmd) @@ -1159,7 +1160,7 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd): assert m._send_buffer == b'' assert m.writer.sent_pdu == b'' - m.state = m.STATE_UP + m.state = State.up await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG) assert 0 == m.send_msg_ofs assert m._forward_buffer == b'' @@ -1189,21 +1190,21 @@ def test_zombie_conn(ConfigTsunInv1, MsgInverterInd): m3 = MemoryStream(MsgInverterInd, (0,)) assert MemoryStream._RefNo == 3 + start_val assert m3.RefNo == 3 + start_val - assert m1.state == m1.STATE_INIT - assert m2.state == m2.STATE_INIT - assert m3.state == m3.STATE_INIT + assert m1.state == m1.State.init + assert m2.state == m2.State.init + assert m3.state == m3.State.init m1.read() # read complete msg, and set unique_id - assert m1.state == m1.STATE_UP - assert m2.state == m2.STATE_INIT - assert m3.state == m3.STATE_INIT + assert m1.state == m1.State.up + assert m2.state == m2.State.init + assert m3.state == m3.State.init m2.read() # read complete msg, and set unique_id - assert m1.state == m1.STATE_CLOSED - assert m2.state == m2.STATE_UP - assert m3.state == m3.STATE_INIT + assert m1.state == m1.State.closed + assert m2.state == m2.State.up + assert m3.state == m3.State.init m3.read() # read complete msg, and set unique_id - assert m1.state == m1.STATE_CLOSED - assert m2.state == m2.STATE_CLOSED - assert m3.state == m3.STATE_UP + assert m1.state == m1.State.closed + assert m2.state == m2.State.closed + assert m3.state == m3.State.up m1.close() m2.close() m3.close() From da832232bb687d1ce6d803a860454697510d7bba Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 17:51:14 +0200 Subject: [PATCH 08/16] calc processing time for healthcheck --- app/src/async_stream.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index ba0204c..bc425ca 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -2,7 +2,7 @@ import logging import traceback import time from asyncio import StreamReader, StreamWriter -from messages import hex_dump_memory +from messages import hex_dump_memory, State from typing import Self import gc @@ -19,6 +19,8 @@ class AsyncStream(): self.addr = addr self.r_addr = '' self.l_addr = '' + self.proc_start = None # start processing start timestamp + self.proc_max = 0 async def server_loop(self, addr: str) -> None: '''Loop for receiving messages from the inverter (server-side)''' @@ -63,8 +65,14 @@ class AsyncStream(): """Async loop handler for precessing all received messages""" self.r_addr = self.writer.get_extra_info('peername') self.l_addr = self.writer.get_extra_info('sockname') + self.proc_start = time.time() while True: try: + proc = time.time() - self.proc_start + if proc > self.proc_max: + self.proc_max = proc + self.proc_start = None + await self.__async_read() if self.unique_id: @@ -123,10 +131,11 @@ class AsyncStream(): elapsed = 0 if self.proc_start is not None: elapsed = time.time() - self.proc_start - logging.info('async_stream healthy() elapsed: ' - f'{round(1000*elapsed)}ms' - f' max:{round(1000*self.proc_max)}ms') - logging.info(f'Healthy()) refs: {gc.get_referrers(self)}') + if self.state == State.closed or elapsed > 1: + logging.info(f'[{self.node_id}]' + f' act:{round(1000*elapsed)}ms' + f' max:{round(1000*self.proc_max)}ms') + logging.info(f'Healthy()) refs: {gc.get_referrers(self)}') return elapsed < 5 ''' @@ -136,6 +145,7 @@ class AsyncStream(): """Async read handler to read received data from TCP stream""" data = await self.reader.read(4096) if data: + self.proc_start = time.time() self._recv_buffer += data self.read() # call read in parent class else: From 4372e49a1e5cf70e477a391b8fd74233860c8f96 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 17:51:51 +0200 Subject: [PATCH 09/16] add HTTP server for healthcheck --- app/src/server.py | 70 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/app/src/server.py b/app/src/server.py index e941975..183ada0 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -3,6 +3,7 @@ import asyncio import signal import os from asyncio import StreamReader, StreamWriter +from aiohttp import web from logging import config # noqa F401 from messages import Message from inverter import Inverter @@ -11,6 +12,51 @@ from gen3plus.inverter_g3p import InverterG3P from scheduler import Schedule from config import Config +routes = web.RouteTableDef() +proxy_is_up = False + + +@routes.get('/') +async def hello(request): + return web.Response(text="Hello, world") + + +@routes.get('/-/ready') +async def ready(request): + if proxy_is_up: + status = 200 + text = 'Is ready' + else: + status = 503 + text = 'Not ready' + return web.Response(status=status, text=text) + + +@routes.get('/-/healthy') +async def healthy(request): + + if proxy_is_up: + # logging.info('web reqeust healthy()') + for stream in Message: + try: + res = stream.healthy() + if not res: + return web.Response(status=503, text="I have a problem") + except Exception as err: + logging.info(f'Exception:{err}') + + return web.Response(status=200, text="I'm fine") + + +async def webserver(runner, addr, port): + await runner.setup() + site = web.TCPSite(runner, addr, port) + await site.start() + logging.info(f'HTTP server listen on port: {port}') + + while True: + await asyncio.sleep(3600) # sleep forever + async def handle_client(reader: StreamReader, writer: StreamWriter): '''Handles a new incoming connection and starts an async loop''' @@ -26,10 +72,12 @@ async def handle_client_v2(reader: StreamReader, writer: StreamWriter): await InverterG3P(reader, writer, addr).server_loop(addr) -async def handle_shutdown(loop): +async def handle_shutdown(loop, runner): '''Close all TCP connections and stop the event loop''' logging.info('Shutdown due to SIGTERM') + await runner.cleanup() + logging.info('HTTP server stopped') # # first, disc all open TCP connections gracefully @@ -87,15 +135,23 @@ if __name__ == "__main__": logging.getLogger('tracer').setLevel(log_level) # logging.getLogger('mqtt').setLevel(log_level) - # read config file - Config.class_init() - loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) + # read config file + ConfigErr = Config.class_init() + if ConfigErr is not None: + logging.info(f'ConfigErr: {ConfigErr}') Inverter.class_init() Schedule.start() + # + # Setup webserver application and runner + # + app = web.Application() + app.add_routes(routes) + runner = web.AppRunner(app) + # # Register some UNIX Signal handler for a gracefully server shutdown # on Docker restart and stop @@ -103,7 +159,7 @@ if __name__ == "__main__": for signame in ('SIGINT', 'SIGTERM'): loop.add_signal_handler(getattr(signal, signame), lambda loop=loop: asyncio.create_task( - handle_shutdown(loop))) + handle_shutdown(loop, runner))) # # Create tasks for our listening servers. These must be tasks! If we call @@ -112,12 +168,16 @@ if __name__ == "__main__": # loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005)) loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000)) + loop.create_task(webserver(runner, '0.0.0.0', 8127)) try: + if ConfigErr is None: + proxy_is_up = True loop.run_forever() except KeyboardInterrupt: pass finally: + proxy_is_up = False Inverter.class_close(loop) logging.info('Close event loop') loop.close() From 8088e6ab3ca961702b0887658dd776f62528d5c3 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 18:13:07 +0200 Subject: [PATCH 10/16] cleanup --- app/src/gen3/talent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index fa3fa8e..a41e7e2 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -71,7 +71,7 @@ class Talent(Message): Our puplic methods ''' def close(self) -> None: - logging.info('Talent.close()') + logging.debug('Talent.close()') # we have refernces to methods of this class in self.switch # so we have to erase self.switch, otherwise this instance can't be # deallocated by the garbage collector ==> we get a memory leak From 8aa1ef59cecb5f65e44b8588be373327ea25d8a3 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 19:35:38 +0200 Subject: [PATCH 11/16] updat changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d12db8..bf96603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -- GEN3PLUS: dump invalid packages with wrong start or stop byte -- label debug imagages als `debug` - ## [unreleased] - MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93) - add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94) +- GEN3: make timestamp handling stateless, to avoid blocking when the TSUN cloud is down [#56](https://github.com/s-allius/tsun-gen3-proxy/issues/56) +- GEN3PLUS: dump invalid packages with wrong start or stop byte +- label debug imagages als debug - print imgae build time during proxy start - add type annotations - improve async unit test and fix pytest warnings @@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - catch all OSError errors in the read loop - log Modbus traces with different log levels - add Modbus fifo and timeout handler -- build version string in the same format as TSUN for GEN3 invterts +- build version string in the same format as TSUN for GEN3 inverters - add graceful shutdown - parse Modbus values and store them in the database - add cron task to request the output power every minute From d14cbe87a2e417ad3d17358af9692a4f68574e2c Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 22:43:59 +0200 Subject: [PATCH 12/16] add docstrings to state enum --- app/src/messages.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/messages.py b/app/src/messages.py index 4743b1f..35e31db 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -54,9 +54,15 @@ class IterRegistry(type): class State(Enum): + '''state of the logical connection''' init = 0 + '''just created''' + received = 1 + '''at least one packet received''' up = 2 + '''at least one cmd-rsp transaction''' closed = 3 + '''connection closed''' class Message(metaclass=IterRegistry): From f4b434cfefb06f90e5e4cfff335c221300621e56 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 22:45:13 +0200 Subject: [PATCH 13/16] set new state State.received --- app/src/gen3/talent.py | 3 +++ app/src/gen3plus/solarman_v5.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index a41e7e2..532e56b 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -114,6 +114,9 @@ class Talent(Message): if self.header_valid and len(self._recv_buffer) >= (self.header_len + self.data_len): + if self.state == State.init: + self.state = State.received + log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING) if callable(log_lvl): log_lvl = log_lvl() diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index f8e2c8a..d65e560 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -184,6 +184,9 @@ class SolarmanV5(Message): self._recv_buffer, self.header_len+self.data_len+2) if self.__trailer_is_ok(self._recv_buffer, self.header_len + self.data_len + 2): + if self.state == State.init: + self.state = State.received + self.__set_serial_no(self.snr) self.__dispatch_msg() self.__flush_recv_msg() From 373916bead86e7dc8c332fb32911432036a16887 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 22:47:45 +0200 Subject: [PATCH 14/16] add healthy method --- app/src/gen3/connection_g3.py | 4 ++++ app/src/gen3plus/connection_g3p.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py index 7730069..7e7b96d 100644 --- a/app/src/gen3/connection_g3.py +++ b/app/src/gen3/connection_g3.py @@ -31,6 +31,10 @@ class ConnectionG3(AsyncStream, Talent): async def async_publ_mqtt(self) -> None: pass + def healthy(self) -> bool: + logger.debug('ConnectionG3 healthy()') + return AsyncStream.healthy(self) + ''' Our private methods ''' diff --git a/app/src/gen3plus/connection_g3p.py b/app/src/gen3plus/connection_g3p.py index ecc5625..352ba5e 100644 --- a/app/src/gen3plus/connection_g3p.py +++ b/app/src/gen3plus/connection_g3p.py @@ -31,6 +31,10 @@ class ConnectionG3P(AsyncStream, SolarmanV5): async def async_publ_mqtt(self) -> None: pass + def healthy(self) -> bool: + logger.debug('ConnectionG3P healthy()') + return AsyncStream.healthy(self) + ''' Our private methods ''' From 7d058e74fef21965515853bfc85711076aa63015 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 22:54:56 +0200 Subject: [PATCH 15/16] log healthcheck infos with DEBUG level --- app/src/async_stream.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index bc425ca..1b55ade 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -132,10 +132,10 @@ class AsyncStream(): if self.proc_start is not None: elapsed = time.time() - self.proc_start if self.state == State.closed or elapsed > 1: - logging.info(f'[{self.node_id}]' - f' act:{round(1000*elapsed)}ms' - f' max:{round(1000*self.proc_max)}ms') - logging.info(f'Healthy()) refs: {gc.get_referrers(self)}') + logging.debug(f'[{self.node_id}:{type(self).__name__}]' + f' act:{round(1000*elapsed)}ms' + f' max:{round(1000*self.proc_max)}ms') + logging.debug(f'Healthy()) refs: {gc.get_referrers(self)}') return elapsed < 5 ''' From c71994c839263a73afeb5d0571047e585fd776b1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 22:58:04 +0200 Subject: [PATCH 16/16] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf96603..8da2ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- Add healthcheck, readiness and liveness checks [#91](https://github.com/s-allius/tsun-gen3-proxy/issues/91) - MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93) - add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94) - GEN3: make timestamp handling stateless, to avoid blocking when the TSUN cloud is down [#56](https://github.com/s-allius/tsun-gen3-proxy/issues/56)