Compare commits

...

21 Commits

Author SHA1 Message Date
Stefan Allius
514c8256fb increase test coverage, remove obsolete if statement 2025-04-24 22:53:32 +02:00
Stefan Allius
267e25a071 remove test fake code 2025-04-24 22:40:32 +02:00
Stefan Allius
503ca8719d adapt unit tests 2025-04-24 22:32:57 +02:00
Stefan Allius
739fd29fe9 fix responsiveness of the tiles 2025-04-24 22:32:14 +02:00
Stefan Allius
181804a7c1 add connection table to dashboard 2025-04-24 22:31:14 +02:00
Stefan Allius
7b7fa630b2 build connection table for dashboard 2025-04-24 22:29:04 +02:00
Stefan Allius
bab750f025 store inverters serial number 2025-04-24 22:28:21 +02:00
Stefan Allius
cf138ce396 store inverters serial number for the dashboard 2025-04-24 22:27:32 +02:00
Stefan Allius
46dbbd90ac store client mode for dashboard 2025-04-24 22:26:44 +02:00
Stefan Allius
78d5170dcc remove obsolete menue points 2025-04-22 00:54:04 +02:00
Stefan Allius
577d7100ca prepare conn-table and notes list building 2025-04-22 00:51:50 +02:00
Stefan Allius
60ecc54c1e test proxy connection counter handling 2025-04-21 22:49:50 +02:00
Stefan Allius
c38560eefc change color of counter tiles 2025-04-21 21:27:28 +02:00
Stefan Allius
d0632177a1 change background color ot the top branch
- use dark-grey instead of black to reduce the contrast
2025-04-21 21:25:45 +02:00
Stefan Allius
8b93a1f955 Provide counter values for the dashboard 2025-04-21 21:23:11 +02:00
Stefan Allius
516e0e8ba4 chance Updated field to a real button 2025-04-21 10:43:30 +02:00
Stefan Allius
f1fb43ff67 display time of last update and add reload button 2025-04-21 00:28:47 +02:00
Stefan Allius
3b4a0c37fe design counter on connection board 2025-04-20 20:06:40 +02:00
Stefan Allius
ff5ed1e606 S allius/issue387 (#389)
* add optional java script to fetch data regulary


* change file extension to `html.j2` for templates

* fix route for fetch data

- for running in iframes (e.g. HA ingress) we must
  use relative path in the URLs

* increase test coverage

* remove unused statements

* update favicon.svg
2025-04-20 01:51:04 +02:00
Stefan Allius
cbabbbd820 S allius/issue387 (#388)
* add optional java script to fetch data regullary

* change file extension to `html.j2` for templates

* fix route for fetch data

- for running in iframes (e.g. HA ingress) we must
  use relative path in the URLs

* increase test coverage

* remove unused statements
2025-04-20 00:53:31 +02:00
Stefan Allius
c270edff15 S allius/issue385 (#386)
* ignore translation and log files

* add quart-babel

* build and install translation files

* don't export the web-ui port 8127

- for security reason the user should use the
  HA ingress and not the direkt access to the web
  dashboard

* set 'lang' in html tag
2025-04-19 16:51:06 +02:00
30 changed files with 492 additions and 223 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,12 +1,13 @@
.PHONY: build 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
debug dev preview rc rel: babel debug dev preview rc rel:
$(MAKE) -C app $@ $(MAKE) -C app $@
clean build: clean build:
$(MAKE) -C ha_addons $@ $(MAKE) -C ha_addons $@
addon-dev addon-debug addon-rc addon-rel: addon-dev addon-debug addon-rc addon-rel:
$(MAKE) -C app babel
$(MAKE) -C ha_addons $(patsubst addon-%,%,$@) $(MAKE) -C ha_addons $(patsubst addon-%,%,$@)
check-docker-compose: check-docker-compose:

View File

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

View File

@@ -6,10 +6,16 @@ IMAGE = tsun-gen3-proxy
# Folders # Folders
SRC=. 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
export BUILD_DATE := ${shell date -Iminutes} export BUILD_DATE := ${shell date -Iminutes}
VERSION := $(shell cat $(SRC)/.version) VERSION := $(shell cat $(APP)/.version)
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.) 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/)
@@ -39,5 +45,17 @@ preview rel:
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \ export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
docker buildx bake -f docker-bake.hcl $@ 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
.PHONY: debug dev preview rc rel $(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 debug dev preview rc rel

View File

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

3
app/src/.babel.cfg Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,9 @@ 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 from quart import Quart, Response, request
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
@@ -31,12 +33,33 @@ 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,
locale_selector=my_get_locale,
timezone_selector=my_get_tz,
default_translation_directories='../translations')
app.register_blueprint(web_routes) app.register_blueprint(web_routes)
@app.context_processor
def utility_processor():
return dict(lang=get_locale())
@app.route('/-/ready') @app.route('/-/ready')
async def ready(): async def ready():
if ProxyState.is_up(): if ProxyState.is_up():

61
app/src/web/conn_table.py Normal file
View File

@@ -0,0 +1,61 @@
from inverter_base import InverterBase
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-down-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 = {
"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

View File

@@ -1,6 +1,9 @@
from quart import Blueprint from quart import Blueprint
from quart import render_template from quart import render_template, url_for
from quart import send_from_directory 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 import os
web_routes = Blueprint('web_routes', __name__) web_routes = Blueprint('web_routes', __name__)
@@ -15,12 +18,30 @@ async def get_icon(file: str, mime: str = 'image/png'):
@web_routes.route('/') @web_routes.route('/')
async def index(): async def index():
return await render_template('index.html') return await render_template(
'index.html.j2',
fetch_url='.'+url_for('web_routes.data_fetch'))
@web_routes.route('/page') @web_routes.route('/page')
async def empty(): async def empty():
return await render_template('empty.html') 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') @web_routes.route('/favicon-96x96.png')

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: 151 KiB

After

Width:  |  Height:  |  Size: 729 KiB

View File

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

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="{{lang}}" >
<head> <head>
<title>{% block title %}{% endblock title %}</title> <title>{% block title %}{% endblock title %}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
@@ -22,13 +22,17 @@
<body class="w3-light-grey"> <body class="w3-light-grey">
<!-- Top container --> <!-- Top container -->
<div class="w3-bar w3-top w3-black w3-large" style="z-index:4"> <div class="w3-bar w3-top 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 w3-hover-none w3-hover-text-light-grey" onclick="w3_open();"><i class="fa fa-bars"></i>  Menu</button>
{% 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>
{% else %}
<span class="w3-bar-item w3-right">Logo</span> <span class="w3-bar-item w3-right">Logo</span>
{% endif %}
</div> </div>
<!-- Sidebar/menu --> <!-- Sidebar/menu -->
<nav class="w3-sidebar w3-collapse w3-white w3-animate-left" 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">
@@ -43,15 +47,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-users fa-fw"></i>  Overview</a> <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_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=".{{ 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="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-users fa-fw {% block menu3_class %}{% endblock %}"></i>  Traffic</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="#" 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> </div>
</nav> </nav>
@@ -106,6 +104,36 @@
mySidebar.style.display = "none"; mySidebar.style.display = "none";
overlayBg.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> </script>
{% endblock trailer %} {% endblock trailer %}

View File

@@ -0,0 +1,37 @@
{% 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%}
<h5>Connections</h5>
<table class="w3-table w3-striped w3-bordered w3-border 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>

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %} {% extends 'base.html.j2' %}
{% block title %} TSUN Proxy - View {% endblock title%} {% block title %} TSUN Proxy - View {% endblock title%}
{% block menu2_class %}w3-blue{% endblock %} {% block menu2_class %}w3-blue{% endblock %}

View File

@@ -1,182 +0,0 @@
{% 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

@@ -0,0 +1,57 @@
{% 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-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 class="w3-quarter">
<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 class="w3-quarter">
<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 class="w3-quarter">
<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 class="w3-container" id="notes-list"></div>
<div class="w3-container" id="conn-table"></div>
{% endblock content%}

View File

View File

@@ -8,6 +8,7 @@ from infos import Infos
from inverter_base import InverterBase from inverter_base import InverterBase
from async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr from async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
from messages import Message from messages import Message
from mock import patch, call
from test_modbus_tcp import FakeReader, FakeWriter from test_modbus_tcp import FakeReader, FakeWriter
from test_inverter_base import config_conn, patch_open_connection from test_inverter_base import config_conn, patch_open_connection
@@ -74,6 +75,13 @@ def test_health():
cnt += 1 cnt += 1
assert cnt == 0 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 @pytest.mark.asyncio
async def test_close_cb(): async def test_close_cb():
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
@@ -529,9 +537,10 @@ async def test_forward_runtime_error2():
del ifc del ifc
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_forward_runtime_error3(): async def test_forward_runtime_error3(spy_inc_cnt):
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
remote = StreamPtr(None) remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
@@ -543,13 +552,17 @@ async def test_forward_runtime_error3():
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote) ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
ifc.fwd_add(b'test-forward_msg') ifc.fwd_add(b'test-forward_msg')
await ifc.server_loop() 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 assert cnt == 1
del ifc del ifc
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_forward_resp(): async def test_forward_resp(spy_inc_cnt):
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
remote = StreamPtr(None) remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0 cnt = 0
def _close_cb(): def _close_cb():
@@ -557,27 +570,35 @@ async def test_forward_resp():
cnt += 1 cnt += 1
cnt = 0 cnt = 0
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), remote, _close_cb) ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), remote, _close_cb, use_emu = True)
create_remote(remote, TestType.FWD_NO_EXCPT) create_remote(remote, TestType.FWD_NO_EXCPT)
ifc.fwd_add(b'test-forward_msg') ifc.fwd_add(b'test-forward_msg')
await ifc.client_loop('') 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 assert cnt == 1
del ifc del ifc
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_forward_resp2(): async def test_forward_resp2(spy_inc_cnt):
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
remote = StreamPtr(None) remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0 cnt = 0
def _close_cb(): def _close_cb():
nonlocal cnt nonlocal cnt
cnt += 1 cnt += 1
cnt = 0 cnt = 0
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb) ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb, use_emu = False)
create_remote(remote, TestType.FWD_NO_EXCPT) create_remote(remote, TestType.FWD_NO_EXCPT)
ifc.fwd_add(b'test-forward_msg') ifc.fwd_add(b'test-forward_msg')
await ifc.client_loop('') 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 assert cnt == 1
del ifc del ifc

View File

@@ -2,8 +2,38 @@
import pytest import pytest
from server import app from server import app
from async_stream import AsyncStreamClient
from gen3plus.inverter_g3p import InverterG3P
from test_inverter_g3p import FakeReader, FakeWriter, config_conn
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
@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 @pytest.mark.asyncio
async def test_home(): async def test_home():
@@ -61,3 +91,36 @@ async def test_manifest():
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'
@pytest.mark.asyncio
async def test_data_fetch(create_inverter):
"""Test the healthy 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 healthy route."""
_ = 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 healthy route."""
_ = 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

View File

@@ -0,0 +1,65 @@
# 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-04-20 21:21+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/templates/base.html.j2:27
msgid "Updated:"
msgstr "Aktualisiert:"
#: src/web/templates/base.html.j2:46
msgid "Connections"
msgstr "Verbindungen"
#: src/web/templates/index.html.j2:5
msgid "Proxy Connection Overview"
msgstr "Proxy Verbindungen"
#: src/web/templates/index.html.j2:16
msgid "Server Mode"
msgstr "Server Modus"
#: src/web/templates/index.html.j2:17
msgid "Established from device to proxy"
msgstr "Vom Gerät zum Proxy aufgebaut"
#: src/web/templates/index.html.j2:27
msgid "Client Mode"
msgstr "Client Modus"
#: src/web/templates/index.html.j2:28
msgid "Established from proxy to device"
msgstr "Vom Proxy zum Gerät aufgebaut"
#: src/web/templates/index.html.j2:38
msgid "Proxy Mode"
msgstr "Proxy Modus"
#: src/web/templates/index.html.j2:39
msgid "Forwarding data to cloud"
msgstr "Weiterleitung in die Cloud"
#: src/web/templates/index.html.j2:49
msgid "Emu Mode"
msgstr "Emu Modus"
#: src/web/templates/index.html.j2:50
msgid "Emulation sends data to cloud"
msgstr "Emulation sendet in die Cloud"

View File

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

View File

@@ -23,11 +23,11 @@ services:
ports: ports:
5005/tcp: 5005 5005/tcp: 5005
10000/tcp: 10000 10000/tcp: 10000
8127/tcp: 8127
webui: "http://[HOST]:[PORT:8127]/" webui: "http://[HOST]:[PORT:8127]/"
watchdog: "http://[HOST]:[PORT:8127]/-/healthy" watchdog: "http://[HOST]:[PORT:8127]/-/healthy"
ingress: true ingress: true
ingress_port: 8127 ingress_port: 8127
panel_icon: "mdi:application-cog-outline"
# Definition of parameters in the configuration tab of the addon # Definition of parameters in the configuration tab of the addon
# parameters are available within the container as /data/options.json # parameters are available within the container as /data/options.json