Compare commits

...

13 Commits

Author SHA1 Message Date
Stefan Allius
bd31dfc15d fix unit test, increase coverage 2025-04-29 00:02:20 +02:00
Stefan Allius
81745313b7 disable french language for now 2025-04-28 23:34:26 +02:00
Stefan Allius
61191f43ba remove debug print 2025-04-28 23:28:17 +02:00
Stefan Allius
b8a39aac55 build relative urls for HA ingress 2025-04-28 23:06:13 +02:00
Stefan Allius
8375679777 translate connection table, fix icon 2025-04-28 21:06:33 +02:00
Stefan Allius
0c981f214d - clean target erases the *.pot
- don't modify the resurt of url_for() calls
- don't translate the language description
2025-04-28 20:45:19 +02:00
Stefan Allius
e51f54381f add translations to docker container 2025-04-27 22:21:47 +02:00
Stefan Allius
8942ad936f set a default key for secure cookies 2025-04-27 17:08:51 +02:00
Stefan Allius
8dbf51df49 increase unit test coverage 2025-04-27 16:55:25 +02:00
Stefan Allius
79c4981edb rename file, since only favicon routes are defined 2025-04-27 12:42:04 +02:00
Stefan Allius
0ef0c210ce move routes into the proper python src file 2025-04-27 12:39:05 +02:00
Stefan Allius
da04e700c7 build a web module for the dashboard
- load all python module from local dir
- initialize Blueprint and Babel
2025-04-27 12:19:33 +02:00
Stefan Allius
5e0aea3364 add button for languages setting 2025-04-27 01:26:34 +02:00
18 changed files with 328 additions and 135 deletions

View File

@@ -1,11 +1,19 @@
.PHONY: build babel clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel check-docker-compose install .PHONY: build babel clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel check-docker-compose install
babel debug dev preview rc rel: babel:
$(MAKE) -C app $@ $(MAKE) -C app $@
clean build: build:
$(MAKE) -C ha_addons $@ $(MAKE) -C ha_addons $@
clean build:
$(MAKE) -C app $@
$(MAKE) -C ha_addons $@
debug dev preview rc rel:
$(MAKE) -C app babel
$(MAKE) -C app $@
addon-dev addon-debug addon-rc addon-rel: addon-dev addon-debug addon-rc addon-rel:
$(MAKE) -C app babel $(MAKE) -C app babel
$(MAKE) -C ha_addons $(patsubst addon-%,%,$@) $(MAKE) -C ha_addons $(patsubst addon-%,%,$@)

View File

@@ -60,6 +60,7 @@ RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* &&
# copy the content of the local src and config directory to the working directory # copy the content of the local src and config directory to the working directory
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
COPY src . COPY src .
COPY translations ./translations
RUN echo ${VERSION} > /proxy-version.txt \ RUN echo ${VERSION} > /proxy-version.txt \
&& date > /build-date.txt && date > /build-date.txt
EXPOSE 5005 8127 10000 EXPOSE 5005 8127 10000

View File

@@ -21,6 +21,8 @@ export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/) PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/) PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
clean:
rm -f $(BABEL_TRANSLATIONS)/*.pot
dev debug: dev debug:
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) @echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
@@ -58,4 +60,4 @@ $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po : $(BABEL_TRANSLATIONS)/messages
$(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.mo : $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.mo : $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po
@pybabel compile -d $(BABEL_TRANSLATIONS) -l $* @pybabel compile -d $(BABEL_TRANSLATIONS) -l $*
.PHONY: babel debug dev preview rc rel .PHONY: babel clean debug dev preview rc rel

View File

@@ -23,7 +23,7 @@ if [ "$user" = '0' ]; then
echo "######################################################" echo "######################################################"
echo "#" echo "#"
exec su-exec $SERVICE_NAME "$@" exec su-exec $SERVICE_NAME "$@" -tr './translations/'
else else
exec "$@" exec "$@"
fi fi

View File

@@ -4,9 +4,7 @@ import logging.handlers
import os import os
import argparse import argparse
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from quart import Quart, Response, request from quart import Quart, Response
from quart_babel import Babel
from quart_babel.locale import get_locale
from logging import config # noqa F401 from logging import config # noqa F401
from proxy import Proxy from proxy import Proxy
from inverter_ifc import InverterIfc from inverter_ifc import InverterIfc
@@ -17,7 +15,9 @@ from cnf.config import Config
from cnf.config_read_env import ConfigReadEnv from cnf.config_read_env import ConfigReadEnv
from cnf.config_read_toml import ConfigReadToml from cnf.config_read_toml import ConfigReadToml
from cnf.config_read_json import ConfigReadJson from cnf.config_read_json import ConfigReadJson
from web.routes import web_routes from web import Web
from web.wrapper import url_for
from modbus_tcp import ModbusTcp from modbus_tcp import ModbusTcp
@@ -33,31 +33,11 @@ class ProxyState:
ProxyState._is_up = value ProxyState._is_up = value
def my_get_locale():
# check how to get the locale form for the add-on - hass.selectedLanguage
# logging.info("get_locale(%s)", request.accept_languages)
return request.accept_languages.best_match(
['de', 'en']
)
def my_get_tz():
return 'CET'
app = Quart(__name__, app = Quart(__name__,
template_folder='web/templates', template_folder='web/templates',
static_folder='web/static') static_folder='web/static')
babel = Babel(app, app.secret_key = 'JKLdks.dajlKKKdladkflKwolafallsdfl'
locale_selector=my_get_locale, app.jinja_env.globals.update(url_for=url_for)
timezone_selector=my_get_tz,
default_translation_directories='../translations')
app.register_blueprint(web_routes)
@app.context_processor
def utility_processor():
return dict(lang=get_locale())
@app.route('/-/ready') @app.route('/-/ready')
@@ -152,6 +132,12 @@ def main(): # pragma: no cover
parser.add_argument('-b', '--log_backups', type=int, parser.add_argument('-b', '--log_backups', type=int,
default=0, default=0,
help='set max number of daily log-files') help='set max number of daily log-files')
parser.add_argument('-tr', '--trans_path', type=str,
default='../translations/',
help='set path for the translations files')
parser.add_argument('-r', '--rel_urls', type=bool,
default=False,
help='use relative dashboard urls')
args = parser.parse_args() args = parser.parse_args()
# #
# Setup our daily, rotating logger # Setup our daily, rotating logger
@@ -170,6 +156,8 @@ def main(): # pragma: no cover
logging.info(f"config_path: {args.config_path}") logging.info(f"config_path: {args.config_path}")
logging.info(f"json_config: {args.json_config}") logging.info(f"json_config: {args.json_config}")
logging.info(f"toml_config: {args.toml_config}") logging.info(f"toml_config: {args.toml_config}")
logging.info(f"trans_path: {args.trans_path}")
logging.info(f"rel_urls: {args.rel_urls}")
logging.info(f"log_path: {args.log_path}") logging.info(f"log_path: {args.log_path}")
if args.log_backups == 0: if args.log_backups == 0:
logging.info("log_backups: unlimited") logging.info("log_backups: unlimited")
@@ -208,6 +196,7 @@ def main(): # pragma: no cover
Proxy.class_init() Proxy.class_init()
Schedule.start() Schedule.start()
ModbusTcp(loop) ModbusTcp(loop)
Web(app, args.trans_path, args.rel_urls)
# #
# 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
@@ -224,7 +213,8 @@ def main(): # pragma: no cover
try: try:
ProxyState.set_up(True) ProxyState.set_up(True)
logging.info("Start Quart") logging.info("Start Quart")
app.run(host='0.0.0.0', port=8127, use_reloader=False, loop=loop) app.run(host='0.0.0.0', port=8127, use_reloader=False, loop=loop,
debug=True,)
logging.info("Quart stopped") logging.info("Quart stopped")
except KeyboardInterrupt: except KeyboardInterrupt:

25
app/src/utils/__init__.py Normal file
View File

@@ -0,0 +1,25 @@
import mimetypes
from importlib import import_module
from pathlib import Path
from collections.abc import Callable
class SourceFileLoader:
""" Represents a SouceFileLoader (__loader__)"""
name: str
get_resource_reader: Callable
def load_modules(loader: SourceFileLoader):
"""Load the entire modules from a SourceFileLoader (__loader__)"""
pkg = loader.name
for load in loader.get_resource_reader().contents():
if "python" not in str(mimetypes.guess_type(load)[0]):
continue
mod = Path(load).stem
if mod == "__init__":
continue
import_module(pkg + "." + mod, pkg)

32
app/src/web/__init__.py Normal file
View File

@@ -0,0 +1,32 @@
'''Quart blueprint for the proxy webserver with the dashboard
Usage:
app = Quart(__name__, ...)
Web(app)
'''
from quart import Quart, Blueprint
from quart_babel import Babel
from utils import load_modules
web = Blueprint('web', __name__)
load_modules(__loader__)
class Web:
'''Helper Class to register the Blueprint at Quart and
initializing Babel'''
def __init__(self,
app: Quart,
translation_directories: str | list[str],
rel_urls: bool):
web.build_relative_urls = rel_urls
app.register_blueprint(web)
from .i18n import get_locale, get_tz
global babel
babel = Babel(
app,
locale_selector=get_locale,
timezone_selector=get_tz,
default_translation_directories=translation_directories)

View File

@@ -1,4 +1,9 @@
from inverter_base import InverterBase from inverter_base import InverterBase
from quart import render_template
from quart_babel import format_datetime, _
from infos import Infos
from . import web
def _get_device_icon(client_mode: bool): def _get_device_icon(client_mode: bool):
@@ -12,7 +17,7 @@ def _get_device_icon(client_mode: bool):
def _get_cloud_icon(emu_mode: bool): def _get_cloud_icon(emu_mode: bool):
'''returns the icon for the cloud conntection''' '''returns the icon for the cloud conntection'''
if emu_mode: if emu_mode:
return 'fa-cloud-arrow-down-alt' return 'fa-cloud-arrow-up-alt'
return 'fa-cloud' return 'fa-cloud'
@@ -49,9 +54,9 @@ def get_table_data():
"w3-hide-small w3-hide-medium", "w3-hide-large", "w3-hide-small w3-hide-medium", "w3-hide-large",
], ],
"thead": [[ "thead": [[
'Device-IP:Port', 'Device-IP', _('Device-IP:Port'), _('Device-IP'),
"Serial-No", _("Serial-No"),
"Cloud-IP:Port", "Cloud-IP" _("Cloud-IP:Port"), _("Cloud-IP")
]], ]],
"tbody": [] "tbody": []
} }
@@ -59,3 +64,19 @@ def get_table_data():
table['tbody'].append(_get_row(inverter)) table['tbody'].append(_get_row(inverter))
return table return table
@web.route('/data-fetch')
async def data_fetch():
data = {
"update-time": format_datetime(format="medium"),
"server-cnt": f"<h3>{Infos.get_counter('ServerMode_Cnt')}</h3>",
"client-cnt": f"<h3>{Infos.get_counter('ClientMode_Cnt')}</h3>",
"proxy-cnt": f"<h3>{Infos.get_counter('ProxyMode_Cnt')}</h3>",
"emulation-cnt": f"<h3>{Infos.get_counter('EmuMode_Cnt')}</h3>",
}
data["conn-table"] = await render_template('conn_table.html.j2',
table=get_table_data())
data["notes-list"] = await render_template('notes_list.html.j2')
return data

37
app/src/web/favicon.py Normal file
View File

@@ -0,0 +1,37 @@
import os
from quart import send_from_directory
from . import web
async def get_icon(file: str, mime: str = 'image/png'):
return await send_from_directory(
os.path.join(web.root_path, 'static/images'),
file,
mimetype=mime)
@web.route('/favicon-96x96.png')
async def favicon():
return await get_icon('favicon-96x96.png')
@web.route('/favicon.ico')
async def favicon_ico():
return await get_icon('favicon.ico', 'image/x-icon')
@web.route('/favicon.svg')
async def favicon_svg():
return await get_icon('favicon.svg', 'image/svg+xml')
@web.route('/apple-touch-icon.png')
async def apple_touch():
return await get_icon('apple-touch-icon.png')
@web.route('/site.webmanifest')
async def webmanifest():
return await get_icon('site.webmanifest', 'application/manifest+json')

42
app/src/web/i18n.py Normal file
View File

@@ -0,0 +1,42 @@
from quart import request, session, redirect, abort
from quart_babel.locale import get_locale as babel_get_locale
from . import web
LANGUAGES = {
'en': 'English',
'de': 'Deutsch',
# 'fr': 'Français'
}
def get_locale():
try:
language = session['language']
except KeyError:
language = None
if language is not None:
return language
# check how to get the locale form for the add-on - hass.selectedLanguage
# logging.info("get_locale(%s)", request.accept_languages)
return request.accept_languages.best_match(LANGUAGES.keys())
def get_tz():
return 'CET'
@web.context_processor
def utility_processor():
return dict(lang=babel_get_locale(),
lang_str=LANGUAGES.get(str(babel_get_locale()), "English"),
languages=LANGUAGES)
@web.route('/language/<language>')
async def set_language(language=None):
if language in LANGUAGES:
session['language'] = language
return redirect('../#')
return abort(404)

16
app/src/web/pages.py Normal file
View File

@@ -0,0 +1,16 @@
from quart import render_template
from .wrapper import url_for
from . import web
@web.route('/')
async def index():
return await render_template(
'index.html.j2',
fetch_url=url_for('web.data_fetch'))
@web.route('/page')
async def empty():
return await render_template('empty.html.j2')

View File

@@ -1,69 +0,0 @@
from quart import Blueprint
from quart import render_template, url_for
from quart import send_from_directory
from quart_babel import format_datetime
from infos import Infos
from web.conn_table import get_table_data
import os
web_routes = Blueprint('web_routes', __name__)
async def get_icon(file: str, mime: str = 'image/png'):
return await send_from_directory(
os.path.join(web_routes.root_path, 'static/images'),
file,
mimetype=mime)
@web_routes.route('/')
async def index():
return await render_template(
'index.html.j2',
fetch_url='.'+url_for('web_routes.data_fetch'))
@web_routes.route('/page')
async def empty():
return await render_template('empty.html.j2')
@web_routes.route('/data-fetch')
async def data_fetch():
data = {
"update-time": format_datetime(format="medium"),
"server-cnt": f"<h3>{Infos.get_counter('ServerMode_Cnt')}</h3>",
"client-cnt": f"<h3>{Infos.get_counter('ClientMode_Cnt')}</h3>",
"proxy-cnt": f"<h3>{Infos.get_counter('ProxyMode_Cnt')}</h3>",
"emulation-cnt": f"<h3>{Infos.get_counter('EmuMode_Cnt')}</h3>",
}
data["conn-table"] = await render_template('conn_table.html.j2',
table=get_table_data())
data["notes-list"] = await render_template('notes_list.html.j2')
return data
@web_routes.route('/favicon-96x96.png')
async def favicon():
return await get_icon('favicon-96x96.png')
@web_routes.route('/favicon.ico')
async def favicon_ico():
return await get_icon('favicon.ico', 'image/x-icon')
@web_routes.route('/favicon.svg')
async def favicon_svg():
return await get_icon('favicon.svg', 'image/svg+xml')
@web_routes.route('/apple-touch-icon.png')
async def apple_touch():
return await get_icon('apple-touch-icon.png')
@web_routes.route('/site.webmanifest')
async def webmanifest():
return await get_icon('site.webmanifest', 'application/manifest+json')

View File

@@ -4,8 +4,8 @@
<title>{% block title %}{% endblock title %}</title> <title>{% block title %}{% endblock title %}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href=".{{ url_for('static', filename= 'css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename= 'css/style.css') }}">
<link rel="stylesheet" href=".{{ url_for('static', filename= 'font-awesome/css/all.min.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename= 'font-awesome/css/all.min.css') }}">
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" /> <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
@@ -14,7 +14,7 @@
<style> <style>
@font-face { @font-face {
font-family: Roboto; font-family: Roboto;
src: url(".{{ url_for('static', filename= 'font/roboto-light.ttf') }}"); src: url("{{ url_for('static', filename= 'font/roboto-light.ttf') }}");
} }
html,body,h1,h2,h3,h4,h5 {font-family: Roboto, sans-serif} html,body,h1,h2,h3,h4,h5 {font-family: Roboto, sans-serif}
</style> </style>
@@ -22,20 +22,28 @@
<body class="w3-light-grey"> <body class="w3-light-grey">
<!-- Top container --> <!-- Top container -->
<div class="w3-bar w3-top w3-dark-grey w3-large" style="z-index:4"> <div class="w3-bar w3-dark-grey w3-large" style="z-index:4">
<button class="w3-bar-item w3-button w3-hide-large w3-hover-none w3-hover-text-light-grey" onclick="w3_open();"><i class="fa fa-bars"></i>  Menu</button> <button class="w3-bar-item w3-button w3-hide-large" onclick="w3_open();"><i class="fa fa-bars"></i>  Menu</button>
<div class="w3-dropdown-hover w3-right">
<button class="w3-button">{{lang_str}}</button>
<div class="w3-dropdown-content w3-bar-block w3-card-4" style="right:0">
{% for language in languages %}
<a href="{{url_for('web.set_language', language=language)}}" class="w3-bar-item w3-button">{{languages[language]}}</a>
{% endfor %}
</div>
</div>
{% if fetch_url is defined %} {% if fetch_url is defined %}
<button class="w3-bar-item w3-button w3-hover-none w3-hover-text-light-grey w3-right" onclick="fetch_data();"><span class="w3-hide-small">{{_('Updated:')}}  </span><span id="update-time"></span>  <i class="fa fa-rotate-right w3-medium"></i></button> <button class="w3-bar-item w3-button w3-right" onclick="fetch_data();"><span class="w3-hide-small">{{_('Updated:')}}  </span><span id="update-time"></span>  <i class="w3-hover fa fa-rotate-right w3-medium"></i></button>
{% else %}
<span class="w3-bar-item w3-right">Logo</span>
{% endif %} {% endif %}
<div class="w3-clear"></div>
</div> </div>
<!-- Sidebar/menu --> <!-- Sidebar/menu -->
<nav class="w3-sidebar w3-collapse w3-white" style="z-index:3;width:250px;" id="mySidebar"><br> <nav class="w3-sidebar w3-collapse w3-white" style="z-index:3;width:250px;" id="mySidebar"><br>
<div class="w3-container w3-row"> <div class="w3-container w3-row">
<div class="w3-col s4"> <div class="w3-col s4">
<img src=".{{ url_for('static', filename= 'images/favicon.svg') }}" alt="" class="w3-circle w3-margin-right" style="width:60px"> <img src="{{url_for('static', filename= 'images/favicon.svg') }}" alt="" class="w3-circle w3-margin-right" style="width:60px">
</div> </div>
<div class="w3-col s8 w3-bar"> <div class="w3-col s8 w3-bar">
<h3>TSUN-Proxy</h3><br> <h3>TSUN-Proxy</h3><br>
@@ -47,9 +55,9 @@
</div> </div>
<div class="w3-bar-block"> <div class="w3-bar-block">
<button href="#" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i>  Close Menu</button> <button href="#" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i>  Close Menu</button>
<a href=".{{ url_for('web_routes.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-network-wired fa-fw"></i>  {{_('Connections')}}</a> <a href="{{ url_for('web.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-network-wired fa-fw"></i>  {{_('Connections')}}</a>
<a href=".{{ url_for('web_routes.empty')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-database fa-fw"></i>  MQTT</a> <a href="{{ url_for('web.empty')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-database fa-fw"></i>  MQTT</a>
<a href=".{{ url_for('web_routes.empty')}}" class="w3-bar-item w3-button w3-padding"><i class="fa fa-file-export fa-fw {% block menu3_class %}{% endblock %}"></i>  Downloads</a> <a href="{{ url_for('web.empty')}}" class="w3-bar-item w3-button w3-padding"><i class="fa fa-file-export fa-fw {% block menu3_class %}{% endblock %}"></i>  Downloads</a>
</div> </div>
</nav> </nav>

26
app/src/web/wrapper.py Normal file
View File

@@ -0,0 +1,26 @@
from quart import url_for as quart_url_for
from . import web
def url_for(*args, **kwargs):
"""Return the url for a specific endpoint.
This wrapper optionally convert into a relative url.
This is most useful in templates and redirects to create a URL
that can be used in the browser.
Arguments:
endpoint: The endpoint to build a url for, if prefixed with
``.`` it targets endpoint's in the current blueprint.
_anchor: Additional anchor text to append (i.e. #text).
_external: Return an absolute url for external (to app) usage.
_method: The method to consider alongside the endpoint.
_scheme: A specific scheme to use.
values: The values to build into the URL, as specified in
the endpoint rule.
"""
url = quart_url_for(*args, **kwargs)
if '/' == url[0] and web.build_relative_urls:
url = '.' + url
return url

View File

@@ -1,13 +1,19 @@
# test_with_pytest.py # test_with_pytest.py
import pytest import pytest
from server import app from server import app
from web import Web, web
from async_stream import AsyncStreamClient from async_stream import AsyncStreamClient
from gen3plus.inverter_g3p import InverterG3P from gen3plus.inverter_g3p import InverterG3P
from test_inverter_g3p import FakeReader, FakeWriter, config_conn from test_inverter_g3p import FakeReader, FakeWriter, config_conn
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
@pytest.fixture(scope="session")
def client():
app.secret_key = 'super secret key'
Web(app, '../transfer', False)
return app.test_client()
@pytest.fixture @pytest.fixture
def create_inverter(config_conn): def create_inverter(config_conn):
_ = config_conn _ = config_conn
@@ -36,58 +42,59 @@ def create_inverter_client(config_conn):
return inv return inv
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_home(): async def test_home(client):
"""Test the home route.""" """Test the home route."""
client = app.test_client()
response = await client.get('/') response = await client.get('/')
assert response.status_code == 200 assert response.status_code == 200
assert response.mimetype == 'text/html' assert response.mimetype == 'text/html'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_page(): async def test_page(client):
"""Test the empty page route.""" """Test the empty page route."""
client = app.test_client()
response = await client.get('/page') response = await client.get('/page')
assert response.status_code == 200 assert response.status_code == 200
assert response.mimetype == 'text/html' assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_rel_page(client):
"""Test the empty page route."""
web.build_relative_urls = True
response = await client.get('/page')
assert response.status_code == 200
assert response.mimetype == 'text/html'
web.build_relative_urls = False
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_favicon96(): async def test_favicon96(client):
"""Test the favicon-96x96.png route.""" """Test the favicon-96x96.png route."""
client = app.test_client()
response = await client.get('/favicon-96x96.png') response = await client.get('/favicon-96x96.png')
assert response.status_code == 200 assert response.status_code == 200
assert response.mimetype == 'image/png' assert response.mimetype == 'image/png'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_favicon(): async def test_favicon(client):
"""Test the favicon.ico route.""" """Test the favicon.ico route."""
client = app.test_client()
response = await client.get('/favicon.ico') response = await client.get('/favicon.ico')
assert response.status_code == 200 assert response.status_code == 200
assert response.mimetype == 'image/x-icon' assert response.mimetype == 'image/x-icon'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_favicon_svg(): async def test_favicon_svg(client):
"""Test the favicon.svg route.""" """Test the favicon.svg route."""
client = app.test_client()
response = await client.get('/favicon.svg') response = await client.get('/favicon.svg')
assert response.status_code == 200 assert response.status_code == 200
assert response.mimetype == 'image/svg+xml' assert response.mimetype == 'image/svg+xml'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_apple_touch_icon(): async def test_apple_touch_icon(client):
"""Test the apple-touch-icon.png route.""" """Test the apple-touch-icon.png route."""
client = app.test_client()
response = await client.get('/apple-touch-icon.png') response = await client.get('/apple-touch-icon.png')
assert response.status_code == 200 assert response.status_code == 200
assert response.mimetype == 'image/png' assert response.mimetype == 'image/png'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_manifest(): async def test_manifest(client):
"""Test the site.webmanifest route.""" """Test the site.webmanifest route."""
client = app.test_client()
response = await client.get('/site.webmanifest') response = await client.get('/site.webmanifest')
assert response.status_code == 200 assert response.status_code == 200
assert response.mimetype == 'application/manifest+json' assert response.mimetype == 'application/manifest+json'
@@ -124,3 +131,29 @@ async def test_data_fetch2(create_inverter_client):
response = await client.get('/data-fetch') response = await client.get('/data-fetch')
assert response.status_code == 200 assert response.status_code == 200
@pytest.mark.asyncio
async def test_language_en(client):
"""Test the language/en route."""
response = await client.get('/language/en')
assert response.status_code == 302
assert response.mimetype == 'text/html'
client.set_cookie('test', key='language', value='de')
response = await client.get('/page')
assert response.status_code == 200
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_language_de(client):
"""Test the language/en route."""
response = await client.get('/language/de')
assert response.status_code == 302
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_language_unknown(client):
"""Test the language/en route."""
response = await client.get('/language/unknonw')
assert response.status_code == 404
assert response.mimetype == 'text/html'

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n" "Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-20 21:21+0200\n" "POT-Creation-Date: 2025-04-28 21:00+0200\n"
"PO-Revision-Date: 2025-04-18 16:24+0200\n" "PO-Revision-Date: 2025-04-18 16:24+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n" "Language: de\n"
@@ -19,11 +19,31 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n" "Generated-By: Babel 2.17.0\n"
#: src/web/templates/base.html.j2:27 #: src/web/conn_table.py:57
msgid "Device-IP:Port"
msgstr "Geräte-IP:Port"
#: src/web/conn_table.py:57
msgid "Device-IP"
msgstr "Geräte-IP"
#: src/web/conn_table.py:58
msgid "Serial-No"
msgstr "Seriennummer"
#: src/web/conn_table.py:59
msgid "Cloud-IP:Port"
msgstr "Cloud-IP:Port"
#: src/web/conn_table.py:59
msgid "Cloud-IP"
msgstr "Cloud-IP"
#: src/web/templates/base.html.j2:37
msgid "Updated:" msgid "Updated:"
msgstr "Aktualisiert:" msgstr "Aktualisiert:"
#: src/web/templates/base.html.j2:46 #: src/web/templates/base.html.j2:58
msgid "Connections" msgid "Connections"
msgstr "Verbindungen" msgstr "Verbindungen"

View File

@@ -87,6 +87,7 @@ SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
$(wildcard $(SRC_PROXY)/cnf/*.toml)\ $(wildcard $(SRC_PROXY)/cnf/*.toml)\
$(wildcard $(SRC_PROXY)/gen3/*.py)\ $(wildcard $(SRC_PROXY)/gen3/*.py)\
$(wildcard $(SRC_PROXY)/gen3plus/*.py)\ $(wildcard $(SRC_PROXY)/gen3plus/*.py)\
$(wildcard $(SRC_PROXY)/utils/*.py)\
$(wildcard $(SRC_PROXY)/web/*.py)\ $(wildcard $(SRC_PROXY)/web/*.py)\
$(wildcard $(SRC_PROXY)/web/templates/*.html.j2)\ $(wildcard $(SRC_PROXY)/web/templates/*.html.j2)\
$(wildcard $(SRC_PROXY)/web/static/css/*.css)\ $(wildcard $(SRC_PROXY)/web/static/css/*.css)\

View File

@@ -30,4 +30,4 @@ cd /home/proxy || exit
export VERSION=$(cat /proxy-version.txt) export VERSION=$(cat /proxy-version.txt)
echo "Start Proxyserver..." echo "Start Proxyserver..."
python3 server.py --json_config=/data/options.json --log_path=/homeassistant/tsun-proxy/logs/ --config_path=/homeassistant/tsun-proxy/ --log_backups=2 python3 server.py --rel_urls=True --json_config=/data/options.json --log_path=/homeassistant/tsun-proxy/logs/ --config_path=/homeassistant/tsun-proxy/ --log_backups=2