Compare commits
6 Commits
s-allius/i
...
s-allius/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbca07bd59 | ||
|
|
e66d2e864b | ||
|
|
e19acbc514 | ||
|
|
6798753d84 | ||
|
|
7d5670b6b5 | ||
|
|
f98273a3eb |
@@ -1 +1 @@
|
||||
3.13.3
|
||||
3.13.2
|
||||
|
||||
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [unreleased]
|
||||
|
||||
- add web UI to add-on
|
||||
- allow `Y00` serial numbers for GEN3PLUS devices
|
||||
|
||||
## [0.13.0] - 2025-04-13
|
||||
|
||||
- update dependency python to 3.13
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
aiomqtt==2.3.2
|
||||
schema==0.7.7
|
||||
aiocron==2.1
|
||||
aiohttp==3.11.16
|
||||
quart==0.20
|
||||
@@ -96,8 +96,8 @@ class Proxy():
|
||||
Infos.new_stat_data[key] = False
|
||||
|
||||
@classmethod
|
||||
def class_close(cls, loop) -> None: # pragma: no cover
|
||||
async def class_close(cls, loop) -> None: # pragma: no cover
|
||||
logging.debug('Proxy.class_close')
|
||||
logging.info('Close MQTT Task')
|
||||
loop.run_until_complete(cls.mqtt.close())
|
||||
await cls.mqtt.close()
|
||||
cls.mqtt = None
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import logging.handlers
|
||||
import signal
|
||||
import os
|
||||
import argparse
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from aiohttp import web
|
||||
from quart import Quart, Response
|
||||
from logging import config # noqa F401
|
||||
from proxy import Proxy
|
||||
from inverter_ifc import InverterIfc
|
||||
@@ -16,63 +15,51 @@ from cnf.config import Config
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from web.routes import web_routes
|
||||
from modbus_tcp import ModbusTcp
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
proxy_is_up = False
|
||||
|
||||
class ProxyState:
|
||||
_is_up = False
|
||||
|
||||
@staticmethod
|
||||
def is_up() -> bool:
|
||||
return ProxyState._is_up
|
||||
|
||||
@staticmethod
|
||||
def set_up(value: bool):
|
||||
ProxyState._is_up = value
|
||||
|
||||
|
||||
@routes.get('/')
|
||||
async def hello(request):
|
||||
return web.Response(text="Hello, world")
|
||||
app = Quart(__name__)
|
||||
app.register_blueprint(web_routes)
|
||||
|
||||
|
||||
@routes.get('/-/ready')
|
||||
async def ready(request):
|
||||
if proxy_is_up:
|
||||
@app.route('/-/ready')
|
||||
async def ready():
|
||||
if ProxyState.is_up():
|
||||
status = 200
|
||||
text = 'Is ready'
|
||||
else:
|
||||
status = 503
|
||||
text = 'Not ready'
|
||||
return web.Response(status=status, text=text)
|
||||
return Response(status=status, response=text)
|
||||
|
||||
|
||||
@routes.get('/-/healthy')
|
||||
async def healthy(request):
|
||||
@app.route('/-/healthy')
|
||||
async def healthy():
|
||||
|
||||
if proxy_is_up:
|
||||
if ProxyState.is_up():
|
||||
# logging.info('web reqeust healthy()')
|
||||
for inverter in InverterIfc:
|
||||
try:
|
||||
res = inverter.healthy()
|
||||
if not res:
|
||||
return web.Response(status=503, text="I have a problem")
|
||||
return Response(status=503, response="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(addr, port):
|
||||
'''coro running our webserver'''
|
||||
app = web.Application()
|
||||
app.add_routes(routes)
|
||||
runner = web.AppRunner(app)
|
||||
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, addr, port)
|
||||
await site.start()
|
||||
logging.info(f'HTTP server listen on port: {port}')
|
||||
|
||||
try:
|
||||
# Normal interaction with aiohttp
|
||||
while True:
|
||||
await asyncio.sleep(3600) # sleep forever
|
||||
except asyncio.CancelledError:
|
||||
logging.info('HTTP server cancelled')
|
||||
await runner.cleanup()
|
||||
logging.debug('HTTP cleanup done')
|
||||
return Response(status=200, response="I'm fine")
|
||||
|
||||
|
||||
async def handle_client(reader: StreamReader, writer: StreamWriter, inv_class):
|
||||
@@ -82,12 +69,13 @@ async def handle_client(reader: StreamReader, writer: StreamWriter, inv_class):
|
||||
await inv.local.ifc.server_loop()
|
||||
|
||||
|
||||
async def handle_shutdown(loop, web_task):
|
||||
@app.after_serving
|
||||
async def handle_shutdown(): # pragma: no cover
|
||||
'''Close all TCP connections and stop the event loop'''
|
||||
|
||||
logging.info('Shutdown due to SIGTERM')
|
||||
global proxy_is_up
|
||||
proxy_is_up = False
|
||||
loop = asyncio.get_event_loop()
|
||||
ProxyState.set_up(False)
|
||||
|
||||
#
|
||||
# first, disc all open TCP connections gracefully
|
||||
@@ -97,24 +85,16 @@ async def handle_shutdown(loop, web_task):
|
||||
|
||||
logging.info('Proxy disconnecting done')
|
||||
|
||||
#
|
||||
# second, cancel the web server
|
||||
#
|
||||
web_task.cancel()
|
||||
await web_task
|
||||
|
||||
#
|
||||
# now cancel all remaining (pending) tasks
|
||||
#
|
||||
pending = asyncio.all_tasks()
|
||||
for task in pending:
|
||||
for task in asyncio.all_tasks():
|
||||
if task == asyncio.current_task():
|
||||
continue
|
||||
task.cancel()
|
||||
logging.info('Proxy cancelling done')
|
||||
|
||||
#
|
||||
# at last, start a coro for stopping the loop
|
||||
#
|
||||
logging.debug("Stop event loop")
|
||||
loop.stop()
|
||||
await Proxy.class_close(loop)
|
||||
|
||||
|
||||
def get_log_level() -> int | None:
|
||||
@@ -214,27 +194,20 @@ def main(): # pragma: no cover
|
||||
loop.create_task(asyncio.start_server(lambda r, w, i=inv_class:
|
||||
handle_client(r, w, i),
|
||||
'0.0.0.0', port))
|
||||
web_task = loop.create_task(webserver('0.0.0.0', 8127))
|
||||
|
||||
#
|
||||
# Register some UNIX Signal handler for a gracefully server shutdown
|
||||
# on Docker restart and stop
|
||||
#
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame),
|
||||
lambda loop=loop: asyncio.create_task(
|
||||
handle_shutdown(loop, web_task)))
|
||||
|
||||
loop.set_debug(log_level == logging.DEBUG)
|
||||
try:
|
||||
global proxy_is_up
|
||||
proxy_is_up = True
|
||||
loop.run_forever()
|
||||
ProxyState.set_up(True)
|
||||
logging.info("Start Quart")
|
||||
app.run(host='0.0.0.0', port=8127, use_reloader=False, loop=loop)
|
||||
logging.info("Quart stopped")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except asyncio.exceptions.CancelledError:
|
||||
logging.info("Quart cancelled")
|
||||
|
||||
finally:
|
||||
logging.info("Event loop is stopped")
|
||||
Proxy.class_close(loop)
|
||||
logging.debug('Close event loop')
|
||||
loop.close()
|
||||
logging.info(f'Finally, exit Server "{serv_name}"')
|
||||
|
||||
9
app/src/web/routes.py
Normal file
9
app/src/web/routes.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from quart import Blueprint
|
||||
from quart import Response
|
||||
|
||||
web_routes = Blueprint('web_routes', __name__)
|
||||
|
||||
|
||||
@web_routes.route('/')
|
||||
async def hello():
|
||||
return Response(response="Hello, world")
|
||||
@@ -3,7 +3,9 @@ import pytest
|
||||
import logging
|
||||
import os
|
||||
from mock import patch
|
||||
from server import get_log_level
|
||||
from server import get_log_level, app, ProxyState
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
def test_get_log_level():
|
||||
|
||||
@@ -30,3 +32,37 @@ def test_get_log_level():
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ready():
|
||||
"""Test the ready route."""
|
||||
|
||||
ProxyState.set_up(False)
|
||||
client = app.test_client()
|
||||
response = await client.get('/-/ready')
|
||||
assert response.status_code == 503
|
||||
result = await response.get_data()
|
||||
assert result == b"Not ready"
|
||||
|
||||
ProxyState.set_up(True)
|
||||
response = await client.get('/-/ready')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"Is ready"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthy():
|
||||
"""Test the healthy route."""
|
||||
|
||||
ProxyState.set_up(False)
|
||||
client = app.test_client()
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
|
||||
ProxyState.set_up(True)
|
||||
response = await client.get('/-/healthy')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"I'm fine"
|
||||
|
||||
15
app/tests/test_web_route.py
Normal file
15
app/tests/test_web_route.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from server import app
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_home():
|
||||
"""Test the home route."""
|
||||
client = app.test_client()
|
||||
response = await client.get('/')
|
||||
assert response.status_code == 200
|
||||
result = await response.get_data()
|
||||
assert result == b"Hello, world"
|
||||
@@ -83,7 +83,8 @@ SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/cnf/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/cnf/*.toml)\
|
||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/web/*.py)
|
||||
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||
|
||||
# determine destination files
|
||||
|
||||
@@ -10,8 +10,8 @@ configuration:
|
||||
Weitere wechselrichterspezifische Parameter (z.B. Polling Mode) können im
|
||||
Konfigurationsblock gesetzt werden.
|
||||
|
||||
Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` oder `R47` und die der GEN3PLUS
|
||||
Wechselrichter mit `Y17`oder `Y47`!
|
||||
Die Seriennummer der GEN3 Wechselrichter beginnen mit 'R17' oder 'R47' und die der GEN3PLUS
|
||||
Wechselrichter mit 'Y00', 'Y17' oder 'Y47'!
|
||||
|
||||
Siehe Beispielkonfiguration im Dokumentations-Tab
|
||||
batteries:
|
||||
@@ -106,3 +106,4 @@ configuration:
|
||||
network:
|
||||
5005/tcp: listening Port für TSUN GEN3 Wechselrichter
|
||||
10000/tcp: listening Port für TSUN GEN3PLUS Wechselrichter
|
||||
8127/tcp: Port für das TSUN-Proxy Dashboard
|
||||
|
||||
@@ -10,7 +10,7 @@ configuration:
|
||||
in the configuration block.
|
||||
|
||||
The serial numbers of all GEN3 inverters start with `R17` or `R47` and that of the GEN3PLUS
|
||||
inverters with ‘Y17’ or ‘Y47’!
|
||||
inverters with 'Y00', ‘Y17’ or ‘Y47’!
|
||||
|
||||
For reference see example configuration in Documentation Tab
|
||||
|
||||
@@ -107,3 +107,4 @@ configuration:
|
||||
network:
|
||||
5005/tcp: listening Port for TSUN GEN3 Devices
|
||||
10000/tcp: listening Port for TSUN GEN3PLUS Devices
|
||||
8127/tcp: Port for the TSUN-Proxy Dashboard
|
||||
@@ -23,8 +23,11 @@ services:
|
||||
ports:
|
||||
5005/tcp: 5005
|
||||
10000/tcp: 10000
|
||||
|
||||
8127/tcp: 8127
|
||||
webui: "http://[HOST]:[PORT:8127]/"
|
||||
watchdog: "http://[HOST]:[PORT:8127]/-/healthy"
|
||||
ingress: true
|
||||
ingress_port: 8127
|
||||
|
||||
# Definition of parameters in the configuration tab of the addon
|
||||
# parameters are available within the container as /data/options.json
|
||||
@@ -32,7 +35,7 @@ watchdog: "http://[HOST]:[PORT:8127]/-/healthy"
|
||||
|
||||
schema:
|
||||
inverters:
|
||||
- serial: match(^(R17|R47|Y17|Y47).{13}$)
|
||||
- serial: match(^(R17|R47|Y00|Y17|Y47).{13}$)
|
||||
monitor_sn: int?
|
||||
node_id: str
|
||||
suggested_area: str
|
||||
|
||||
Reference in New Issue
Block a user