isolate Modbus fix

This commit is contained in:
Stefan Allius
2024-06-16 13:00:02 +02:00
parent 377c09bc66
commit b688d04836
10 changed files with 15 additions and 112 deletions

View File

@@ -64,10 +64,7 @@ COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
COPY config . COPY config .
COPY src . COPY src .
RUN date > /build-date.txt RUN date > /build-date.txt
EXPOSE 5005 8127 10000 EXPOSE 5005
# HEALTHCHECK --interval=10s --timeout=3s \
# CMD wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1
# command to run on container start # command to run on container start
ENTRYPOINT ["/root/entrypoint.sh"] ENTRYPOINT ["/root/entrypoint.sh"]

View File

@@ -17,5 +17,6 @@ if [ "$environment" = "production" ] ; then \
-name od -o \ -name od -o \
-name strings -o \ -name strings -o \
-name su -o \ -name su -o \
-name wget -o \
\) -delete \ \) -delete \
; fi ; fi

View File

@@ -1,4 +1,3 @@
aiomqtt==2.0.1 aiomqtt==2.0.1
schema==0.7.5 schema==0.7.5
aiocron==1.8 aiocron==1.8
aiohttp==3.9.5

View File

@@ -1,6 +1,5 @@
import logging import logging
import traceback import traceback
import time
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from messages import hex_dump_memory from messages import hex_dump_memory
from typing import Self from typing import Self
@@ -18,8 +17,6 @@ class AsyncStream():
self.addr = addr self.addr = addr
self.r_addr = '' self.r_addr = ''
self.l_addr = '' self.l_addr = ''
self.proc_start = None # start processing start timestamp
self.proc_max = 0
async def server_loop(self, addr: str) -> None: async def server_loop(self, addr: str) -> None:
'''Loop for receiving messages from the inverter (server-side)''' '''Loop for receiving messages from the inverter (server-side)'''
@@ -64,14 +61,8 @@ class AsyncStream():
"""Async loop handler for precessing all received messages""" """Async loop handler for precessing all received messages"""
self.r_addr = self.writer.get_extra_info('peername') self.r_addr = self.writer.get_extra_info('peername')
self.l_addr = self.writer.get_extra_info('sockname') self.l_addr = self.writer.get_extra_info('sockname')
self.proc_start = time.time()
while True: while True:
try: try:
proc = time.time() - self.proc_start
if proc > self.proc_max:
self.proc_max = proc
self.proc_start = None
await self.__async_read() await self.__async_read()
if self.unique_id: if self.unique_id:
@@ -126,15 +117,6 @@ class AsyncStream():
logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}') logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
self.writer.close() self.writer.close()
def healthy(self) -> bool:
elapsed = 0
if self.proc_start is not None:
elapsed = time.time() - self.proc_start
logging.debug('async_stream healthy() elapsed: '
f'{round(1000*elapsed)}ms'
f' max:{round(1000*self.proc_max)}ms')
return elapsed < 5
''' '''
Our private methods Our private methods
''' '''
@@ -142,7 +124,6 @@ class AsyncStream():
"""Async read handler to read received data from TCP stream""" """Async read handler to read received data from TCP stream"""
data = await self.reader.read(4096) data = await self.reader.read(4096)
if data: if data:
self.proc_start = time.time()
self._recv_buffer += data self._recv_buffer += data
self.read() # call read in parent class self.read() # call read in parent class
else: else:

View File

@@ -3,7 +3,6 @@
import shutil import shutil
import tomllib import tomllib
import logging import logging
from typing import Tuple
from schema import Schema, And, Or, Use, Optional from schema import Schema, And, Or, Use, Optional
@@ -85,7 +84,7 @@ class Config():
) )
@classmethod @classmethod
def class_init(cls) -> None | str: # pragma: no cover def class_init(cls): # pragma: no cover
try: try:
# make the default config transparaent by copying it # make the default config transparaent by copying it
# in the config.example file # in the config.example file
@@ -95,12 +94,11 @@ class Config():
"config/config.example.toml") "config/config.example.toml")
except Exception: except Exception:
pass pass
return cls.read() cls.read()
@classmethod @classmethod
def _read_config_file(cls) -> Tuple[dict, None | str]: # pragma: no cover def _read_config_file(cls) -> dict: # pragma: no cover
usr_config = {} usr_config = {}
err = None
try: try:
with open("config/config.toml", "rb") as f: with open("config/config.toml", "rb") as f:
@@ -112,7 +110,7 @@ class Config():
'\n To create the missing config.toml file, ' '\n To create the missing config.toml file, '
'you can rename the template config.example.toml\n' 'you can rename the template config.example.toml\n'
' and customize it for your scenario.\n') ' and customize it for your scenario.\n')
return usr_config, err return usr_config
@classmethod @classmethod
def read(cls, path='') -> None | str: def read(cls, path='') -> None | str:
@@ -131,7 +129,7 @@ class Config():
# overwrite the default values, with values from # overwrite the default values, with values from
# the config.toml file # the config.toml file
usr_config, err = cls._read_config_file() usr_config = cls._read_config_file()
# merge the default and the user config # merge the default and the user config
config = def_config.copy() config = def_config.copy()

View File

@@ -31,10 +31,6 @@ class ConnectionG3(AsyncStream, Talent):
async def async_publ_mqtt(self) -> None: async def async_publ_mqtt(self) -> None:
pass pass
def healthy(self) -> bool:
logger.debug('ConnectionG3 healthy()')
return AsyncStream.healthy(self)
''' '''
Our private methods Our private methods
''' '''

View File

@@ -31,10 +31,6 @@ class ConnectionG3P(AsyncStream, SolarmanV5):
async def async_publ_mqtt(self) -> None: async def async_publ_mqtt(self) -> None:
pass pass
def healthy(self) -> bool:
logger.debug('ConnectionG3P healthy()')
return AsyncStream.healthy(self)
''' '''
Our private methods Our private methods
''' '''

View File

@@ -3,7 +3,6 @@ import asyncio
import signal import signal
import os import os
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from aiohttp import web
from logging import config # noqa F401 from logging import config # noqa F401
from messages import Message from messages import Message
from inverter import Inverter from inverter import Inverter
@@ -12,51 +11,6 @@ from gen3plus.inverter_g3p import InverterG3P
from scheduler import Schedule from scheduler import Schedule
from config import Config 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 request 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): async def handle_client(reader: StreamReader, writer: StreamWriter):
'''Handles a new incoming connection and starts an async loop''' '''Handles a new incoming connection and starts an async loop'''
@@ -72,12 +26,10 @@ async def handle_client_v2(reader: StreamReader, writer: StreamWriter):
await InverterG3P(reader, writer, addr).server_loop(addr) await InverterG3P(reader, writer, addr).server_loop(addr)
async def handle_shutdown(loop, runner): async def handle_shutdown(loop):
'''Close all TCP connections and stop the event loop''' '''Close all TCP connections and stop the event loop'''
logging.info('Shutdown due to SIGTERM') logging.info('Shutdown due to SIGTERM')
await runner.cleanup()
logging.info('HTTP server stopped')
# #
# first, disc all open TCP connections gracefully # first, disc all open TCP connections gracefully
@@ -135,22 +87,15 @@ if __name__ == "__main__":
logging.getLogger('tracer').setLevel(log_level) logging.getLogger('tracer').setLevel(log_level)
# logging.getLogger('mqtt').setLevel(log_level) # logging.getLogger('mqtt').setLevel(log_level)
# read config file
Config.class_init()
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
# read config file
ConfigErr = Config.class_init()
logging.debug(f'ConfigErr: {ConfigErr}')
Inverter.class_init() Inverter.class_init()
Schedule.start() 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 # Register some UNIX Signal handler for a gracefully server shutdown
# on Docker restart and stop # on Docker restart and stop
@@ -158,7 +103,7 @@ if __name__ == "__main__":
for signame in ('SIGINT', 'SIGTERM'): for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame), loop.add_signal_handler(getattr(signal, signame),
lambda loop=loop: asyncio.create_task( lambda loop=loop: asyncio.create_task(
handle_shutdown(loop, runner))) handle_shutdown(loop)))
# #
# Create tasks for our listening servers. These must be tasks! If we call # Create tasks for our listening servers. These must be tasks! If we call
@@ -167,16 +112,12 @@ if __name__ == "__main__":
# #
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005)) 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(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
loop.create_task(webserver(runner, '0.0.0.0', 8127))
try: try:
if ConfigErr is None:
proxy_is_up = True
loop.run_forever() loop.run_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
finally: finally:
proxy_is_up = False
Inverter.class_close(loop) Inverter.class_close(loop)
logging.info('Close event loop') logging.info('Close event loop')
loop.close() loop.close()

View File

@@ -2,7 +2,6 @@
import tomllib import tomllib
from schema import SchemaMissingKeyError from schema import SchemaMissingKeyError
from app.src.config import Config from app.src.config import Config
from typing import Tuple
class TstConfig(Config): class TstConfig(Config):
@@ -11,8 +10,8 @@ class TstConfig(Config):
cls.config = cnf cls.config = cnf
@classmethod @classmethod
def _read_config_file(cls) -> Tuple[dict, str| None]: def _read_config_file(cls) -> dict:
return cls.config, None return cls.config
def test_empty_config(): def test_empty_config():

View File

@@ -77,15 +77,10 @@ services:
- ${DNS2:-4.4.4.4} - ${DNS2:-4.4.4.4}
ports: ports:
- 5005:5005 - 5005:5005
- 8127:8127
- 10000:10000 - 10000:10000
volumes: volumes:
- ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log - ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log
- ${PROJECT_DIR:-./}tsun-proxy/config:/home/tsun-proxy/config - ${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: networks:
- outside - outside