diff --git a/include/Configuration.h b/include/Configuration.h index 7f3d7db..15c037a 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -34,6 +34,7 @@ struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; char Name[CHAN_MAX_NAME_STRLEN]; + float YieldTotalOffset; }; struct INVERTER_CONFIG_T { diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index 79ff16d..0af50ed 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -59,9 +59,21 @@ const byteAssign_t* StatisticsParser::getAssignmentByChannelField(ChannelType_t return NULL; } +fieldSettings_t* StatisticsParser::getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + for (auto& i : _fieldSettings) { + if (i.type == type && i.ch == channel && i.fieldId == fieldId) { + return &i; + } + } + return NULL; +} + float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (pos == NULL) { return 0; } @@ -88,6 +100,9 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch } result /= static_cast(div); + if (setting != NULL) { + result += setting->offset; + } return result; } else { // Value has to be calculated @@ -121,6 +136,25 @@ uint8_t StatisticsParser::getChannelFieldDigits(ChannelType_t type, ChannelNum_t return pos->digits; } +float StatisticsParser::getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != NULL) { + return setting->offset; + } + return 0; +} + +void StatisticsParser::setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset) +{ + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != NULL) { + setting->offset = offset; + } else { + _fieldSettings.push_back({ type, channel, fieldId, offset }); + } +} + std::list StatisticsParser::getChannelTypes() { return { diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 3611679..c52514a 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -82,6 +82,13 @@ typedef struct { uint8_t digits; // number of valid digits after the decimal point } byteAssign_t; +typedef struct { + ChannelType_t type; + ChannelNum_t ch; // channel 0 - 4 + FieldId_t fieldId; // field id + float offset; // offset (positive/negative) to be applied on the fetched value +} fieldSettings_t; + class StatisticsParser : public Parser { public: void clearBuffer(); @@ -90,12 +97,17 @@ public: void setByteAssignment(const std::list* byteAssignment); const byteAssign_t* getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + float getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); bool hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); const char* getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); uint8_t getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + float getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + void setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset); + std::list getChannelTypes(); const char* getChannelTypeName(ChannelType_t type); std::list getChannelsByType(ChannelType_t type); @@ -113,6 +125,7 @@ private: uint16_t _stringMaxPower[CH4]; const std::list* _byteAssignment; + std::list _fieldSettings; uint32_t _rxFailureCount = 0; }; \ No newline at end of file diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 47142e7..8ec7a9e 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -100,6 +100,7 @@ bool ConfigurationClass::write() JsonObject chanData = channel.createNestedObject(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; + chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; } } @@ -228,6 +229,7 @@ bool ConfigurationClass::read() JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { config.Inverter[i].channel[c].MaxChannelPower = channel[c]["max_power"] | 0; + config.Inverter[i].channel[c].YieldTotalOffset = channel[c]["yield_total_offset"] | 0.0f; strlcpy(config.Inverter[i].channel[c].Name, channel[c]["name"] | "", sizeof(config.Inverter[i].channel[c].Name)); } } diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 4305c87..77a9c88 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -67,6 +67,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) JsonObject chanData = channel.createNestedObject(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; + chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; } } } @@ -267,6 +268,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) uint8_t arrayCount = 0; for (JsonVariant channel : channelArray) { inverter.channel[arrayCount].MaxChannelPower = channel[F("max_power")].as(); + inverter.channel[arrayCount].YieldTotalOffset = channel[F("yield_total_offset")].as(); strncpy(inverter.channel[arrayCount].Name, channel[F("name")] | "", sizeof(inverter.channel[arrayCount].Name)); arrayCount++; } @@ -297,6 +299,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (inv != nullptr) { 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/src/main.cpp b/src/main.cpp index 56aa65b..273a7bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -150,6 +150,7 @@ void setup() if (inv != nullptr) { 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); } } MessageOutput.println(F(" done")); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index c63fa35..711ffed 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -404,6 +404,8 @@ "StringNameHint": "Hier kann ein eigener Name für den entsprechenden Port des Wechselrichters angegeben werden.", "StringMaxPower": "Max. Leistung String {num}:", "StringMaxPowerHint": "Eingabe der maximalen Leistung der angeschlossenen Solarmodule.", + "StringYtOffset": "Ertragsversatz String {num}:", + "StringYtOffsetHint": "Dieser Offset wird beim Auslesen des Gesamtertragswertes des Wechselrichters angewendet. Damit kann der Gesamtertrag des Wechselrichters auf Null gesetzt werden, wenn ein gebrauchter Wechselrichter verwendet wird.", "InverterHint": "*) Geben Sie die Wp des Ports ein, um die Einstrahlung zu errechnen.", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 655a056..cd8bb55 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -404,6 +404,8 @@ "StringNameHint": "Here you can specify a custom name for the respective port of your inverter.", "StringMaxPower": "Max power string {num}:", "StringMaxPowerHint": "Enter the max power of the connected solar panels.", + "StringYtOffset": "Yield total offset string {num}:", + "StringYtOffsetHint": "This offset is applied the read yield total value from the inverter. This can be used to set the yield total of the inverter to zero if a used inverter is used.", "InverterHint": "*) Enter the Wp of the channel to calculate irradiation.", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index a5ecbc8..f9764a5 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -404,6 +404,8 @@ "StringNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour le port respectif de votre onduleur.", "StringMaxPower": "Puissance maximale de la ligne {num}:", "StringMaxPowerHint": "Entrez la puissance maximale des panneaux solaires connectés.", + "StringYtOffset": "Yield total offset string {num}:", + "StringYtOffsetHint": "This offset is applied the read yield total value from the inverter. This can be used to set the yield total of the inverter to zero if a used inverter is used.", "InverterHint": "*) Entrez le Wp du canal pour calculer l'irradiation.", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index e5c441e..b30f083 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -55,7 +55,7 @@
@@ -163,6 +180,7 @@ import { defineComponent } from 'vue'; declare interface Channel { name: string; max_power: number; + yield_total_offset: number; } declare interface Inverter {