From 6127fbe940fd70bd6014d71e978d94516271cd78 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sat, 2 Sep 2023 12:22:22 +0200 Subject: [PATCH] Feature: Added option to set runtime values to zero when inverter becames unreachable --- include/Configuration.h | 1 + .../src/commands/RealTimeRunDataCommand.cpp | 5 ++ .../src/inverters/InverterAbstract.cpp | 10 +++ lib/Hoymiles/src/inverters/InverterAbstract.h | 5 ++ lib/Hoymiles/src/parser/StatisticsParser.cpp | 77 +++++++++++++++++++ lib/Hoymiles/src/parser/StatisticsParser.h | 4 + src/Configuration.cpp | 2 + src/InverterSettings.cpp | 1 + src/WebApi_inverter.cpp | 3 + webapp/src/locales/de.json | 2 + webapp/src/locales/en.json | 2 + webapp/src/locales/fr.json | 2 + webapp/src/views/InverterAdminView.vue | 6 ++ 13 files changed, 120 insertions(+) diff --git a/include/Configuration.h b/include/Configuration.h index 464fbcfb6..97e5c3e9c 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -46,6 +46,7 @@ struct INVERTER_CONFIG_T { bool Command_Enable; bool Command_Enable_Night; uint8_t ReachableThreshold; + bool ZeroRuntimeDataIfUnrechable; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index 3f0aed36b..e5ece4092 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -55,4 +55,9 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment void RealTimeRunDataCommand::gotTimeout(InverterAbstract* inverter) { inverter->Statistics()->incrementRxFailureCount(); + + if (inverter->getZeroValuesIfUnreachable() && !inverter->isReachable()) { + Hoymiles.getMessageOutput()->println("Set runtime data to zero"); + inverter->Statistics()->zeroRuntimeData(); + } } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index f78be2330..26be98e52 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -106,6 +106,16 @@ uint8_t InverterAbstract::getReachableThreshold() return _reachableThreshold; } +void InverterAbstract::setZeroValuesIfUnreachable(bool enabled) +{ + _zeroValuesIfUnreachable = enabled; +} + +bool InverterAbstract::getZeroValuesIfUnreachable() +{ + return _zeroValuesIfUnreachable; +} + bool InverterAbstract::sendChangeChannelRequest() { return false; diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 63a61c546..003ccaa5c 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -51,6 +51,9 @@ class InverterAbstract { void setReachableThreshold(uint8_t threshold); uint8_t getReachableThreshold(); + void setZeroValuesIfUnreachable(bool enabled); + bool getZeroValuesIfUnreachable(); + void clearRxFragmentBuffer(); void addRxFragment(uint8_t fragment[], uint8_t len); uint8_t verifyAllFragments(CommandAbstract* cmd); @@ -91,6 +94,8 @@ class InverterAbstract { uint8_t _reachableThreshold = 3; + bool _zeroValuesIfUnreachable = false; + std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; std::unique_ptr _powerCommandParser; diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index ac61be81b..2c373b645 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -33,6 +33,28 @@ const calcFunc_t calcFunctions[] = { { CALC_IRR_CH, &calcIrradiation } }; +const FieldId_t runtimeFields[] = { + FLD_UDC, + FLD_IDC, + FLD_PDC, + FLD_UAC, + FLD_IAC, + FLD_PAC, + FLD_F, + FLD_T, + FLD_PF, + FLD_Q, + FLD_UAC_1N, + FLD_UAC_2N, + FLD_UAC_3N, + FLD_UAC_12, + FLD_UAC_23, + FLD_UAC_31, + FLD_IAC_1, + FLD_IAC_2, + FLD_IAC_3, +}; + StatisticsParser::StatisticsParser() : Parser() { @@ -150,6 +172,47 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch return 0; } +bool StatisticsParser::setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value) +{ + const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + + if (pos == NULL) { + return false; + } + + uint8_t ptr = pos->start + pos->num - 1; + uint8_t end = pos->start; + uint16_t div = pos->div; + + if (CMD_CALC == div) { + return false; + } + + if (setting != NULL) { + value -= setting->offset; + } + value *= static_cast(div); + + uint32_t val = 0; + if (pos->isSigned && pos->num == 2) { + val = static_cast(static_cast(value)); + } else if (pos->isSigned && pos->num == 4) { + val = static_cast(static_cast(value)); + } else { + val = static_cast(value); + } + + HOY_SEMAPHORE_TAKE(); + do { + _payloadStatistic[ptr] = val; + val >>= 8; + } while (--ptr >= end); + HOY_SEMAPHORE_GIVE(); + + return true; +} + String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { return String( @@ -253,6 +316,20 @@ uint32_t StatisticsParser::getRxFailureCount() return _rxFailureCount; } +void StatisticsParser::zeroRuntimeData() +{ + // Loop all channels + for (auto& t : getChannelTypes()) { + for (auto& c : getChannelsByType(t)) { + for (uint8_t i = 0; i < (sizeof(runtimeFields) / sizeof(runtimeFields[0])); i++) { + if (hasChannelFieldValue(t, c, runtimeFields[i])) { + setChannelFieldValue(t, c, runtimeFields[i], 0); + } + } + } + } +} + static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) { float yield = 0; diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 13d7d4f47..b50bcdb2d 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -125,6 +125,8 @@ class StatisticsParser : public Parser { const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); uint8_t getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + bool setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value); + float getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); void setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset); @@ -139,6 +141,8 @@ class StatisticsParser : public Parser { void incrementRxFailureCount(); uint32_t getRxFailureCount(); + void zeroRuntimeData(); + private: uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {}; uint8_t _statisticLength = 0; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index d7aeba05c..4a7979ac6 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -111,6 +111,7 @@ bool ConfigurationClass::write() inv["command_enable"] = config.Inverter[i].Command_Enable; inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night; inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold; + inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -260,6 +261,7 @@ bool ConfigurationClass::read() config.Inverter[i].Command_Enable = inv["command_enable"] | true; config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true; config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD; + config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false; JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 604708286..93b5958c8 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -72,6 +72,7 @@ void InverterSettingsClass::init() if (inv != nullptr) { inv->setReachableThreshold(config.Inverter[i].ReachableThreshold); + inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset); diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index ad9fd2c82..458fcf415 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -59,6 +59,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) obj["command_enable"] = config.Inverter[i].Command_Enable; obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night; obj["reachable_threshold"] = config.Inverter[i].ReachableThreshold; + obj["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); uint8_t max_channels; @@ -284,6 +285,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.Command_Enable = root["command_enable"] | true; inverter.Command_Enable_Night = root["command_enable_night"] | true; inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; + inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; arrayCount++; } @@ -315,6 +317,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inv->setEnablePolling(inverter.Poll_Enable); inv->setEnableCommands(inverter.Command_Enable); inv->setReachableThreshold(inverter.ReachableThreshold); + inv->setZeroValuesIfUnreachable(inverter.ZeroRuntimeDataIfUnrechable); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, inverter.channel[c].YieldTotalOffset); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 5f4410f2e..6f042dedc 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -472,6 +472,8 @@ "InverterHint": "*) Geben Sie die Wp des Ports ein, um die Einstrahlung zu errechnen.", "ReachableThreshold": "Erreichbarkeit Schwellenwert:", "ReachableThresholdHint": "Legt fest, wie viele Anfragen fehlschlagen dürfen, bis der Wechselrichter als unerreichbar eingestuft wird.", + "ZeroRuntime": "Nulle Laufzeit Daten", + "ZeroRuntimeHint": "Nulle Laufzeit Daten (keine Ertragsdaten), wenn der Wechselrichter nicht erreichbar ist.", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", "DeleteMsg": "Soll der Wechselrichter \"{name}\" mit der Seriennummer {serial} wirklich gelöscht werden?", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index d6c74e184..5da435824 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -472,6 +472,8 @@ "InverterHint": "*) Enter the Wp of the channel to calculate irradiation.", "ReachableThreshold": "Reachable Threshold:", "ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.", + "ZeroRuntime": "Zero runtime data", + "ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", "DeleteMsg": "Are you sure you want to delete the inverter \"{name}\" with serial number {serial}?", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index ae6574ef5..609a2e887 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -472,6 +472,8 @@ "InverterHint": "*) Entrez le Wp du canal pour calculer l'irradiation.", "ReachableThreshold": "Reachable Threshold:", "ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.", + "ZeroRuntime": "Zero runtime data", + "ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", "DeleteMsg": "Êtes-vous sûr de vouloir supprimer l'onduleur \"{name}\" avec le numéro de série \"{serial}\" ?", diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index c7c798a36..3b2b41fc8 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -182,6 +182,11 @@ v-model="selectedInverterData.reachable_threshold" type="number" min="1" max="100" :tooltip="$t('inverteradmin.ReachableThresholdHint')" wide /> + + @@ -257,6 +262,7 @@ declare interface Inverter { command_enable: boolean; command_enable_night: boolean; reachable_threshold: number; + zero_runtime: boolean; channel: Array; }