Compare commits
16 Commits
s-allius/i
...
s-allius/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0563200ce | ||
|
|
94f3b67cb2 | ||
|
|
a2da9d255f | ||
|
|
27ae3132c2 | ||
|
|
e74f9e0848 | ||
|
|
bb4640a623 | ||
|
|
78a7a7577d | ||
|
|
d7c003cb15 | ||
|
|
8db0f18c05 | ||
|
|
f3c4fdb093 | ||
|
|
59e8508f4c | ||
|
|
24950244c6 | ||
|
|
36dfdd3d41 | ||
|
|
035f96461a | ||
|
|
7542c112f7 | ||
|
|
093ec03d60 |
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [unreleased]
|
## [unreleased]
|
||||||
|
|
||||||
|
- Dashboard: add Log-File page
|
||||||
|
- Dashboard: add Connection page
|
||||||
- add web UI to add-on
|
- add web UI to add-on
|
||||||
- allow `Y00` serial numbers for GEN3PLUS devices
|
- allow `Y00` serial numbers for GEN3PLUS devices
|
||||||
|
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -1,11 +1,19 @@
|
|||||||
.PHONY: build babel clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel check-docker-compose install
|
.PHONY: build babel clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel check-docker-compose install
|
||||||
|
|
||||||
babel debug dev preview rc rel:
|
babel:
|
||||||
$(MAKE) -C app $@
|
$(MAKE) -C app $@
|
||||||
|
|
||||||
clean build:
|
build:
|
||||||
$(MAKE) -C ha_addons $@
|
$(MAKE) -C ha_addons $@
|
||||||
|
|
||||||
|
clean build:
|
||||||
|
$(MAKE) -C app $@
|
||||||
|
$(MAKE) -C ha_addons $@
|
||||||
|
|
||||||
|
debug dev preview rc rel:
|
||||||
|
$(MAKE) -C app babel
|
||||||
|
$(MAKE) -C app $@
|
||||||
|
|
||||||
addon-dev addon-debug addon-rc addon-rel:
|
addon-dev addon-debug addon-rc addon-rel:
|
||||||
$(MAKE) -C app babel
|
$(MAKE) -C app babel
|
||||||
$(MAKE) -C ha_addons $(patsubst addon-%,%,$@)
|
$(MAKE) -C ha_addons $(patsubst addon-%,%,$@)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* &&
|
|||||||
# copy the content of the local src and config directory to the working directory
|
# copy the content of the local src and config directory to the working directory
|
||||||
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
||||||
COPY src .
|
COPY src .
|
||||||
|
COPY translations ./translations
|
||||||
RUN echo ${VERSION} > /proxy-version.txt \
|
RUN echo ${VERSION} > /proxy-version.txt \
|
||||||
&& date > /build-date.txt
|
&& date > /build-date.txt
|
||||||
EXPOSE 5005 8127 10000
|
EXPOSE 5005 8127 10000
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
|
|||||||
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
||||||
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BABEL_TRANSLATIONS)/*.pot
|
||||||
|
|
||||||
dev debug:
|
dev debug:
|
||||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
||||||
@@ -58,4 +60,4 @@ $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po : $(BABEL_TRANSLATIONS)/messages
|
|||||||
$(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.mo : $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po
|
$(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.mo : $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po
|
||||||
@pybabel compile -d $(BABEL_TRANSLATIONS) -l $*
|
@pybabel compile -d $(BABEL_TRANSLATIONS) -l $*
|
||||||
|
|
||||||
.PHONY: babel debug dev preview rc rel
|
.PHONY: babel clean debug dev preview rc rel
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ if [ "$user" = '0' ]; then
|
|||||||
echo "######################################################"
|
echo "######################################################"
|
||||||
echo "#"
|
echo "#"
|
||||||
|
|
||||||
exec su-exec $SERVICE_NAME "$@"
|
exec su-exec $SERVICE_NAME "$@" -tr './translations/'
|
||||||
else
|
else
|
||||||
exec "$@"
|
exec "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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}] '
|
||||||
|
|||||||
@@ -162,12 +162,13 @@ class Config():
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def init(cls, def_reader: ConfigIfc) -> None | str:
|
def init(cls, def_reader: ConfigIfc, log_path: str = '') -> None | str:
|
||||||
'''Initialise the Proxy-Config
|
'''Initialise the Proxy-Config
|
||||||
|
|
||||||
Copy the internal default config file into the config directory
|
Copy the internal default config file into the config directory
|
||||||
and initialise the Config with the default configuration '''
|
and initialise the Config with the default configuration '''
|
||||||
cls.err = None
|
cls.err = None
|
||||||
|
cls.log_path = log_path
|
||||||
cls.def_config = {}
|
cls.def_config = {}
|
||||||
try:
|
try:
|
||||||
# make the default config transparaent by copying it
|
# make the default config transparaent by copying it
|
||||||
@@ -247,3 +248,7 @@ here. The default config reader is handled in the Config.init method'''
|
|||||||
'''Check if the member is the default value'''
|
'''Check if the member is the default value'''
|
||||||
|
|
||||||
return cls.act_config.get(member) == cls.def_config.get(member)
|
return cls.act_config.get(member) == cls.def_config.get(member)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_log_path(cls) -> str:
|
||||||
|
return cls.log_path
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'):
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import logging.handlers
|
|||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
from quart import Quart, Response, request
|
from quart import Quart, Response
|
||||||
from quart_babel import Babel
|
|
||||||
from quart_babel.locale import get_locale
|
|
||||||
from logging import config # noqa F401
|
from logging import config # noqa F401
|
||||||
from proxy import Proxy
|
from proxy import Proxy
|
||||||
from inverter_ifc import InverterIfc
|
from inverter_ifc import InverterIfc
|
||||||
@@ -17,7 +15,9 @@ from cnf.config import Config
|
|||||||
from cnf.config_read_env import ConfigReadEnv
|
from cnf.config_read_env import ConfigReadEnv
|
||||||
from cnf.config_read_toml import ConfigReadToml
|
from cnf.config_read_toml import ConfigReadToml
|
||||||
from cnf.config_read_json import ConfigReadJson
|
from cnf.config_read_json import ConfigReadJson
|
||||||
from web.routes import web_routes
|
from web import Web
|
||||||
|
from web.wrapper import url_for
|
||||||
|
|
||||||
from modbus_tcp import ModbusTcp
|
from modbus_tcp import ModbusTcp
|
||||||
|
|
||||||
|
|
||||||
@@ -33,26 +33,11 @@ class ProxyState:
|
|||||||
ProxyState._is_up = value
|
ProxyState._is_up = value
|
||||||
|
|
||||||
|
|
||||||
def my_get_locale():
|
|
||||||
# check how to get the locale form for the add-on - hass.selectedLanguage
|
|
||||||
# logging.info("get_locale(%s)", request.accept_languages)
|
|
||||||
return request.accept_languages.best_match(
|
|
||||||
['de', 'en']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app = Quart(__name__,
|
app = Quart(__name__,
|
||||||
template_folder='web/templates',
|
template_folder='web/templates',
|
||||||
static_folder='web/static')
|
static_folder='web/static')
|
||||||
babel = Babel(app,
|
app.secret_key = 'JKLdks.dajlKKKdladkflKwolafallsdfl'
|
||||||
locale_selector=my_get_locale,
|
app.jinja_env.globals.update(url_for=url_for)
|
||||||
default_translation_directories='../translations')
|
|
||||||
app.register_blueprint(web_routes)
|
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
|
||||||
def utility_processor():
|
|
||||||
return dict(lang=get_locale())
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/-/ready')
|
@app.route('/-/ready')
|
||||||
@@ -147,6 +132,12 @@ def main(): # pragma: no cover
|
|||||||
parser.add_argument('-b', '--log_backups', type=int,
|
parser.add_argument('-b', '--log_backups', type=int,
|
||||||
default=0,
|
default=0,
|
||||||
help='set max number of daily log-files')
|
help='set max number of daily log-files')
|
||||||
|
parser.add_argument('-tr', '--trans_path', type=str,
|
||||||
|
default='../translations/',
|
||||||
|
help='set path for the translations files')
|
||||||
|
parser.add_argument('-r', '--rel_urls', type=bool,
|
||||||
|
default=False,
|
||||||
|
help='use relative dashboard urls')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
#
|
#
|
||||||
# Setup our daily, rotating logger
|
# Setup our daily, rotating logger
|
||||||
@@ -165,6 +156,8 @@ def main(): # pragma: no cover
|
|||||||
logging.info(f"config_path: {args.config_path}")
|
logging.info(f"config_path: {args.config_path}")
|
||||||
logging.info(f"json_config: {args.json_config}")
|
logging.info(f"json_config: {args.json_config}")
|
||||||
logging.info(f"toml_config: {args.toml_config}")
|
logging.info(f"toml_config: {args.toml_config}")
|
||||||
|
logging.info(f"trans_path: {args.trans_path}")
|
||||||
|
logging.info(f"rel_urls: {args.rel_urls}")
|
||||||
logging.info(f"log_path: {args.log_path}")
|
logging.info(f"log_path: {args.log_path}")
|
||||||
if args.log_backups == 0:
|
if args.log_backups == 0:
|
||||||
logging.info("log_backups: unlimited")
|
logging.info("log_backups: unlimited")
|
||||||
@@ -186,7 +179,8 @@ def main(): # pragma: no cover
|
|||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
# read config file
|
# read config file
|
||||||
Config.init(ConfigReadToml(src_dir + "cnf/default_config.toml"))
|
Config.init(ConfigReadToml(src_dir + "cnf/default_config.toml"),
|
||||||
|
log_path=args.log_path)
|
||||||
ConfigReadEnv()
|
ConfigReadEnv()
|
||||||
ConfigReadJson(args.config_path + "config.json")
|
ConfigReadJson(args.config_path + "config.json")
|
||||||
ConfigReadToml(args.config_path + "config.toml")
|
ConfigReadToml(args.config_path + "config.toml")
|
||||||
@@ -203,6 +197,7 @@ def main(): # pragma: no cover
|
|||||||
Proxy.class_init()
|
Proxy.class_init()
|
||||||
Schedule.start()
|
Schedule.start()
|
||||||
ModbusTcp(loop)
|
ModbusTcp(loop)
|
||||||
|
Web(app, args.trans_path, args.rel_urls)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Create tasks for our listening servers. These must be tasks! If we call
|
# Create tasks for our listening servers. These must be tasks! If we call
|
||||||
@@ -219,7 +214,8 @@ def main(): # pragma: no cover
|
|||||||
try:
|
try:
|
||||||
ProxyState.set_up(True)
|
ProxyState.set_up(True)
|
||||||
logging.info("Start Quart")
|
logging.info("Start Quart")
|
||||||
app.run(host='0.0.0.0', port=8127, use_reloader=False, loop=loop)
|
app.run(host='0.0.0.0', port=8127, use_reloader=False, loop=loop,
|
||||||
|
debug=log_level == logging.DEBUG)
|
||||||
logging.info("Quart stopped")
|
logging.info("Quart stopped")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
25
app/src/utils/__init__.py
Normal file
25
app/src/utils/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import mimetypes
|
||||||
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
|
class SourceFileLoader:
|
||||||
|
""" Represents a SouceFileLoader (__loader__)"""
|
||||||
|
name: str
|
||||||
|
get_resource_reader: Callable
|
||||||
|
|
||||||
|
|
||||||
|
def load_modules(loader: SourceFileLoader):
|
||||||
|
"""Load the entire modules from a SourceFileLoader (__loader__)"""
|
||||||
|
pkg = loader.name
|
||||||
|
for load in loader.get_resource_reader().contents():
|
||||||
|
|
||||||
|
if "python" not in str(mimetypes.guess_type(load)[0]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
mod = Path(load).stem
|
||||||
|
if mod == "__init__":
|
||||||
|
continue
|
||||||
|
|
||||||
|
import_module(pkg + "." + mod, pkg)
|
||||||
32
app/src/web/__init__.py
Normal file
32
app/src/web/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
'''Quart blueprint for the proxy webserver with the dashboard
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
app = Quart(__name__, ...)
|
||||||
|
Web(app)
|
||||||
|
'''
|
||||||
|
from quart import Quart, Blueprint
|
||||||
|
from quart_babel import Babel
|
||||||
|
from utils import load_modules
|
||||||
|
|
||||||
|
web = Blueprint('web', __name__)
|
||||||
|
|
||||||
|
load_modules(__loader__)
|
||||||
|
|
||||||
|
|
||||||
|
class Web:
|
||||||
|
'''Helper Class to register the Blueprint at Quart and
|
||||||
|
initializing Babel'''
|
||||||
|
def __init__(self,
|
||||||
|
app: Quart,
|
||||||
|
translation_directories: str | list[str],
|
||||||
|
rel_urls: bool):
|
||||||
|
web.build_relative_urls = rel_urls
|
||||||
|
app.register_blueprint(web)
|
||||||
|
|
||||||
|
from .i18n import get_locale, get_tz
|
||||||
|
global babel
|
||||||
|
babel = Babel(
|
||||||
|
app,
|
||||||
|
locale_selector=get_locale,
|
||||||
|
timezone_selector=get_tz,
|
||||||
|
default_translation_directories=translation_directories)
|
||||||
82
app/src/web/conn_table.py
Normal file
82
app/src/web/conn_table.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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 = {
|
||||||
|
"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_conn_table.html.j2',
|
||||||
|
table=get_table_data())
|
||||||
|
|
||||||
|
data["notes-list"] = await render_template('templ_notes_list.html.j2')
|
||||||
|
return data
|
||||||
37
app/src/web/favicon.py
Normal file
37
app/src/web/favicon.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from quart import send_from_directory
|
||||||
|
|
||||||
|
from . import web
|
||||||
|
|
||||||
|
|
||||||
|
async def get_icon(file: str, mime: str = 'image/png'):
|
||||||
|
return await send_from_directory(
|
||||||
|
os.path.join(web.root_path, 'static/images'),
|
||||||
|
file,
|
||||||
|
mimetype=mime)
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/favicon-96x96.png')
|
||||||
|
async def favicon():
|
||||||
|
return await get_icon('favicon-96x96.png')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/favicon.ico')
|
||||||
|
async def favicon_ico():
|
||||||
|
return await get_icon('favicon.ico', 'image/x-icon')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/favicon.svg')
|
||||||
|
async def favicon_svg():
|
||||||
|
return await get_icon('favicon.svg', 'image/svg+xml')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/apple-touch-icon.png')
|
||||||
|
async def apple_touch():
|
||||||
|
return await get_icon('apple-touch-icon.png')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/site.webmanifest')
|
||||||
|
async def webmanifest():
|
||||||
|
return await get_icon('site.webmanifest', 'application/manifest+json')
|
||||||
45
app/src/web/i18n.py
Normal file
45
app/src/web/i18n.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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)
|
||||||
52
app/src/web/log_files.py
Normal file
52
app/src/web/log_files.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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())
|
||||||
|
|
||||||
|
data["notes-list"] = await render_template('templ_notes_list.html.j2')
|
||||||
|
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)
|
||||||
23
app/src/web/pages.py
Normal file
23
app/src/web/pages.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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('/page')
|
||||||
|
async def empty():
|
||||||
|
return await render_template('empty.html.j2')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/logging')
|
||||||
|
async def logging():
|
||||||
|
return await render_template(
|
||||||
|
'page_logging.html.j2',
|
||||||
|
fetch_url=url_for('.file_fetch'))
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
from quart import Blueprint
|
|
||||||
from quart import render_template, url_for
|
|
||||||
from quart import send_from_directory
|
|
||||||
import os
|
|
||||||
|
|
||||||
web_routes = Blueprint('web_routes', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_icon(file: str, mime: str = 'image/png'):
|
|
||||||
return await send_from_directory(
|
|
||||||
os.path.join(web_routes.root_path, 'static/images'),
|
|
||||||
file,
|
|
||||||
mimetype=mime)
|
|
||||||
|
|
||||||
|
|
||||||
def get_inv_count():
|
|
||||||
return 1234
|
|
||||||
|
|
||||||
|
|
||||||
TsunCnt = 0
|
|
||||||
|
|
||||||
|
|
||||||
def get_tsun_count():
|
|
||||||
global TsunCnt
|
|
||||||
TsunCnt += 1
|
|
||||||
return TsunCnt
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.context_processor
|
|
||||||
def utility_processor():
|
|
||||||
return dict(inv_count=get_inv_count(),
|
|
||||||
tsun_count=get_tsun_count())
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/')
|
|
||||||
async def index():
|
|
||||||
return await render_template(
|
|
||||||
'index.html.j2',
|
|
||||||
fetch_url='.'+url_for('web_routes.data_fetch'))
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/page')
|
|
||||||
async def empty():
|
|
||||||
return await render_template('empty.html.j2')
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/data-fetch')
|
|
||||||
async def data_fetch():
|
|
||||||
global TsunCnt
|
|
||||||
TsunCnt += 1
|
|
||||||
return {
|
|
||||||
"geology-fact": f"<h3>{TsunCnt}</h3>",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/favicon-96x96.png')
|
|
||||||
async def favicon():
|
|
||||||
return await get_icon('favicon-96x96.png')
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/favicon.ico')
|
|
||||||
async def favicon_ico():
|
|
||||||
return await get_icon('favicon.ico', 'image/x-icon')
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/favicon.svg')
|
|
||||||
async def favicon_svg():
|
|
||||||
return await get_icon('favicon.svg', 'image/svg+xml')
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/apple-touch-icon.png')
|
|
||||||
async def apple_touch():
|
|
||||||
return await get_icon('apple-touch-icon.png')
|
|
||||||
|
|
||||||
|
|
||||||
@web_routes.route('/site.webmanifest')
|
|
||||||
async def webmanifest():
|
|
||||||
return await get_icon('site.webmanifest', 'application/manifest+json')
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
|||||||
<title>{% block title %}{% endblock title %}</title>
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href=".{{ url_for('static', filename= 'css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename= 'css/style.css') }}">
|
||||||
<link rel="stylesheet" href=".{{ url_for('static', filename= 'font-awesome/css/all.min.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename= 'font-awesome/css/all.min.css') }}">
|
||||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<style>
|
<style>
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Roboto;
|
font-family: Roboto;
|
||||||
src: url(".{{ url_for('static', filename= 'font/roboto-light.ttf') }}");
|
src: url("{{ url_for('static', filename= 'font/roboto-light.ttf') }}");
|
||||||
}
|
}
|
||||||
html,body,h1,h2,h3,h4,h5 {font-family: Roboto, sans-serif}
|
html,body,h1,h2,h3,h4,h5 {font-family: Roboto, sans-serif}
|
||||||
</style>
|
</style>
|
||||||
@@ -22,16 +22,28 @@
|
|||||||
<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-dark-grey w3-large" style="z-index:4">
|
||||||
<button class="w3-bar-item w3-button w3-hide-large w3-hover-none w3-hover-text-light-grey" onclick="w3_open();"><i class="fa fa-bars"></i> Menu</button>
|
<button class="w3-bar-item w3-button w3-hide-large" onclick="w3_open();"><i class="fa fa-bars"></i> Menu</button>
|
||||||
<span class="w3-bar-item w3-right">Logo</span>
|
<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>
|
</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">
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col s8 w3-bar">
|
<div class="w3-col s8 w3-bar">
|
||||||
<h3>TSUN-Proxy</h3><br>
|
<h3>TSUN-Proxy</h3><br>
|
||||||
@@ -43,22 +55,16 @@
|
|||||||
</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('.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('.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('.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>
|
||||||
<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>
|
||||||
|
|
||||||
|
|
||||||
<!-- Overlay effect when opening sidebar on small screens -->
|
<!-- 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>
|
<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! -->
|
<!-- !PAGE CONTENT! -->
|
||||||
<div class="w3-main" style="margin-left:250px;margin-top:43px;">
|
<div class="w3-main" style="margin-left:250px;margin-top:43px;">
|
||||||
|
|
||||||
@@ -130,7 +136,7 @@
|
|||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
// Your document is loaded.
|
// Your document is loaded.
|
||||||
var fetchInterval = 5000; // 5 seconds.
|
var fetchInterval = 5000; // 5 seconds.
|
||||||
|
fetch_data()
|
||||||
// Invoke the request every 5 seconds.
|
// Invoke the request every 5 seconds.
|
||||||
setInterval(fetch_data, fetchInterval);
|
setInterval(fetch_data, fetchInterval);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
{% extends 'base.html.j2' %}
|
|
||||||
|
|
||||||
{% 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>{{inv_count}}</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>{{tsun_count}}</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 id = "geology-fact" 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>
|
|
||||||
</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%}
|
|
||||||
57
app/src/web/templates/page_index.html.j2
Normal file
57
app/src/web/templates/page_index.html.j2
Normal 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%}
|
||||||
11
app/src/web/templates/page_logging.html.j2
Normal file
11
app/src/web/templates/page_logging.html.j2
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %} TSUN Proxy - Downloads {% 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 class="w3-container" id="file-list"></div>
|
||||||
|
{% endblock content%}
|
||||||
|
|
||||||
|
{% block footer %}{% endblock footer %}
|
||||||
|
|
||||||
37
app/src/web/templates/templ_conn_table.html.j2
Normal file
37
app/src/web/templates/templ_conn_table.html.j2
Normal 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>
|
||||||
31
app/src/web/templates/templ_log_files_list.html.j2
Normal file
31
app/src/web/templates/templ_log_files_list.html.j2
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<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-blue">
|
||||||
|
<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-blue">
|
||||||
|
<a href="{{ url_for('web.send',file=file.name)}}" class="w3-button w3-hover-blue w3-hover-text-black"><i class="fa fa-file-export"></i> {{_('Download File')}}</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% if 0 == (loop.index%4) and not last %}
|
||||||
|
</div>
|
||||||
|
<div class="w3-row-padding w3-margin-bottom">
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
0
app/src/web/templates/templ_notes_list.html.j2
Normal file
0
app/src/web/templates/templ_notes_list.html.j2
Normal file
26
app/src/web/wrapper.py
Normal file
26
app/src/web/wrapper.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from quart import url_for as quart_url_for
|
||||||
|
from . import web
|
||||||
|
|
||||||
|
|
||||||
|
def url_for(*args, **kwargs):
|
||||||
|
"""Return the url for a specific endpoint.
|
||||||
|
|
||||||
|
This wrapper optionally convert into a relative url.
|
||||||
|
|
||||||
|
This is most useful in templates and redirects to create a URL
|
||||||
|
that can be used in the browser.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
endpoint: The endpoint to build a url for, if prefixed with
|
||||||
|
``.`` it targets endpoint's in the current blueprint.
|
||||||
|
_anchor: Additional anchor text to append (i.e. #text).
|
||||||
|
_external: Return an absolute url for external (to app) usage.
|
||||||
|
_method: The method to consider alongside the endpoint.
|
||||||
|
_scheme: A specific scheme to use.
|
||||||
|
values: The values to build into the URL, as specified in
|
||||||
|
the endpoint rule.
|
||||||
|
"""
|
||||||
|
url = quart_url_for(*args, **kwargs)
|
||||||
|
if '/' == url[0] and web.build_relative_urls:
|
||||||
|
url = '.' + url
|
||||||
|
return url
|
||||||
0
app/tests/log/sub_dir/not_reachable.txt
Normal file
0
app/tests/log/sub_dir/not_reachable.txt
Normal file
19
app/tests/log/test.txt
Normal file
19
app/tests/log/test.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ def config_conn():
|
|||||||
},
|
},
|
||||||
'solarman':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234}, 'inverters':{'allow_all':True}
|
'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)
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
def module_init():
|
def module_init():
|
||||||
|
|||||||
@@ -1,73 +1,208 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest
|
import pytest
|
||||||
from server import app
|
from server import app
|
||||||
|
from web import Web, web
|
||||||
|
from async_stream import AsyncStreamClient
|
||||||
|
from gen3plus.inverter_g3p import InverterG3P
|
||||||
|
from test_inverter_g3p import FakeReader, FakeWriter, config_conn
|
||||||
|
from cnf.config import Config
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def client():
|
||||||
|
app.secret_key = 'super secret key'
|
||||||
|
Web(app, '../transfer', False)
|
||||||
|
return app.test_client()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
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(client):
|
||||||
"""Test the home route."""
|
"""Test the home route."""
|
||||||
client = app.test_client()
|
|
||||||
response = await client.get('/')
|
response = await client.get('/')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.mimetype == 'text/html'
|
assert response.mimetype == 'text/html'
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_page():
|
async def test_page(client):
|
||||||
"""Test the empty page route."""
|
"""Test the empty page route."""
|
||||||
client = app.test_client()
|
|
||||||
response = await client.get('/page')
|
response = await client.get('/page')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.mimetype == 'text/html'
|
assert response.mimetype == 'text/html'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_rel_page(client):
|
||||||
|
"""Test the empty page route."""
|
||||||
|
web.build_relative_urls = True
|
||||||
|
response = await client.get('/page')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.mimetype == 'text/html'
|
||||||
|
web.build_relative_urls = False
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_favicon96():
|
async def test_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):
|
||||||
"""Test the favicon-96x96.png route."""
|
"""Test the favicon-96x96.png route."""
|
||||||
client = app.test_client()
|
|
||||||
response = await client.get('/favicon-96x96.png')
|
response = await client.get('/favicon-96x96.png')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.mimetype == 'image/png'
|
assert response.mimetype == 'image/png'
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_favicon():
|
async def test_favicon(client):
|
||||||
"""Test the favicon.ico route."""
|
"""Test the favicon.ico route."""
|
||||||
client = app.test_client()
|
|
||||||
response = await client.get('/favicon.ico')
|
response = await client.get('/favicon.ico')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.mimetype == 'image/x-icon'
|
assert response.mimetype == 'image/x-icon'
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_favicon_svg():
|
async def test_favicon_svg(client):
|
||||||
"""Test the favicon.svg route."""
|
"""Test the favicon.svg route."""
|
||||||
client = app.test_client()
|
|
||||||
response = await client.get('/favicon.svg')
|
response = await client.get('/favicon.svg')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.mimetype == 'image/svg+xml'
|
assert response.mimetype == 'image/svg+xml'
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_apple_touch_icon():
|
async def test_apple_touch_icon(client):
|
||||||
"""Test the apple-touch-icon.png route."""
|
"""Test the apple-touch-icon.png route."""
|
||||||
client = app.test_client()
|
|
||||||
response = await client.get('/apple-touch-icon.png')
|
response = await client.get('/apple-touch-icon.png')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.mimetype == 'image/png'
|
assert response.mimetype == 'image/png'
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_manifest():
|
async def test_manifest(client):
|
||||||
"""Test the site.webmanifest route."""
|
"""Test the site.webmanifest route."""
|
||||||
client = app.test_client()
|
|
||||||
response = await client.get('/site.webmanifest')
|
response = await client.get('/site.webmanifest')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.mimetype == 'application/manifest+json'
|
assert response.mimetype == 'application/manifest+json'
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_data_fetch():
|
async def test_data_fetch(create_inverter):
|
||||||
"""Test the healthy route."""
|
"""Test the data-fetch route."""
|
||||||
|
_ = create_inverter
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
response = await client.get('/data-fetch')
|
response = await client.get('/data-fetch')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
response = await client.get('/data-fetch')
|
response = await client.get('/data-fetch')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_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('/page')
|
||||||
|
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_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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
|
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-04-20 00:01+0200\n"
|
"POT-Creation-Date: 2025-05-01 17:48+0200\n"
|
||||||
"PO-Revision-Date: 2025-04-18 16:24+0200\n"
|
"PO-Revision-Date: 2025-04-18 16:24+0200\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
@@ -19,7 +19,89 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
#: src/web/templates/index.html.j2:5
|
#: src/web/conn_table.py:57
|
||||||
msgid "My Dashboard"
|
msgid "Device-IP:Port"
|
||||||
msgstr "Mein Dashboard"
|
msgstr "Geräte-IP:Port"
|
||||||
|
|
||||||
|
#: src/web/conn_table.py:57
|
||||||
|
msgid "Device-IP"
|
||||||
|
msgstr "Geräte-IP"
|
||||||
|
|
||||||
|
#: src/web/conn_table.py:58
|
||||||
|
msgid "Serial-No"
|
||||||
|
msgstr "Seriennummer"
|
||||||
|
|
||||||
|
#: src/web/conn_table.py:59
|
||||||
|
msgid "Cloud-IP:Port"
|
||||||
|
msgstr "Cloud-IP:Port"
|
||||||
|
|
||||||
|
#: src/web/conn_table.py:59
|
||||||
|
msgid "Cloud-IP"
|
||||||
|
msgstr "Cloud-IP"
|
||||||
|
|
||||||
|
#: src/web/templates/base.html.j2:37
|
||||||
|
msgid "Updated:"
|
||||||
|
msgstr "Aktualisiert:"
|
||||||
|
|
||||||
|
#: src/web/templates/base.html.j2:58
|
||||||
|
#: src/web/templates/templ_conn_table.html.j2:15
|
||||||
|
msgid "Connections"
|
||||||
|
msgstr "Verbindungen"
|
||||||
|
|
||||||
|
#: 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:16
|
||||||
|
msgid "Server Mode"
|
||||||
|
msgstr "Server Modus"
|
||||||
|
|
||||||
|
#: src/web/templates/page_index.html.j2:17
|
||||||
|
msgid "Established from device to proxy"
|
||||||
|
msgstr "Vom Gerät zum Proxy aufgebaut"
|
||||||
|
|
||||||
|
#: src/web/templates/page_index.html.j2:27
|
||||||
|
msgid "Client Mode"
|
||||||
|
msgstr "Client Modus"
|
||||||
|
|
||||||
|
#: src/web/templates/page_index.html.j2:28
|
||||||
|
msgid "Established from proxy to device"
|
||||||
|
msgstr "Vom Proxy zum Gerät aufgebaut"
|
||||||
|
|
||||||
|
#: src/web/templates/page_index.html.j2:38
|
||||||
|
msgid "Proxy Mode"
|
||||||
|
msgstr "Proxy Modus"
|
||||||
|
|
||||||
|
#: src/web/templates/page_index.html.j2:39
|
||||||
|
msgid "Forwarding data to cloud"
|
||||||
|
msgstr "Weiterleitung in die Cloud"
|
||||||
|
|
||||||
|
#: src/web/templates/page_index.html.j2:49
|
||||||
|
msgid "Emu Mode"
|
||||||
|
msgstr "Emu Modus"
|
||||||
|
|
||||||
|
#: src/web/templates/page_index.html.j2:50
|
||||||
|
msgid "Emulation sends data to cloud"
|
||||||
|
msgstr "Emulation sendet in die Cloud"
|
||||||
|
|
||||||
|
#: 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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
|||||||
$(wildcard $(SRC_PROXY)/cnf/*.toml)\
|
$(wildcard $(SRC_PROXY)/cnf/*.toml)\
|
||||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)\
|
$(wildcard $(SRC_PROXY)/gen3plus/*.py)\
|
||||||
|
$(wildcard $(SRC_PROXY)/utils/*.py)\
|
||||||
$(wildcard $(SRC_PROXY)/web/*.py)\
|
$(wildcard $(SRC_PROXY)/web/*.py)\
|
||||||
$(wildcard $(SRC_PROXY)/web/templates/*.html.j2)\
|
$(wildcard $(SRC_PROXY)/web/templates/*.html.j2)\
|
||||||
$(wildcard $(SRC_PROXY)/web/static/css/*.css)\
|
$(wildcard $(SRC_PROXY)/web/static/css/*.css)\
|
||||||
|
|||||||
@@ -30,4 +30,4 @@ cd /home/proxy || exit
|
|||||||
export VERSION=$(cat /proxy-version.txt)
|
export VERSION=$(cat /proxy-version.txt)
|
||||||
|
|
||||||
echo "Start Proxyserver..."
|
echo "Start Proxyserver..."
|
||||||
python3 server.py --json_config=/data/options.json --log_path=/homeassistant/tsun-proxy/logs/ --config_path=/homeassistant/tsun-proxy/ --log_backups=2
|
python3 server.py --rel_urls=True --json_config=/data/options.json --log_path=/homeassistant/tsun-proxy/logs/ --config_path=/homeassistant/tsun-proxy/ --log_backups=2
|
||||||
|
|||||||
Reference in New Issue
Block a user