Compare commits

..

16 Commits

Author SHA1 Message Date
Stefan Allius
9acce70317 add unit tests for all favicons 2025-04-18 03:12:02 +02:00
Stefan Allius
e1cc5c3c2c fix sonarqube warnings 2025-04-18 02:49:48 +02:00
Stefan Allius
a2866955e6 add all font and favicons to add-on 2025-04-18 01:47:10 +02:00
Stefan Allius
f422c5ba91 adapt unit tests 2025-04-18 01:29:23 +02:00
Stefan Allius
ae3cf2a68c load fonts locally 2025-04-18 01:28:43 +02:00
Stefan Allius
5cedece1b3 add more favicons formats 2025-04-18 01:27:07 +02:00
Stefan Allius
b26f7ba591 add more favicon formats 2025-04-18 01:26:11 +02:00
Stefan Allius
2ab200e199 add awesome web font 2025-04-18 01:25:18 +02:00
Stefan Allius
bf523c0713 add Roboto font 2025-04-18 01:24:19 +02:00
Stefan Allius
f33209bf8f add favicon.ico 2025-04-17 20:33:17 +02:00
Stefan Allius
9c7b891a60 add favicon.ico 2025-04-17 20:24:20 +02:00
Stefan Allius
4d3166b81f fix unit test for test dashboard 2025-04-17 18:06:17 +02:00
Stefan Allius
e31a5a2f5a add w3.css dashboard 2025-04-17 00:14:48 +02:00
Stefan Allius
5eded48ee8 add test template and stylesheet 2025-04-16 19:42:52 +02:00
Stefan Allius
7425195b8b copy web-file into the add-on container 2025-04-16 19:42:24 +02:00
Stefan Allius
3862abc8e4 configure path to web files for Quart 2025-04-16 19:41:27 +02:00
51 changed files with 374 additions and 1329 deletions

4
.gitignore vendored
View File

@@ -13,7 +13,3 @@ Doku/**
.env
.venv
coverage.xml
*.pot
*.mo
*.log
*.log.*

View File

@@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased]
- Dashboard: add Log-File page
- Dashboard: add Connection page
- add web UI to add-on
- allow `Y00` serial numbers for GEN3PLUS devices

View File

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

View File

@@ -2,6 +2,4 @@ tests/
**/__pycache__
*.pyc
.DS_Store
build.sh
*.pot
*.po
build.sh

View File

@@ -60,7 +60,6 @@ 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 --chmod=0700 entrypoint.sh /root/entrypoint.sh
COPY src .
COPY translations ./translations
RUN echo ${VERSION} > /proxy-version.txt \
&& date > /build-date.txt
EXPOSE 5005 8127 10000

View File

@@ -6,23 +6,15 @@ IMAGE = tsun-gen3-proxy
# Folders
APP=.
SRC=$(APP)/src
# Folders for Babel translation
BABEL_INPUT_JINJA=$(SRC)/web/templates
BABEL_INPUT= $(foreach dir,$(BABEL_INPUT_JINJA),$(wildcard $(dir)/*.html.j2)) \
BABEL_TRANSLATIONS=$(APP)/translations
SRC=.
export BUILD_DATE := ${shell date -Iminutes}
VERSION := $(shell cat $(APP)/.version)
VERSION := $(shell cat $(SRC)/.version)
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
clean:
rm -f $(BABEL_TRANSLATIONS)/*.pot
dev debug:
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
@@ -47,17 +39,5 @@ preview rel:
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
docker buildx bake -f docker-bake.hcl $@
babel: $(BABEL_TRANSLATIONS)/de/LC_MESSAGES/messages.mo $(BABEL_TRANSLATIONS)/de/LC_MESSAGES/messages.po $(BABEL_TRANSLATIONS)/messages.pot
$(BABEL_TRANSLATIONS)/%.pot : $(SRC)/.babel.cfg $(BABEL_INPUT)
@mkdir -p $(@D)
@pybabel extract -F $< --project=$(IMAGE) --version=$(VERSION) -o $@ $(SRC)
$(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po : $(BABEL_TRANSLATIONS)/messages.pot
@mkdir -p $(@D)
@pybabel update --init-missing -i $< -d $(BABEL_TRANSLATIONS) -l $*
$(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.mo : $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po
@pybabel compile -d $(BABEL_TRANSLATIONS) -l $*
.PHONY: babel clean debug dev preview rc rel
.PHONY: debug dev preview rc rel

View File

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

View File

@@ -1,5 +1,4 @@
aiomqtt==2.3.2
schema==0.7.7
aiocron==2.1
quart==0.20
quart-babel==1.0.7
quart==0.20

View File

@@ -1,3 +0,0 @@
[python: **.py]
[jinja2: web/templates/**.html]
[jinja2: web/templates/**.html.j2]

View File

@@ -327,10 +327,8 @@ class AsyncStreamServer(AsyncStream):
logger.info(f'[{self.node_id}:{self.conn_no}] '
f'Accept connection from {self.r_addr}')
Infos.inc_counter('Inverter_Cnt')
Infos.inc_counter('ServerMode_Cnt')
await self.publish_outstanding_mqtt()
await self.loop()
Infos.dec_counter('ServerMode_Cnt')
Infos.dec_counter('Inverter_Cnt')
await self.publish_outstanding_mqtt()
logger.info(f'[{self.node_id}:{self.conn_no}] Server loop stopped for'
@@ -361,11 +359,9 @@ class AsyncStreamServer(AsyncStream):
class AsyncStreamClient(AsyncStream):
def __init__(self, reader: StreamReader, writer: StreamWriter,
rstream: "StreamPtr", close_cb,
use_emu: bool = False) -> None:
rstream: "StreamPtr", close_cb) -> None:
AsyncStream.__init__(self, reader, writer, rstream)
self.close_cb = close_cb
self.emu_mode = use_emu
async def disc(self) -> None:
logging.debug('AsyncStreamClient.disc()')
@@ -380,16 +376,8 @@ class AsyncStreamClient(AsyncStream):
async def client_loop(self, _: str) -> None:
'''Loop for receiving messages from the TSUN cloud (client-side)'''
Infos.inc_counter('Cloud_Conn_Cnt')
if self.emu_mode:
Infos.inc_counter('EmuMode_Cnt')
else:
Infos.inc_counter('ProxyMode_Cnt')
await self.publish_outstanding_mqtt()
await self.loop()
if self.emu_mode:
Infos.dec_counter('EmuMode_Cnt')
else:
Infos.dec_counter('ProxyMode_Cnt')
Infos.dec_counter('Cloud_Conn_Cnt')
await self.publish_outstanding_mqtt()
logger.info(f'[{self.node_id}:{self.conn_no}] '

View File

@@ -162,13 +162,12 @@ class Config():
)
@classmethod
def init(cls, def_reader: ConfigIfc, log_path: str = '') -> None | str:
def init(cls, def_reader: ConfigIfc) -> None | str:
'''Initialise the Proxy-Config
Copy the internal default config file into the config directory
and initialise the Config with the default configuration '''
cls.err = None
cls.log_path = log_path
cls.def_config = {}
try:
# make the default config transparaent by copying it
@@ -248,7 +247,3 @@ here. The default config reader is handled in the Config.init method'''
'''Check if the member is the default value'''
return cls.act_config.get(member) == cls.def_config.get(member)
@classmethod
def get_log_path(cls) -> str:
return cls.log_path

View File

@@ -100,7 +100,7 @@ class Talent(Message):
if serial_no in inverters:
inv = inverters[serial_no]
self._set_config_parms(inv, serial_no)
self._set_config_parms(inv)
self.db.set_pv_module_details(inv)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
else:

View File

@@ -393,7 +393,7 @@ class SolarmanV5(SolarmanBase):
def _set_config_parms(self, inv: dict, serial_no: str = ""):
'''init connection with params from the configuration'''
super()._set_config_parms(inv, serial_no)
super()._set_config_parms(inv)
snr = serial_no[:3]
if '410' == snr:
self.db.set_db_def_value(Register.EQUIPMENT_MODEL,

View File

@@ -838,10 +838,7 @@ class Infos:
def inc_counter(cls, counter: str) -> None:
'''inc proxy statistic counter'''
db_dict = cls.stat['proxy']
try:
db_dict[counter] += 1
except Exception:
db_dict[counter] = 1
db_dict[counter] += 1
cls.new_stat_data['proxy'] = True
@classmethod
@@ -851,15 +848,6 @@ class Infos:
db_dict[counter] -= 1
cls.new_stat_data['proxy'] = True
@classmethod
def get_counter(cls, counter: str) -> int:
'''get proxy statistic counter'''
try:
db_dict = cls.stat['proxy']
return db_dict[counter]
except Exception:
return 0
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
-> Generator[tuple[str, str, str, str], None, None]:
'''Generator function yields json register struct for home-assistant

View File

@@ -28,14 +28,11 @@ class InverterBase(InverterIfc, Proxy):
Proxy.__init__(self)
self._registry.append(weakref.ref(self))
self.addr = writer.get_extra_info('peername')
self.client_mode = client_mode
self.config_id = config_id
if remote_prot_class:
self.prot_class = remote_prot_class
self.use_emulation = True
else:
self.prot_class = prot_class
self.use_emulation = False
self.__ha_restarts = -1
self.remote = StreamPtr(None)
ifc = AsyncStreamServer(reader, writer,
@@ -120,8 +117,7 @@ class InverterBase(InverterIfc, Proxy):
Config.act_config[self.config_id]['enabled'] = False
ifc = AsyncStreamClient(
reader, writer, self.local,
self.__del_remote, self.use_emulation)
reader, writer, self.local, self.__del_remote)
self.remote.ifc = ifc
if hasattr(stream, 'id_str'):

View File

@@ -108,7 +108,6 @@ class Message(ProtocolIfc):
self.header_len = 0
self.data_len = 0
self.unique_id = 0
self.inv_serial = ''
self.sug_area = ''
self.new_data = {}
self.state = State.init
@@ -141,9 +140,8 @@ class Message(ProtocolIfc):
# to our _recv_buffer
return # pragma: no cover
def _set_config_parms(self, inv: dict, inv_serial: str):
def _set_config_parms(self, inv: dict):
'''init connection with params from the configuration'''
self.inv_serial = inv_serial
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']

View File

@@ -28,12 +28,10 @@ class ModbusConn():
logging.info(f'[{stream.node_id}:{stream.conn_no}] '
f'Connected to {self.addr}')
Infos.inc_counter('Inverter_Cnt')
Infos.inc_counter('ClientMode_Cnt')
await self.inverter.local.ifc.publish_outstanding_mqtt()
return self.inverter
async def __aexit__(self, exc_type, exc, tb):
Infos.dec_counter('ClientMode_Cnt')
Infos.dec_counter('Inverter_Cnt')
await self.inverter.local.ifc.publish_outstanding_mqtt()
self.inverter.__exit__(exc_type, exc, tb)

View File

@@ -7,18 +7,13 @@ from modbus import Modbus
from messages import Message
from cnf.config import Config
from singleton import Singleton
from datetime import datetime
logger_mqtt = logging.getLogger('mqtt')
class Mqtt(metaclass=Singleton):
__client: aiomqtt.Client = None
__client = None
__cb_mqtt_is_up = None
ctime = None
published: int = 0
received: int = 0
def __init__(self, cb_mqtt_is_up):
logger_mqtt.debug('MQTT: __init__')
@@ -57,7 +52,6 @@ class Mqtt(metaclass=Singleton):
| int | float | None = None) -> None:
if self.__client:
await self.__client.publish(topic, payload)
self.published += 1
async def __loop(self) -> None:
mqtt = Config.get('mqtt')
@@ -75,9 +69,6 @@ class Mqtt(metaclass=Singleton):
try:
async with self.__client:
logger_mqtt.info('MQTT broker connection established')
self.ctime = datetime.now()
self.published = 0
self.received = 0
if self.__cb_mqtt_is_up:
await self.__cb_mqtt_is_up()
@@ -93,8 +84,6 @@ class Mqtt(metaclass=Singleton):
await self.dispatch_msg(message)
except aiomqtt.MqttError:
self.ctime = None
if Config.is_default('mqtt'):
logger_mqtt.info(
"MQTT is unconfigured; Check your config.toml!")
@@ -112,14 +101,11 @@ class Mqtt(metaclass=Singleton):
return
except Exception:
# self.inc_counter('SW_Exception') # fixme
self.ctime = None
logger_mqtt.error(
f"Exception:\n"
f"{traceback.format_exc()}")
async def dispatch_msg(self, message):
self.received += 1
if message.topic.matches(self.ha_status_topic):
status = message.payload.decode("UTF-8")
logger_mqtt.info('Home-Assistant Status:'

View File

@@ -15,9 +15,7 @@ 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 import Web
from web.wrapper import url_for
from web.routes import web_routes
from modbus_tcp import ModbusTcp
@@ -36,8 +34,7 @@ class ProxyState:
app = Quart(__name__,
template_folder='web/templates',
static_folder='web/static')
app.secret_key = 'JKLdks.dajlKKKdladkflKwolafallsdfl'
app.jinja_env.globals.update(url_for=url_for)
app.register_blueprint(web_routes)
@app.route('/-/ready')
@@ -132,12 +129,6 @@ def main(): # pragma: no cover
parser.add_argument('-b', '--log_backups', type=int,
default=0,
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()
#
# Setup our daily, rotating logger
@@ -145,10 +136,6 @@ def main(): # pragma: no cover
serv_name = os.getenv('SERVICE_NAME', 'proxy')
version = os.getenv('VERSION', 'unknown')
@app.context_processor
def utility_processor():
return dict(version=version)
setattr(logging.handlers, "log_path", args.log_path)
setattr(logging.handlers, "log_backups", args.log_backups)
os.makedirs(args.log_path, exist_ok=True)
@@ -160,8 +147,6 @@ def main(): # pragma: no cover
logging.info(f"config_path: {args.config_path}")
logging.info(f"json_config: {args.json_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}")
if args.log_backups == 0:
logging.info("log_backups: unlimited")
@@ -183,8 +168,7 @@ def main(): # pragma: no cover
asyncio.set_event_loop(loop)
# read config file
Config.init(ConfigReadToml(src_dir + "cnf/default_config.toml"),
log_path=args.log_path)
Config.init(ConfigReadToml(src_dir + "cnf/default_config.toml"))
ConfigReadEnv()
ConfigReadJson(args.config_path + "config.json")
ConfigReadToml(args.config_path + "config.toml")
@@ -201,7 +185,6 @@ def main(): # pragma: no cover
Proxy.class_init()
Schedule.start()
ModbusTcp(loop)
Web(app, args.trans_path, args.rel_urls)
#
# Create tasks for our listening servers. These must be tasks! If we call
@@ -218,8 +201,7 @@ def main(): # pragma: no cover
try:
ProxyState.set_up(True)
logging.info("Start Quart")
app.run(host='0.0.0.0', port=8127, use_reloader=False, loop=loop,
debug=log_level == logging.DEBUG)
app.run(host='0.0.0.0', port=8127, use_reloader=False, loop=loop)
logging.info("Quart stopped")
except KeyboardInterrupt:

View File

@@ -1,25 +0,0 @@
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)

View File

@@ -1,32 +0,0 @@
'''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,83 +0,0 @@
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):
'''returns the icon for the device conntection'''
if client_mode:
return 'fa-download fa-rotate-180'
return 'fa-upload fa-rotate-180'
def _get_cloud_icon(emu_mode: bool):
'''returns the icon for the cloud conntection'''
if emu_mode:
return 'fa-cloud-arrow-up-alt'
return 'fa-cloud'
def _get_row(inv: InverterBase):
'''build one row for the connection table'''
client_mode = inv.client_mode
inv_serial = inv.local.stream.inv_serial
icon1 = _get_device_icon(client_mode)
ip1, port1 = inv.addr
icon2 = ''
ip2 = '--'
port2 = '--'
if inv.remote.ifc:
ip2, port2 = inv.remote.ifc.r_addr
icon2 = _get_cloud_icon(client_mode)
row = []
row.append(f'<i class="fa {icon1}"></i> {ip1}:{port1}')
row.append(f'<i class="fa {icon1}"></i> {ip1}')
row.append(inv_serial)
row.append(f'<i class="fa {icon2}"></i> {ip2}:{port2}')
row.append(f'<i class="fa {icon2}"></i> {ip2}')
return row
def get_table_data():
'''build the connection table'''
table = {
"headline": _('Connections'),
"col_classes": [
"w3-hide-small w3-hide-medium", "w3-hide-large",
"",
"w3-hide-small w3-hide-medium", "w3-hide-large",
],
"thead": [[
_('Device-IP:Port'), _('Device-IP'),
_("Serial-No"),
_("Cloud-IP:Port"), _("Cloud-IP")
]],
"tbody": []
}
for inverter in InverterBase:
table['tbody'].append(_get_row(inverter))
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('templ_table.html.j2',
table=get_table_data())
data["notes-list"] = await render_template('templ_notes_list.html.j2')
return data

View File

@@ -1,45 +0,0 @@
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
rsp = redirect(request.referrer if request.referrer else '../#')
rsp.content_language = language
return rsp
return abort(404)

View File

@@ -1,60 +0,0 @@
from quart import render_template
from quart_babel import format_datetime, format_decimal
from quart.helpers import send_from_directory
from werkzeug.utils import secure_filename
from cnf.config import Config
import os
from . import web
def _get_file(file):
'''build one row for the connection table'''
entry = {}
entry['name'] = file.name
stat = file.stat()
entry['size'] = format_decimal(stat.st_size)
entry['date'] = stat.st_mtime
entry['created'] = format_datetime(stat.st_ctime, format="short")
entry['modified'] = format_datetime(stat.st_mtime, format="short")
return entry
def get_list_data():
'''build the connection table'''
file_list = []
with os.scandir(Config.get_log_path()) as it:
for entry in it:
if entry.is_file():
file_list.append(_get_file(entry))
file_list.sort(key=lambda x: x['date'], reverse=True)
return file_list
@web.route('/file-fetch')
async def file_fetch():
data = {
"update-time": format_datetime(format="medium"),
}
data["file-list"] = await render_template('templ_log_files_list.html.j2',
dir_list=get_list_data())
return data
@web.route('/send-file/<file>')
async def send(file):
return await send_from_directory(
directory=Config.get_log_path(),
file_name=secure_filename(file),
as_attachment=True)
@web.route('/del-file/<file>', methods=['DELETE'])
async def delete(file):
try:
os.remove(Config.get_log_path() + secure_filename(file))
except OSError:
return 'File not found', 404
return '', 204

View File

@@ -1,58 +0,0 @@
from inverter_base import InverterBase
from quart import render_template
from quart_babel import format_datetime, _
from mqtt import Mqtt
from . import web
def _get_row(inv: InverterBase):
'''build one row for the connection table'''
entity_prfx = inv.entity_prfx
inv_serial = inv.local.stream.inv_serial
node_id = inv.local.stream.node_id
sug_area = inv.local.stream.sug_area
row = []
row.append(inv_serial)
row.append(entity_prfx+node_id)
row.append(sug_area)
return row
def get_table_data():
'''build the connection table'''
table = {
"headline": _('MQTT devices'),
"col_classes": [
"",
"",
"",
],
"thead": [[
_("Serial-No"),
_('Node-ID'),
_('HA-Area'),
]],
"tbody": []
}
for inverter in InverterBase:
table['tbody'].append(_get_row(inverter))
return table
@web.route('/mqtt-fetch')
async def mqtt_fetch():
mqtt = Mqtt(None)
ctime = format_datetime(dt=mqtt.ctime, format='short')
data = {
"update-time": format_datetime(format="medium"),
"mqtt-ctime": f"<h3>{ctime}</h3>",
"mqtt-tx": f"<h3>{mqtt.published}</h3>",
"mqtt-rx": f"<h3>{mqtt.received}</h3>",
}
data["mqtt-table"] = await render_template('templ_table.html.j2',
table=get_table_data())
return data

View File

@@ -1,25 +0,0 @@
from quart import render_template
from .wrapper import url_for
from . import web
@web.route('/')
async def index():
return await render_template(
'page_index.html.j2',
fetch_url=url_for('.data_fetch'))
@web.route('/mqtt')
async def mqtt():
return await render_template(
'page_mqtt.html.j2',
fetch_url=url_for('.mqtt_fetch'))
@web.route('/logging')
async def logging():
return await render_template(
'page_logging.html.j2',
fetch_url=url_for('.file_fetch'))

View File

@@ -1,37 +1,48 @@
from quart import Blueprint
from quart import render_template
from quart import send_from_directory
import os
from quart import send_from_directory
from . import web
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.root_path, 'static/images'),
os.path.join(web_routes.root_path, 'static/images'),
file,
mimetype=mime)
@web.route('/favicon-96x96.png')
@web_routes.route('/')
async def index():
return await render_template('index.html')
@web_routes.route('/page')
async def empty():
return await render_template('empty.html')
@web_routes.route('/favicon-96x96.png')
async def favicon():
return await get_icon('favicon-96x96.png')
@web.route('/favicon.ico')
@web_routes.route('/favicon.ico')
async def favicon_ico():
return await get_icon('favicon.ico', 'image/x-icon')
@web.route('/favicon.svg')
@web_routes.route('/favicon.svg')
async def favicon_svg():
return await get_icon('favicon.svg', 'image/svg+xml')
@web.route('/apple-touch-icon.png')
@web_routes.route('/apple-touch-icon.png')
async def apple_touch():
return await get_icon('apple-touch-icon.png')
@web.route('/site.webmanifest')
@web_routes.route('/site.webmanifest')
async def webmanifest():
return await get_icon('site.webmanifest', 'application/manifest+json')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 729 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "TSUN-Proxy",
"short_name": "Proxy",
"short_name": "TsunProxy",
"icons": [
{
"src": "/web-app-manifest-192x192.png",

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{% endblock title %}</title>
<meta charset="UTF-8">
<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= 'font-awesome/css/all.min.css') }}">
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<style>
@font-face {
font-family: Roboto;
src: url(".{{ url_for('static', filename= 'font/roboto-light.ttf') }}");
}
html,body,h1,h2,h3,h4,h5 {font-family: Roboto, sans-serif}
</style>
</head>
<body class="w3-light-grey">
<!-- Top container -->
<div class="w3-bar w3-top w3-black 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>
<span class="w3-bar-item w3-right">Logo</span>
</div>
<!-- Sidebar/menu -->
<nav class="w3-sidebar w3-collapse w3-white w3-animate-left" style="z-index:3;width:250px;" id="mySidebar"><br>
<div class="w3-container w3-row">
<div class="w3-col s4">
<img src=".{{ url_for('static', filename= 'images/favicon.svg') }}" alt="" class="w3-circle w3-margin-right" style="width:60px">
</div>
<div class="w3-col s8 w3-bar">
<h3>TSUN-Proxy</h3><br>
</div>
</div>
<hr>
<div class="w3-container">
<h5>Dashboard</h5>
</div>
<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>
<a href=".{{ url_for('web_routes.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-users fa-fw"></i>  Overview</a>
<a href=".{{ url_for('web_routes.empty')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-eye fa-fw"></i>  Views</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-users fa-fw {% block menu3_class %}{% endblock %}"></i>  Traffic</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-bullseye fa-fw {% block menu4_class %}{% endblock %}"></i>  Geo</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-gem fa-fw {% block menu5_class %}{% endblock %}"></i>  Orders</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-bell fa-fw {% block menu6_class %}{% endblock %}"></i>  News</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-university fa-fw {% block menu7_class %}{% endblock %}"></i>  General</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-history fa-fw {% block menu8_class %}{% endblock %}"></i>  History</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-cog fa-fw {% block menu9_class %}{% endblock %}"></i>  Settings</a><br><br>
</div>
</nav>
<!-- Overlay effect when opening sidebar on small screens -->
<button class="w3-overlay w3-hide-large w3-animate-opacity" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></button>
<!-- !PAGE CONTENT! -->
<div class="w3-main" style="margin-left:250px;margin-top:43px;">
<!-- Header -->
{% block header %}
<header class="w3-container" style="padding-top:22px">
<h5><b>{% block headline %}{% endblock headline %}</b></h5>
</header>
{% endblock header %}
{% block content %} {% endblock content%}
<!-- Footer -->
{% block footer %}
<footer class="w3-container w3-padding-16 w3-light-grey">
<h4>FOOTER</h4>
<p>Powered by <a href="https://www.w3schools.com/w3css/default.asp" target="_blank">w3.css</a></p>
</footer>
{% endblock footer %}
<!-- End page content -->
</div>
{% block trailer %}
<script>
// Get the Sidebar
var mySidebar = document.getElementById("mySidebar");
// Get the DIV with overlay effect
var overlayBg = document.getElementById("myOverlay");
// Toggle between showing and hiding the sidebar, and add overlay effect
function w3_open() {
if (mySidebar.style.display === 'block') {
mySidebar.style.display = 'none';
overlayBg.style.display = "none";
} else {
mySidebar.style.display = 'block';
overlayBg.style.display = "block";
}
}
// Close the sidebar with the close button
function w3_close() {
mySidebar.style.display = "none";
overlayBg.style.display = "none";
}
</script>
{% endblock trailer %}
</body>
</html>

View File

@@ -1,149 +0,0 @@
<!DOCTYPE html>
<html lang="{{lang}}" >
<head>
<title>{% block title %}{% endblock title %}</title>
<meta charset="UTF-8">
<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= 'font-awesome/css/all.min.css') }}">
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<style>
@font-face {
font-family: Roboto;
src: url("{{ url_for('static', filename= 'font/roboto-light.ttf') }}");
}
html,body,h1,h2,h3,h4,h5 {font-family: Roboto, sans-serif}
</style>
</head>
<body class="w3-light-grey">
<!-- Top container -->
<div class="w3-bar w3-dark-grey w3-large" style="z-index:4">
<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 %}
<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>
{% endif %}
<div class="w3-clear"></div>
</div>
<!-- Sidebar/menu -->
<nav class="w3-sidebar w3-collapse w3-white" style="z-index:3;width:250px;" id="mySidebar"><br>
<div class="w3-container w3-cell-row">
<div class="w3-cell w3-cell-middle">
<img src="{{url_for('static', filename= 'images/favicon.svg') }}" alt="" class="w3-circle w3-margin-right" style="width:60px">
</div>
<div class="w3-cell">
<span><b class="w3-xlarge">TSUN-Proxy</b><br>{{_('Version:')}} {{version}}</span>
</div>
</div>
<hr>
<div class="w3-container">
<h5>Dashboard</h5>
</div>
<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>
<a href="{{ url_for('.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('.mqtt')}}" 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('.logging')}}" class="w3-bar-item w3-button w3-padding {% block menu3_class %}{% endblock %}"><i class="fa fa-file-export fa-fw"></i>  {{_('Log Files')}}</a>
</div>
</nav>
<!-- Overlay effect when opening sidebar on small screens -->
<button class="w3-overlay w3-hide-large w3-animate-opacity" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></button>
<!-- !PAGE CONTENT! -->
<div class="w3-main" style="margin-left:250px;margin-top:43px;">
<!-- Header -->
{% block header %}
<header class="w3-container" style="padding-top:22px">
<h5><b>{% block headline %}{% endblock headline %}</b></h5>
</header>
{% endblock header %}
{% block content %} {% endblock content%}
<!-- Footer -->
{% block footer %}
<footer class="w3-container w3-padding-16 w3-light-grey">
<h4>FOOTER</h4>
<p>Powered by <a href="https://www.w3schools.com/w3css/default.asp" target="_blank">w3.css</a></p>
</footer>
{% endblock footer %}
<!-- End page content -->
</div>
{% block trailer %}
<script>
// Get the Sidebar
var mySidebar = document.getElementById("mySidebar");
// Get the DIV with overlay effect
var overlayBg = document.getElementById("myOverlay");
// Toggle between showing and hiding the sidebar, and add overlay effect
function w3_open() {
if (mySidebar.style.display === 'block') {
mySidebar.style.display = 'none';
overlayBg.style.display = "none";
} else {
mySidebar.style.display = 'block';
overlayBg.style.display = "block";
}
}
// Close the sidebar with the close button
function w3_close() {
mySidebar.style.display = "none";
overlayBg.style.display = "none";
}
{% if fetch_url is defined %}
function fetch_data() {
fetch("{{fetch_url}}")
.then(response => response.json())
.then(function (data) {
Object.keys(data).forEach(key => {
//console.log(`${key}: ${data[key]}`);
try {
elm = document.getElementById(key)
elm.innerHTML = data[key]
}
catch(err) {
console.log('error: ' + err + ' (for key: ' + key + ')');
}
});
})
.catch(function (err) {
console.log('error: ' + err);
});
}
window.addEventListener('load', function () {
// Your document is loaded.
var fetchInterval = 5000; // 5 seconds.
fetch_data()
// Invoke the request every 5 seconds.
setInterval(fetch_data, fetchInterval);
});
{% endif %}
</script>
{% endblock trailer %}
</body>
</html>

View File

@@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block title %} TSUN Proxy - View {% endblock title%}
{% block menu2_class %}w3-blue{% endblock %}
{% block content %}
{% endblock content%}
{% block footer %}{% endblock footer %}

View File

@@ -0,0 +1,182 @@
{% extends 'base.html' %}
{% block title %} TSUN Proxy - Dashboard {% endblock title%}
{% block menu1_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-dashboard"></i> My Dashboard{% endblock headline %}
{% block content %}
<div class="w3-row-padding w3-margin-bottom">
<div class="w3-quarter">
<div class="w3-container w3-red w3-padding-16">
<div class="w3-left"><i class="fa fa-comment w3-xxxlarge"></i></div>
<div class="w3-right">
<h3>52</h3>
</div>
<div class="w3-clear"></div>
<h4>Messages</h4>
</div>
</div>
<div class="w3-quarter">
<div class="w3-container w3-blue w3-padding-16">
<div class="w3-left"><i class="fa fa-eye w3-xxxlarge"></i></div>
<div class="w3-right">
<h3>99</h3>
</div>
<div class="w3-clear"></div>
<h4>Views</h4>
</div>
</div>
<div class="w3-quarter">
<div class="w3-container w3-teal w3-padding-16">
<div class="w3-left"><i class="fa fa-share-alt w3-xxxlarge"></i></div>
<div class="w3-right">
<h3>23</h3>
</div>
<div class="w3-clear"></div>
<h4>Shares</h4>
</div>
</div>
<div class="w3-quarter">
<div class="w3-container w3-orange w3-text-white w3-padding-16">
<div class="w3-left"><i class="fa fa-users w3-xxxlarge"></i></div>
<div class="w3-right">
<h3>50</h3>
</div>
<div class="w3-clear"></div>
<h4>Users</h4>
</div>
</div>
</div>
<div class="w3-panel">
<div class="w3-row-padding" style="margin:0 -16px">
<div class="w3-third">
<h5>Regions</h5>
<img src="/w3images/region.jpg" style="width:100%" alt="Google Regional Map">
</div>
<div class="w3-twothird">
<h5>Feeds</h5>
<table class="w3-table w3-striped w3-white">
<tr>
<th class="w3-hide-large"></th>
</tr>
<tr>
<td><i class="fa fa-user w3-text-blue w3-large"></i></td>
<td>New record, over 90 views.</td>
<td><i>10 mins</i></td>
</tr>
<tr>
<td><i class="fa fa-bell w3-text-red w3-large"></i></td>
<td>Database error.</td>
<td><i>15 mins</i></td>
</tr>
<tr>
<td><i class="fa fa-users w3-text-yellow w3-large"></i></td>
<td>New record, over 40 users.</td>
<td><i>17 mins</i></td>
</tr>
<tr>
<td><i class="fa fa-comment w3-text-red w3-large"></i></td>
<td>New comments.</td>
<td><i>25 mins</i></td>
</tr>
<tr>
<td><i class="fa fa-bookmark w3-text-blue w3-large"></i></td>
<td>Check transactions.</td>
<td><i>28 mins</i></td>
</tr>
<tr>
<td><i class="fa fa-laptop w3-text-red w3-large"></i></td>
<td>CPU overload.</td>
<td><i>35 mins</i></td>
</tr>
<tr>
<td><i class="fa fa-share-alt w3-text-green w3-large"></i></td>
<td>New shares.</td>
<td><i>39 mins</i></td>
</tr>
</table>
</div>
</div>
</div>
<hr>
<div class="w3-container">
<h5>General Stats</h5>
<p>New Visitors</p>
<div class="w3-grey">
<div class="w3-container w3-center w3-padding w3-green" style="width:25%">+25%</div>
</div>
<p>New Users</p>
<div class="w3-grey">
<div class="w3-container w3-center w3-padding w3-orange" style="width:50%">50%</div>
</div>
<p>Bounce Rate</p>
<div class="w3-grey">
<div class="w3-container w3-center w3-padding w3-red" style="width:75%">75%</div>
</div>
</div>
<hr>
<div class="w3-container">
<h5>Countries</h5>
<table class="w3-table w3-striped w3-bordered w3-border w3-hoverable w3-white">
<tr>
<th class="w3-hide-large"></th>
</tr>
<tr>
<td>United States</td>
<td>65%</td>
</tr>
<tr>
<td>UK</td>
<td>15.7%</td>
</tr>
<tr>
<td>Russia</td>
<td>5.6%</td>
</tr>
<tr>
<td>Spain</td>
<td>2.1%</td>
</tr>
<tr>
<td>India</td>
<td>1.9%</td>
</tr>
<tr>
<td>France</td>
<td>1.5%</td>
</tr>
</table><br>
<button class="w3-button w3-dark-grey">More Countries  <i class="fa fa-arrow-right"></i></button>
</div>
<br>
<div class="w3-container w3-dark-grey w3-padding-32">
<div class="w3-row">
<div class="w3-container w3-third">
<h5 class="w3-bottombar w3-border-green">Demographic</h5>
<p>Language</p>
<p>Country</p>
<p>City</p>
</div>
<div class="w3-container w3-third">
<h5 class="w3-bottombar w3-border-red">System</h5>
<p>Browser</p>
<p>OS</p>
<p>More</p>
</div>
<div class="w3-container w3-third">
<h5 class="w3-bottombar w3-border-orange">Target</h5>
<p>Users</p>
<p>Active</p>
<p>Geo</p>
<p>Interests</p>
</div>
</div>
</div>
{% endblock content%}

View File

@@ -1,67 +0,0 @@
{% extends 'base.html.j2' %}
{% block title %} TSUN Proxy - Connections {% endblock title%}
{% block menu1_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-network-wired"></i>  {{_('Proxy Connection Overview')}}{% endblock headline %}
{% block content %}
<div class="w3-row-padding w3-margin-bottom">
<div class="w3-quarter">
<div class="w3-card-4">
<div class="w3-container w3-indigo w3-padding-16">
<div class="w3-left"><i class="fa fa-upload w3-xxxlarge fa-rotate-180"></i></div>
<div id = "server-cnt" class="w3-right">
<h3>-</h3>
</div>
<div class="w3-clear"></div>
<h4>{{_('Server Mode')}}</h4>
<div class="w3-hide-small w3-hide-medium" style="min-height:50px">{{_('Established from device to proxy')}}</div>
</div>
</div>
</div>
<div class="w3-quarter">
<div class="w3-card-4">
<div class="w3-container w3-purple w3-padding-16">
<div class="w3-left"><i class="fa fa-download w3-xxxlarge fa-rotate-180"></i></div>
<div id = "client-cnt" class="w3-right">
<h3>-</h3>
</div>
<div class="w3-clear"></div>
<h4>{{_('Client Mode')}}</h4>
<div class="w3-hide-small w3-hide-medium" style="min-height:50px">{{_('Established from proxy to device')}}</div>
</div>
</div>
</div>
<div class="w3-quarter">
<div class="w3-card-4">
<div class="w3-container w3-orange w3-text-white w3-padding-16">
<div class="w3-left"><i class="fa fa-cloud w3-xxxlarge"></i></div>
<div id = "proxy-cnt" class="w3-right">
<h3>-</h3>
</div>
<div class="w3-clear"></div>
<h4>{{_('Proxy Mode')}}</h4>
<div class="w3-hide-small w3-hide-medium" style="min-height:50px">{{_('Forwarding data to cloud')}}</div>
</div>
</div>
</div>
<div class="w3-quarter">
<div class="w3-card-4">
<div class="w3-container w3-teal w3-padding-16">
<div class="w3-left"><i class="fa fa-cloud-arrow-up-alt w3-xxxlarge"></i></div>
<div id = "emulation-cnt" class="w3-right">
<h3>-</h3>
</div>
<div class="w3-clear"></div>
<h4>{{_('Emu Mode')}}</h4>
<div class="w3-hide-small w3-hide-medium" style="min-height:50px">{{_('Emulation sends data to cloud')}}</div>
</div>
</div>
</div>
</div>
<div id="notes-list"></div>
<div id="conn-table"></div>
{% endblock content%}
{% block footer %}{% endblock footer %}

View File

@@ -1,30 +0,0 @@
{% extends 'base.html.j2' %}
{% block title %} TSUN Proxy - Log Files {% endblock title%}
{% block menu3_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-file-export fa-fw"></i>  {{_('Log Files')}}{% endblock headline %}
{% block content %}
<div id="id01" class="w3-modal">
<div class="w3-modal-content" style="width:600px">
<div class="w3-container w3-padding-24">
<h2>{{_("Do you really want to delete the log file")}}:<br><b><span id="id03"></span></b> ?</h2>
<div class="w3-bar">
<button id="id02" class="w3-button w3-red" onclick="deleteFile(); document.getElementById('id01').style.display='none'">{{_('Delete File</button')}}>
<button class="w3-button w3-grey w3-right" onclick="document.getElementById('id01').style.display='none'">{{_('Abort')}}</button>
</div>
</div>
</div>
</div>
<div id="file-list"></div>
<script>
function deleteFile() {
fname = document.getElementById('id02').href;
fetch(fname, {method: 'DELETE'})
.then(fetch_data())
}
</script>
{% endblock content%}
{% block footer %}{% endblock footer %}

View File

@@ -1,51 +0,0 @@
{% extends 'base.html.j2' %}
{% block title %} TSUN Proxy - MQTT Status {% endblock title%}
{% block menu2_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-database"></i>  {{_('MQTT Overview')}}{% endblock headline %}
{% block content %}
<div class="w3-row-padding w3-margin-bottom">
<div class="w3-third">
<div class="w3-card-4">
<div class="w3-container w3-indigo w3-padding-16">
<div class="w3-left"><i class="fa fa-link w3-xxxlarge"></i></div>
<div id = "mqtt-ctime" class="w3-right">
<h3>-</h3>
</div>
<div class="w3-clear"></div>
<h4>{{_('Connection Time')}}</h4>
<div class="w3-hide-small w3-hide-medium" style="min-height:50px">{{_('Time at which the connection was established')}}</div>
</div>
</div>
</div>
<div class="w3-third">
<div class="w3-card-4">
<div class="w3-container w3-purple w3-padding-16">
<div class="w3-left"><i class="fa fa-server w3-xxxlarge"></i></div>
<div id = "mqtt-tx" class="w3-right">
<h3>-</h3>
</div>
<div class="w3-clear"></div>
<h4>{{_('Published Topics')}}</h4>
<div class="w3-hide-small w3-hide-medium" style="min-height:50px">{{_('Number of published topics')}}</div>
</div>
</div>
</div>
<div class="w3-third">
<div class="w3-card-4">
<div class="w3-container w3-orange w3-text-white w3-padding-16">
<div class="w3-left"><i class="fa fa-user w3-xxxlarge"></i></div>
<div id = "mqtt-rx" class="w3-right">
<h3>-</h3>
</div>
<div class="w3-clear"></div>
<h4>{{_('Received Topics')}}</h4>
<div class="w3-hide-small w3-hide-medium" style="min-height:50px">{{_('Number of topics received')}}</div>
</div>
</div>
</div>
</div>
<div id="mqtt-table"></div>
{% endblock content%}
{% block footer %}{% endblock footer %}

View File

@@ -1,33 +0,0 @@
<div class="w3-row-padding w3-margin-bottom">
{% for file in dir_list %}
<div class="w3-quarter w3-margin-bottom">
<div class="w3-card-4">
<header class="w3-container w3-teal" style="min-height:80px">
<h4>{{file.name}}</h4>
</header>
<table class="w3-table">
{% for idx, name in [('created',_('Created')), ('modified', _('Modified')), ('size', _('Size'))]%}
<tr>
<td>{{_(name)}}:</td>
<td>{{file[idx]}}</td>
</tr>
{% endfor %}
</table>
<footer class="w3-teal">
<a href="{{ url_for('.send',file=file.name)}}" class="w3-button w3-hover-teal w3-hover-text-black"><i class="fa fa-file-download"></i>  {{_('Download File')}}</a>
<a class="w3-button w3-right w3-hover-teal w3-hover-text-black"
onclick="document.getElementById('id03').innerHTML='{{file.name}}'; document.getElementById('id02').href='{{ url_for('.delete',file=file.name)}}'; document.getElementById('id01').style.display='block';"><i class="fa fa-trash"></i></a>
</footer>
</div>
</div>
{% if 0 == (loop.index%4) and not last %}
</div>
<div class="w3-row-padding w3-margin-bottom">
{% endif %}
{% endfor %}
</div>

View File

@@ -1,42 +0,0 @@
{% macro table_elm(elm, col_class, tag='td') -%}
{% if elm is mapping %}
<{{tag}}
{% if (col_class|length) > 0 or elm.class is defined%}class="{{col_class}} {{elm.class}}"{% endif %}
{% if elm.colspan is defined %}colspan="{{elm.colspan}}"{% endif %}
{% if elm.rowspan is defined %}rowspan="{{elm.rowspan}}"{% endif %}
>{{elm.val}}</{{tag}}>
{% else %}
<{{tag}}
{% if (col_class|length) > 0 %}class="{{col_class}}"{% endif %}
>{{elm}}</{{tag}}>
{% endif %}
{%- endmacro%}
<div class="w3-container w3-margin-bottom">
<h5>{{table.headline}}</h5>
<div class="w3-card-4">
<table class="w3-table w3-bordered w3-hoverable w3-white">
{% if table.thead is defined%}
<thead>
{% for row in table.thead %}
<tr>
{% for col in row %}
{{table_elm(col, table.col_classes[loop.index0], 'th')}}
{% endfor %}
</tr>
{% endfor %}
</thead>
{% endif %}
<tbody>
{% for row in table.tbody %}
<tr>
{% for col in row %}
{{table_elm(col, table.col_classes[loop.index0])}}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@@ -1,26 +0,0 @@
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,19 +0,0 @@
2025-04-30 00:01:23 INFO | root | Server "proxy - unknown" will be started
2025-04-30 00:01:23 INFO | root | current dir: /Users/sallius/tsun/tsun-gen3-proxy
2025-04-30 00:01:23 INFO | root | config_path: ./config/
2025-04-30 00:01:23 INFO | root | json_config: None
2025-04-30 00:01:23 INFO | root | toml_config: None
2025-04-30 00:01:23 INFO | root | trans_path: ../translations/
2025-04-30 00:01:23 INFO | root | rel_urls: False
2025-04-30 00:01:23 INFO | root | log_path: ./log/
2025-04-30 00:01:23 INFO | root | log_backups: unlimited
2025-04-30 00:01:23 INFO | root | LOG_LVL : None
2025-04-30 00:01:23 INFO | root | ******
2025-04-30 00:01:23 INFO | root | Read from /Users/sallius/tsun/tsun-gen3-proxy/app/src/cnf/default_config.toml => ok
2025-04-30 00:01:23 INFO | root | Read from environment => ok
2025-04-30 00:01:23 INFO | root | Read from ./config/config.json => n/a
2025-04-30 00:01:23 INFO | root | Read from ./config/config.toml => n/a
2025-04-30 00:01:23 INFO | root | ******
2025-04-30 00:01:23 INFO | root | listen on port: 5005 for inverters
2025-04-30 00:01:23 INFO | root | listen on port: 10000 for inverters
2025-04-30 00:01:23 INFO | root | Start Quart

View File

@@ -8,7 +8,6 @@ from infos import Infos
from inverter_base import InverterBase
from async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
from messages import Message
from mock import patch, call
from test_modbus_tcp import FakeReader, FakeWriter
from test_inverter_base import config_conn, patch_open_connection
@@ -75,13 +74,6 @@ def test_health():
cnt += 1
assert cnt == 0
@pytest.fixture
def spy_inc_cnt():
with patch.object(Infos, 'inc_counter', wraps=Infos.inc_counter) as infos:
yield infos
@pytest.mark.asyncio
async def test_close_cb():
assert asyncio.get_running_loop()
@@ -537,10 +529,9 @@ async def test_forward_runtime_error2():
del ifc
@pytest.mark.asyncio
async def test_forward_runtime_error3(spy_inc_cnt):
async def test_forward_runtime_error3():
assert asyncio.get_running_loop()
remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0
async def _create_remote():
@@ -552,17 +543,13 @@ async def test_forward_runtime_error3(spy_inc_cnt):
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
ifc.fwd_add(b'test-forward_msg')
await ifc.server_loop()
spy.assert_has_calls([call('Inverter_Cnt'), call('ServerMode_Cnt')])
assert Infos.get_counter('Inverter_Cnt') == 0
assert Infos.get_counter('ServerMode_Cnt') == 0
assert cnt == 1
del ifc
@pytest.mark.asyncio
async def test_forward_resp(spy_inc_cnt):
async def test_forward_resp():
assert asyncio.get_running_loop()
remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0
def _close_cb():
@@ -570,35 +557,27 @@ async def test_forward_resp(spy_inc_cnt):
cnt += 1
cnt = 0
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), remote, _close_cb, use_emu = True)
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), remote, _close_cb)
create_remote(remote, TestType.FWD_NO_EXCPT)
ifc.fwd_add(b'test-forward_msg')
await ifc.client_loop('')
spy.assert_has_calls([call('Cloud_Conn_Cnt'), call('EmuMode_Cnt')])
assert Infos.get_counter('Cloud_Conn_Cnt') == 0
assert Infos.get_counter('EmuMode_Cnt') == 0
assert cnt == 1
del ifc
@pytest.mark.asyncio
async def test_forward_resp2(spy_inc_cnt):
async def test_forward_resp2():
assert asyncio.get_running_loop()
remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0
def _close_cb():
nonlocal cnt
cnt += 1
cnt = 0
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb, use_emu = False)
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb)
create_remote(remote, TestType.FWD_NO_EXCPT)
ifc.fwd_add(b'test-forward_msg')
await ifc.client_loop('')
spy.assert_has_calls([call('Cloud_Conn_Cnt'), call('ProxyMode_Cnt'), call('SW_Exception')])
assert Infos.get_counter('Cloud_Conn_Cnt') == 0
assert Infos.get_counter('ProxyMode_Cnt') == 0
assert cnt == 1
del ifc

View File

@@ -37,7 +37,6 @@ def config_conn():
},
'solarman':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234}, 'inverters':{'allow_all':True}
}
Config.log_path='app/tests/log/'
@pytest.fixture(scope="module", autouse=True)
def module_init():

View File

@@ -140,7 +140,6 @@ async def test_ha_reconnect(config_mqtt_conn):
assert on_connect.is_set()
finally:
assert m.received == 2
await m.close()
@pytest.mark.asyncio

View File

@@ -1,257 +1,63 @@
# test_with_pytest.py
import pytest
from server import app
from web import Web, web
from async_stream import AsyncStreamClient
from gen3plus.inverter_g3p import InverterG3P
from test_inverter_g3p import FakeReader, FakeWriter, config_conn
from cnf.config import Config
from mock import patch
from proxy import Proxy
import os, errno
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
def create_inverter(config_conn):
_ = config_conn
inv = InverterG3P(FakeReader(), FakeWriter(), client_mode=False)
return inv
@pytest.fixture
def create_inverter_server(config_conn):
_ = config_conn
inv = InverterG3P(FakeReader(), FakeWriter(), client_mode=False)
ifc = AsyncStreamClient(FakeReader(), FakeWriter(), inv.local,
None, inv.use_emulation)
inv.remote.ifc = ifc
return inv
@pytest.fixture
def create_inverter_client(config_conn):
_ = config_conn
inv = InverterG3P(FakeReader(), FakeWriter(), client_mode=True)
ifc = AsyncStreamClient(FakeReader(), FakeWriter(), inv.local,
None, inv.use_emulation)
inv.remote.ifc = ifc
return inv
@pytest.mark.asyncio
async def test_home(client):
async def test_home():
"""Test the home route."""
client = app.test_client()
response = await client.get('/')
assert response.status_code == 200
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_page(client):
"""Test the mqtt page route."""
response = await client.get('/mqtt')
async def test_page():
"""Test the empty page route."""
client = app.test_client()
response = await client.get('/page')
assert response.status_code == 200
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_rel_page(client):
"""Test the mqtt route."""
web.build_relative_urls = True
response = await client.get('/mqtt')
assert response.status_code == 200
assert response.mimetype == 'text/html'
web.build_relative_urls = False
@pytest.mark.asyncio
async def test_logging(client):
"""Test the logging page route."""
response = await client.get('/logging')
assert response.status_code == 200
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_favicon96(client):
async def test_favicon96():
"""Test the favicon-96x96.png route."""
client = app.test_client()
response = await client.get('/favicon-96x96.png')
assert response.status_code == 200
assert response.mimetype == 'image/png'
@pytest.mark.asyncio
async def test_favicon(client):
async def test_favicon():
"""Test the favicon.ico route."""
client = app.test_client()
response = await client.get('/favicon.ico')
assert response.status_code == 200
assert response.mimetype == 'image/x-icon'
@pytest.mark.asyncio
async def test_favicon_svg(client):
async def test_favicon_svg():
"""Test the favicon.svg route."""
client = app.test_client()
response = await client.get('/favicon.svg')
assert response.status_code == 200
assert response.mimetype == 'image/svg+xml'
@pytest.mark.asyncio
async def test_apple_touch_icon(client):
async def test_apple_touch_icon():
"""Test the apple-touch-icon.png route."""
client = app.test_client()
response = await client.get('/apple-touch-icon.png')
assert response.status_code == 200
assert response.mimetype == 'image/png'
@pytest.mark.asyncio
async def test_manifest(client):
async def test_manifest():
"""Test the site.webmanifest route."""
client = app.test_client()
response = await client.get('/site.webmanifest')
assert response.status_code == 200
assert response.mimetype == 'application/manifest+json'
@pytest.mark.asyncio
async def test_data_fetch(create_inverter):
"""Test the data-fetch route."""
_ = create_inverter
client = app.test_client()
response = await client.get('/data-fetch')
assert response.status_code == 200
response = await client.get('/data-fetch')
assert response.status_code == 200
@pytest.mark.asyncio
async def test_data_fetch1(create_inverter_server):
"""Test the data-fetch route with server connection."""
_ = create_inverter_server
client = app.test_client()
response = await client.get('/data-fetch')
assert response.status_code == 200
response = await client.get('/data-fetch')
assert response.status_code == 200
@pytest.mark.asyncio
async def test_data_fetch2(create_inverter_client):
"""Test the data-fetch route with client connection."""
_ = create_inverter_client
client = app.test_client()
response = await client.get('/data-fetch')
assert response.status_code == 200
response = await client.get('/data-fetch')
assert response.status_code == 200
@pytest.mark.asyncio
async def test_language_en(client):
"""Test the language/en route and cookie."""
response = await client.get('/language/en', headers={'referer': '/index'})
assert response.status_code == 302
assert response.content_language.pop() == 'en'
assert response.location == '/index'
assert response.mimetype == 'text/html'
client.set_cookie('test', key='language', value='de')
response = await client.get('/mqtt')
assert response.status_code == 200
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_language_de(client):
"""Test the language/de route."""
response = await client.get('/language/de', headers={'referer': '/'})
assert response.status_code == 302
assert response.content_language.pop() == 'de'
assert response.location == '/'
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_language_unknown(client):
"""Test the language/unknown route."""
response = await client.get('/language/unknown')
assert response.status_code == 404
assert response.mimetype == 'text/html'
@pytest.mark.asyncio
async def test_mqtt_fetch(client, create_inverter):
"""Test the mqtt-fetch route."""
_ = create_inverter
Proxy.class_init()
response = await client.get('/mqtt-fetch')
assert response.status_code == 200
@pytest.mark.asyncio
async def test_file_fetch(client, config_conn):
"""Test the data-fetch route."""
_ = config_conn
assert Config.log_path == 'app/tests/log/'
response = await client.get('/file-fetch')
assert response.status_code == 200
@pytest.mark.asyncio
async def test_send_file(client, config_conn):
"""Test the send-file route."""
_ = config_conn
assert Config.log_path == 'app/tests/log/'
response = await client.get('/send-file/test.txt')
assert response.status_code == 200
@pytest.mark.asyncio
async def test_missing_send_file(client, config_conn):
"""Test the send-file route (file not found)."""
_ = config_conn
assert Config.log_path == 'app/tests/log/'
response = await client.get('/send-file/no_file.log')
assert response.status_code == 404
@pytest.mark.asyncio
async def test_invalid_send_file(client, config_conn):
"""Test the send-file route (invalid filename)."""
_ = config_conn
assert Config.log_path == 'app/tests/log/'
response = await client.get('/send-file/../test_web_route.py')
assert response.status_code == 404
@pytest.fixture
def patch_os_remove_err():
def new_remove(file_path: str):
raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), file_path)
with patch.object(os, 'remove', new_remove) as wrapped_os:
yield wrapped_os
@pytest.fixture
def patch_os_remove_ok():
def new_remove(file_path: str):
return
with patch.object(os, 'remove', new_remove) as wrapped_os:
yield wrapped_os
@pytest.mark.asyncio
async def test_del_file_ok(client, config_conn, patch_os_remove_ok):
"""Test the del-file route with no error."""
_ = config_conn
_ = patch_os_remove_ok
assert Config.log_path == 'app/tests/log/'
response = await client.delete ('/del-file/test.txt')
assert response.status_code == 204
@pytest.mark.asyncio
async def test_del_file_err(client, config_conn, patch_os_remove_err):
"""Test the send-file route with OSError."""
_ = config_conn
_ = patch_os_remove_err
assert Config.log_path == 'app/tests/log/'
response = await client.delete ('/del-file/test.txt')
assert response.status_code == 404

View File

@@ -1,182 +0,0 @@
# German translations for tsun-gen3-proxy.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the tsun-gen3-proxy
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-05-03 21:59+0200\n"
"PO-Revision-Date: 2025-04-18 16:24+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
"Language-Team: de <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: src/web/conn_table.py:51 src/web/templates/base.html.j2:58
msgid "Connections"
msgstr "Verbindungen"
#: src/web/conn_table.py:58
msgid "Device-IP:Port"
msgstr "Geräte-IP:Port"
#: src/web/conn_table.py:58
msgid "Device-IP"
msgstr "Geräte-IP"
#: src/web/conn_table.py:59 src/web/mqtt_table.py:33
msgid "Serial-No"
msgstr "Seriennummer"
#: src/web/conn_table.py:60
msgid "Cloud-IP:Port"
msgstr "Cloud-IP:Port"
#: src/web/conn_table.py:60
msgid "Cloud-IP"
msgstr "Cloud-IP"
#: src/web/mqtt_table.py:26
msgid "MQTT devices"
msgstr "MQTT Geräte"
#: src/web/mqtt_table.py:34
msgid "Node-ID"
msgstr ""
#: src/web/mqtt_table.py:35
msgid "HA-Area"
msgstr ""
#: src/web/templates/base.html.j2:37
msgid "Updated:"
msgstr "Aktualisiert:"
#: src/web/templates/base.html.j2:49
msgid "Version:"
msgstr ""
#: src/web/templates/base.html.j2:60 src/web/templates/page_logging.html.j2:5
msgid "Log Files"
msgstr "Log Dateien"
#: src/web/templates/page_index.html.j2:5
msgid "Proxy Connection Overview"
msgstr "Proxy Verbindungen"
#: src/web/templates/page_index.html.j2:17
msgid "Server Mode"
msgstr "Server Modus"
#: src/web/templates/page_index.html.j2:18
msgid "Established from device to proxy"
msgstr "Vom Gerät zum Proxy aufgebaut"
#: src/web/templates/page_index.html.j2:30
msgid "Client Mode"
msgstr "Client Modus"
#: src/web/templates/page_index.html.j2:31
msgid "Established from proxy to device"
msgstr "Vom Proxy zum Gerät aufgebaut"
#: src/web/templates/page_index.html.j2:43
msgid "Proxy Mode"
msgstr "Proxy Modus"
#: src/web/templates/page_index.html.j2:44
msgid "Forwarding data to cloud"
msgstr "Weiterleitung in die Cloud"
#: src/web/templates/page_index.html.j2:56
msgid "Emu Mode"
msgstr "Emu Modus"
#: src/web/templates/page_index.html.j2:57
msgid "Emulation sends data to cloud"
msgstr "Emulation sendet in die Cloud"
#: src/web/templates/page_logging.html.j2:10
msgid "Do you really want to delete the log file"
msgstr "Soll die Datei wirklich gelöscht werden"
#: src/web/templates/page_logging.html.j2:12
msgid "Delete File</button"
msgstr "File löschen"
#: src/web/templates/page_logging.html.j2:13
msgid "Abort"
msgstr "Abbruch"
#: src/web/templates/page_mqtt.html.j2:5
msgid "MQTT Overview"
msgstr "MQTT Überblick"
#: src/web/templates/page_mqtt.html.j2:16
msgid "Connection Time"
msgstr "Verbindungszeit"
#: src/web/templates/page_mqtt.html.j2:17
msgid "Time at which the connection was established"
msgstr "Zeitpunkt des Verbindungsaufbaus"
#: src/web/templates/page_mqtt.html.j2:29
msgid "Published Topics"
msgstr "Gesendete Topics"
#: src/web/templates/page_mqtt.html.j2:30
msgid "Number of published topics"
msgstr "Anzahl der veröffentlichten Topics"
#: src/web/templates/page_mqtt.html.j2:42
msgid "Received Topics"
msgstr "Empfangene Topics"
#: src/web/templates/page_mqtt.html.j2:43
msgid "Number of topics received"
msgstr "Anzahl der empfangenen Topics"
#: src/web/templates/templ_log_files_list.html.j2:11
msgid "Created"
msgstr "Erzeugt"
#: src/web/templates/templ_log_files_list.html.j2:11
msgid "Modified"
msgstr "Modifiziert"
#: src/web/templates/templ_log_files_list.html.j2:11
msgid "Size"
msgstr "Größe"
#: src/web/templates/templ_log_files_list.html.j2:20
msgid "Download File"
msgstr "Datei Download"
#~ msgid "MQTT Server"
#~ msgstr ""
#~ msgid "MQTT User"
#~ msgstr ""
#~ msgid "MQTT Connected"
#~ msgstr ""
#~ msgid "Home Assistant Status"
#~ msgstr ""
#~ msgid "MQTT Publish Count"
#~ msgstr ""
#~ msgid "MQTT Reveiced Count"
#~ msgstr ""
#~ msgid "MQTT Connect Time"
#~ msgstr "MQTT Verbindungszeit"

View File

@@ -12,14 +12,11 @@ IMAGE = tsun-gen3-addon
SRC=../app
SRC_PROXY=$(SRC)/src
CNF_PROXY=$(SRC)/config
TRANSLATION_PROXY=$(SRC)/translations
# Target folders for building the local add-on and the docker container
ADDON_PATH = ha_addon
DST=$(ADDON_PATH)/rootfs
DST_PROXY=$(DST)/home/proxy
DST_TRANSLATION=$(DST)/home/translations
# base director of the add-on repro for installing the add-on git repros
INST_BASE=../../ha-addons
@@ -87,23 +84,20 @@ SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
$(wildcard $(SRC_PROXY)/cnf/*.toml)\
$(wildcard $(SRC_PROXY)/gen3/*.py)\
$(wildcard $(SRC_PROXY)/gen3plus/*.py)\
$(wildcard $(SRC_PROXY)/utils/*.py)\
$(wildcard $(SRC_PROXY)/web/*.py)\
$(wildcard $(SRC_PROXY)/web/templates/*.html.j2)\
$(wildcard $(SRC_PROXY)/web/templates/*.html)\
$(wildcard $(SRC_PROXY)/web/static/css/*.css)\
$(wildcard $(SRC_PROXY)/web/static/font/*)\
$(wildcard $(SRC_PROXY)/web/static/font-awesome/*/*)\
$(wildcard $(SRC_PROXY)/web/static/images/*)
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
MO_FILES := $(wildcard $(TRANSLATION_PROXY)/de/LC_MESSAGES/*.mo)
# determine destination files
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
TRANSLATION_FILES = $(MO_FILES:$(TRANSLATION_PROXY)/%=$(DST_TRANSLATION)/%)
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(TRANSLATION_FILES) $(DST)/requirements.txt
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
@echo Copy $< to $@
@@ -115,11 +109,6 @@ $(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
@mkdir -p $(@D)
@cp $< $@
$(TRANSLATION_FILES): $(DST_TRANSLATION)/% : $(TRANSLATION_PROXY)/%
@echo Copy $< to $@
@mkdir -p $(@D)
@cp $< $@
$(DST)/requirements.txt : $(SRC)/requirements.txt
@echo Copy $< to $@
@cp $< $@

View File

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

View File

@@ -23,11 +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
panel_icon: "mdi:application-cog-outline"
# Definition of parameters in the configuration tab of the addon
# parameters are available within the container as /data/options.json