Feature: Inverter radio statistics (rx/tx statistics)

The  statistics are shown in the WebApp and published via MQTT.
Statistics are reset at midnight.
This commit is contained in:
Thomas Basler 2024-09-22 12:44:48 +02:00
parent 1115418ce1
commit a54b19bf5b
11 changed files with 189 additions and 3 deletions

View File

@ -145,6 +145,7 @@ void HoymilesClass::loop()
if (inv->getClearEventlogOnMidnight()) { if (inv->getClearEventlogOnMidnight()) {
inv->EventLog()->clearBuffer(); inv->EventLog()->clearBuffer();
} }
inv->resetRadioStats();
} }
lastWeekDay = currentWeekDay; lastWeekDay = currentWeekDay;

View File

@ -66,16 +66,25 @@ void HoymilesRadio::handleReceivedPackage()
} else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) {
Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded");
// Statistics: Count RX Fail No Answer
inv->RadioStats.RxFailNoAnswer++;
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) {
Hoymiles.getMessageOutput()->println("Retransmit timeout"); Hoymiles.getMessageOutput()->println("Retransmit timeout");
// Statistics: Count RX Fail Partial Answer
inv->RadioStats.RxFailPartialAnswer++;
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} else if (verifyResult == FRAGMENT_HANDLE_ERROR) { } else if (verifyResult == FRAGMENT_HANDLE_ERROR) {
Hoymiles.getMessageOutput()->println("Packet handling error"); Hoymiles.getMessageOutput()->println("Packet handling error");
// Statistics: Count RX Fail Corrupt Data
inv->RadioStats.RxFailCorruptData++;
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
@ -83,17 +92,24 @@ void HoymilesRadio::handleReceivedPackage()
// Perform Retransmit // Perform Retransmit
Hoymiles.getMessageOutput()->print("Request retransmit: "); Hoymiles.getMessageOutput()->print("Request retransmit: ");
Hoymiles.getMessageOutput()->println(verifyResult); Hoymiles.getMessageOutput()->println(verifyResult);
// Statistics: Count TX Re-Request Fragment
inv->RadioStats.TxReRequestFragment++;
sendRetransmitPacket(verifyResult); sendRetransmitPacket(verifyResult);
} else { } else {
// Successful received all packages // Successful received all packages
Hoymiles.getMessageOutput()->println("Success"); Hoymiles.getMessageOutput()->println("Success");
// Statistics: Count RX Success
inv->RadioStats.RxSuccess++;
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} }
} else { } else {
// If inverter was not found, assume the command is invalid // If inverter was not found, assume the command is invalid
Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); Hoymiles.getMessageOutput()->println("RX: Invalid inverter found");
// Statistics: Count RX Fail Unknown Data
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} }
@ -105,6 +121,9 @@ void HoymilesRadio::handleReceivedPackage()
auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress());
if (nullptr != inv) { if (nullptr != inv) {
inv->clearRxFragmentBuffer(); inv->clearRxFragmentBuffer();
// Statistics: TX Requests
inv->RadioStats.TxRequestData++;
sendEsbPacket(*cmd); sendEsbPacket(*cmd);
} else { } else {
Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); Hoymiles.getMessageOutput()->println("TX: Invalid inverter found");

View File

@ -272,3 +272,8 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd)
return FRAGMENT_OK; return FRAGMENT_OK;
} }
void InverterAbstract::resetRadioStats()
{
RadioStats = {};
}

View File

@ -65,6 +65,28 @@ public:
void addRxFragment(const uint8_t fragment[], const uint8_t len); void addRxFragment(const uint8_t fragment[], const uint8_t len);
uint8_t verifyAllFragments(CommandAbstract& cmd); uint8_t verifyAllFragments(CommandAbstract& cmd);
void resetRadioStats();
struct {
// TX Request Data
uint32_t TxRequestData;
// TX Re-Request Fragment
uint32_t TxReRequestFragment;
// RX Success
uint32_t RxSuccess;
// RX Fail Partial Answer
uint32_t RxFailPartialAnswer;
// RX Fail No Answer
uint32_t RxFailNoAnswer;
// RX Fail Corrupt Data
uint32_t RxFailCorruptData;
} RadioStats = {};
virtual bool sendStatsRequest() = 0; virtual bool sendStatsRequest() = 0;
virtual bool sendAlarmLogRequest(const bool force = false) = 0; virtual bool sendAlarmLogRequest(const bool force = false) = 0;
virtual bool sendDevInfoRequest() = 0; virtual bool sendDevInfoRequest() = 0;

View File

@ -50,6 +50,14 @@ void MqttHandleInverterClass::loop()
// Name // Name
MqttSettings.publish(subtopic + "/name", inv->name()); MqttSettings.publish(subtopic + "/name", inv->name());
// Radio Statistics
MqttSettings.publish(subtopic + "/radio/tx_request", String(inv->RadioStats.TxRequestData));
MqttSettings.publish(subtopic + "/radio/tx_re_request", String(inv->RadioStats.TxReRequestFragment));
MqttSettings.publish(subtopic + "/radio/rx_success", String(inv->RadioStats.RxSuccess));
MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData));
if (inv->DevInfo()->getLastUpdate() > 0) { if (inv->DevInfo()->getLastUpdate() > 0) {
// Bootloader Version // Bootloader Version
MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion()));

View File

@ -134,6 +134,12 @@ void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std
} else { } else {
root["limit_absolute"] = -1; root["limit_absolute"] = -1;
} }
root["radio_stats"]["tx_request"] = inv->RadioStats.TxRequestData;
root["radio_stats"]["tx_re_request"] = inv->RadioStats.TxReRequestFragment;
root["radio_stats"]["rx_success"] = inv->RadioStats.RxSuccess;
root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer;
root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer;
root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData;
} }
void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv) void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv)

View File

@ -141,7 +141,14 @@
"Unknown": "Unbekannt", "Unknown": "Unbekannt",
"ShowGridProfile": "Zeige Grid Profil", "ShowGridProfile": "Zeige Grid Profil",
"GridProfile": "Grid Profil", "GridProfile": "Grid Profil",
"LoadingInverter": "Warte auf Daten... (kann bis zu 10 Sekunden dauern)" "LoadingInverter": "Warte auf Daten... (kann bis zu 10 Sekunden dauern)",
"RadioStats": "Funkstatistik",
"TxRequest": "Gesendete Anfragen",
"RxSuccess": "Empfang Erfolgreich",
"RxFailNothing": "Empfang Fehler: Nichts empfangen",
"RxFailPartial": "Empfang Fehler: Teilweise empfangen",
"RxFailCorrupt": "Empfang Fehler: Beschädigt empfangen",
"TxReRequest": "Gesendete Fragment Wiederanforderungen"
}, },
"eventlog": { "eventlog": {
"Start": "Beginn", "Start": "Beginn",

View File

@ -141,7 +141,14 @@
"Unknown": "Unknown", "Unknown": "Unknown",
"ShowGridProfile": "Show Grid Profile", "ShowGridProfile": "Show Grid Profile",
"GridProfile": "Grid Profile", "GridProfile": "Grid Profile",
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)" "LoadingInverter": "Waiting for data... (can take up to 10 seconds)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment"
}, },
"eventlog": { "eventlog": {
"Start": "Start", "Start": "Start",

View File

@ -141,7 +141,14 @@
"Unknown": "Inconnu", "Unknown": "Inconnu",
"ShowGridProfile": "Show Grid Profile", "ShowGridProfile": "Show Grid Profile",
"GridProfile": "Grid Profile", "GridProfile": "Grid Profile",
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)" "LoadingInverter": "Waiting for data... (can take up to 10 seconds)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment"
}, },
"eventlog": { "eventlog": {
"Start": "Départ", "Start": "Départ",

View File

@ -21,6 +21,15 @@ export interface InverterStatistics {
Irradiation?: ValueObject; Irradiation?: ValueObject;
} }
export interface RadioStatistics {
tx_request: number;
tx_re_request: number;
rx_success: number;
rx_fail_nothing: number;
rx_fail_partial: number;
rx_fail_corrupt: number;
}
export interface Inverter { export interface Inverter {
serial: string; serial: string;
name: string; name: string;
@ -35,6 +44,7 @@ export interface Inverter {
AC: InverterStatistics[]; AC: InverterStatistics[];
DC: InverterStatistics[]; DC: InverterStatistics[];
INV: InverterStatistics[]; INV: InverterStatistics[];
radio_stats: RadioStatistics;
} }
export interface Total { export interface Total {

View File

@ -201,6 +201,7 @@
</template> </template>
</template> </template>
</div> </div>
<BootstrapAlert class="m-3" :show="!inverter.hasOwnProperty('INV')"> <BootstrapAlert class="m-3" :show="!inverter.hasOwnProperty('INV')">
<div class="d-flex justify-content-center align-items-center"> <div class="d-flex justify-content-center align-items-center">
<div class="spinner-border m-1" role="status"> <div class="spinner-border m-1" role="status">
@ -209,6 +210,93 @@
<span>{{ $t('home.LoadingInverter') }}</span> <span>{{ $t('home.LoadingInverter') }}</span>
</div> </div>
</BootstrapAlert> </BootstrapAlert>
<div class="accordion mt-5" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseStats"
aria-expanded="true"
aria-controls="collapseStats"
>
{{ $t('home.RadioStats') }}
</button>
</h2>
<div
id="collapseStats"
class="accordion-collapse collapse"
data-bs-parent="#accordionExample"
>
<div class="accordion-body">
<table class="table table-striped table-hover">
<tbody>
<tr>
<td>{{ $t('home.TxRequest') }}</td>
<td>{{ $n(inverter.radio_stats.tx_request) }}</td>
<td></td>
</tr>
<tr>
<td>{{ $t('home.RxSuccess') }}</td>
<td>{{ $n(inverter.radio_stats.rx_success) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_success,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.RxFailNothing') }}</td>
<td>{{ $n(inverter.radio_stats.rx_fail_nothing) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_fail_nothing,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.RxFailPartial') }}</td>
<td>{{ $n(inverter.radio_stats.rx_fail_partial) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_fail_partial,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.RxFailCorrupt') }}</td>
<td>{{ $n(inverter.radio_stats.rx_fail_corrupt) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_fail_corrupt,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.TxReRequest') }}</td>
<td>{{ $n(inverter.radio_stats.tx_re_request) }}</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -786,6 +874,12 @@ export default defineComponent({
}); });
return total; return total;
}, },
ratio(val_small: number, val_large: number): string {
if (val_large == 0) {
return '-';
}
return this.$n(val_small / val_large, 'percent');
},
}, },
}); });
</script> </script>