Compare commits

...

18 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
18 changed files with 326 additions and 206 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -108,6 +108,7 @@ class Message(ProtocolIfc):
self.header_len = 0
self.data_len = 0
self.unique_id = 0
self.inv_serial = ''
self.sug_area = ''
self.new_data = {}
self.state = State.init
@@ -140,8 +141,9 @@ class Message(ProtocolIfc):
# to our _recv_buffer
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'''
self.inv_serial = inv_serial
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']

View File

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

View File

@@ -41,11 +41,16 @@ def my_get_locale():
)
def my_get_tz():
return 'CET'
app = Quart(__name__,
template_folder='web/templates',
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)

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 render_template, url_for
from quart import send_from_directory
from quart_babel import format_datetime
from infos import Infos
from web.conn_table import get_table_data
import os
web_routes = Blueprint('web_routes', __name__)
@@ -13,25 +16,6 @@ async def get_icon(file: str, mime: str = 'image/png'):
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(
@@ -46,11 +30,18 @@ async def empty():
@web_routes.route('/data-fetch')
async def data_fetch():
global TsunCnt
TsunCnt += 1
return {
"geology-fact": f"<h3>{TsunCnt}</h3>",
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')

File diff suppressed because one or more lines are too long

View File

@@ -22,13 +22,17 @@
<body class="w3-light-grey">
<!-- 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>
{% 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>
{% endif %}
</div>
<!-- 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-col s4">
<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 class="w3-bar-block">
<button href="#" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i>  Close Menu</button>
<a href=".{{ url_for('web_routes.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-users fa-fw"></i>  Overview</a>
<a href=".{{ url_for('web_routes.empty')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-eye fa-fw"></i>  Views</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-users fa-fw {% block menu3_class %}{% endblock %}"></i>  Traffic</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-bullseye fa-fw {% block menu4_class %}{% endblock %}"></i>  Geo</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-gem fa-fw {% block menu5_class %}{% endblock %}"></i>  Orders</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-bell fa-fw {% block menu6_class %}{% endblock %}"></i>  News</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-university fa-fw {% block menu7_class %}{% endblock %}"></i>  General</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-history fa-fw {% block menu8_class %}{% endblock %}"></i>  History</a>
<a href="#" class="w3-bar-item w3-button w3-padding"><i class="fa fa-cog fa-fw {% block menu9_class %}{% endblock %}"></i>  Settings</a><br><br>
<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-database fa-fw"></i>  MQTT</a>
<a href=".{{ url_for('web_routes.empty')}}" class="w3-bar-item w3-button w3-padding"><i class="fa fa-file-export fa-fw {% block menu3_class %}{% endblock %}"></i>  Downloads</a>
</div>
</nav>
@@ -130,7 +128,7 @@
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);
});

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,179 +1,57 @@
{% extends 'base.html.j2' %}
{% block title %} TSUN Proxy - Dashboard {% endblock title%}
{% block title %} TSUN Proxy - Connections {% endblock title%}
{% block menu1_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-dashboard"></i> {{_('My Dashboard')}}{% endblock headline %}
{% 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-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 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>Messages</h4>
<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-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 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>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>
<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-users w3-xxxlarge"></i></div>
<div class="w3-right">
<h3>50</h3>
<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>Users</h4>
<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-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>
<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 async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
from messages import Message
from mock import patch, call
from test_modbus_tcp import FakeReader, FakeWriter
from test_inverter_base import config_conn, patch_open_connection
@@ -74,6 +75,13 @@ def test_health():
cnt += 1
assert cnt == 0
@pytest.fixture
def spy_inc_cnt():
with patch.object(Infos, 'inc_counter', wraps=Infos.inc_counter) as infos:
yield infos
@pytest.mark.asyncio
async def test_close_cb():
assert asyncio.get_running_loop()
@@ -529,9 +537,10 @@ async def test_forward_runtime_error2():
del ifc
@pytest.mark.asyncio
async def test_forward_runtime_error3():
async def test_forward_runtime_error3(spy_inc_cnt):
assert asyncio.get_running_loop()
remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0
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.fwd_add(b'test-forward_msg')
await ifc.server_loop()
spy.assert_has_calls([call('Inverter_Cnt'), call('ServerMode_Cnt')])
assert Infos.get_counter('Inverter_Cnt') == 0
assert Infos.get_counter('ServerMode_Cnt') == 0
assert cnt == 1
del ifc
@pytest.mark.asyncio
async def test_forward_resp():
async def test_forward_resp(spy_inc_cnt):
assert asyncio.get_running_loop()
remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0
def _close_cb():
@@ -557,27 +570,35 @@ async def test_forward_resp():
cnt += 1
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)
ifc.fwd_add(b'test-forward_msg')
await ifc.client_loop('')
spy.assert_has_calls([call('Cloud_Conn_Cnt'), call('EmuMode_Cnt')])
assert Infos.get_counter('Cloud_Conn_Cnt') == 0
assert Infos.get_counter('EmuMode_Cnt') == 0
assert cnt == 1
del ifc
@pytest.mark.asyncio
async def test_forward_resp2():
async def test_forward_resp2(spy_inc_cnt):
assert asyncio.get_running_loop()
remote = StreamPtr(None)
spy = spy_inc_cnt
cnt = 0
def _close_cb():
nonlocal cnt
cnt += 1
cnt = 0
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb)
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb, use_emu = False)
create_remote(remote, TestType.FWD_NO_EXCPT)
ifc.fwd_add(b'test-forward_msg')
await ifc.client_loop('')
spy.assert_has_calls([call('Cloud_Conn_Cnt'), call('ProxyMode_Cnt'), call('SW_Exception')])
assert Infos.get_counter('Cloud_Conn_Cnt') == 0
assert Infos.get_counter('ProxyMode_Cnt') == 0
assert cnt == 1
del ifc

View File

@@ -2,8 +2,38 @@
import pytest
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.fixture
def create_inverter(config_conn):
_ = config_conn
inv = InverterG3P(FakeReader(), FakeWriter(), client_mode=False)
return inv
@pytest.fixture
def create_inverter_server(config_conn):
_ = config_conn
inv = InverterG3P(FakeReader(), FakeWriter(), client_mode=False)
ifc = AsyncStreamClient(FakeReader(), FakeWriter(), inv.local,
None, inv.use_emulation)
inv.remote.ifc = ifc
return inv
@pytest.fixture
def create_inverter_client(config_conn):
_ = config_conn
inv = InverterG3P(FakeReader(), FakeWriter(), client_mode=True)
ifc = AsyncStreamClient(FakeReader(), FakeWriter(), inv.local,
None, inv.use_emulation)
inv.remote.ifc = ifc
return inv
@pytest.mark.asyncio
async def test_home():
@@ -63,8 +93,31 @@ async def test_manifest():
assert response.mimetype == 'application/manifest+json'
@pytest.mark.asyncio
async def test_data_fetch():
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

View File

@@ -8,7 +8,7 @@ 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 00:01+0200\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"
@@ -19,7 +19,47 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: src/web/templates/index.html.j2:5
msgid "My Dashboard"
msgstr "Mein Dashboard"
#: 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"