diff --git a/include/Configuration.h b/include/Configuration.h index f1c607d94..62d95c050 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -47,6 +47,7 @@ struct INVERTER_CONFIG_T { bool Command_Enable_Night; uint8_t ReachableThreshold; bool ZeroRuntimeDataIfUnrechable; + bool ZeroYieldDayOnMidnight; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 6b1bd7a4c..62f346c05 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "Hoymiles.h" +#include "Utils.h" #include "inverters/HMS_1CH.h" #include "inverters/HMS_2CH.h" #include "inverters/HMS_4CH.h" @@ -106,6 +107,24 @@ void HoymilesClass::loop() _lastPoll = millis(); } + + // Perform housekeeping of all inverters on day change + int8_t currentWeekDay = Utils::getWeekDay(); + static int8_t lastWeekDay = -1; + if (lastWeekDay == -1) { + lastWeekDay = currentWeekDay; + } else { + if (currentWeekDay != lastWeekDay) { + + for (auto& inv : _inverters) { + if (inv->getZeroYieldDayOnMidnight()) { + inv->Statistics()->zeroDailyData(); + } + } + + lastWeekDay = currentWeekDay; + } + } } } } diff --git a/lib/Hoymiles/src/Utils.cpp b/lib/Hoymiles/src/Utils.cpp new file mode 100644 index 000000000..138e32a16 --- /dev/null +++ b/lib/Hoymiles/src/Utils.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "Utils.h" +#include + +uint8_t Utils::getWeekDay() +{ + time_t raw; + struct tm info; + time(&raw); + localtime_r(&raw, &info); + return info.tm_mday; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/Utils.h b/lib/Hoymiles/src/Utils.h new file mode 100644 index 000000000..157dd75ce --- /dev/null +++ b/lib/Hoymiles/src/Utils.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class Utils { +public: + static uint8_t getWeekDay(); +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 26be98e52..83d128ad7 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -116,6 +116,16 @@ bool InverterAbstract::getZeroValuesIfUnreachable() return _zeroValuesIfUnreachable; } +void InverterAbstract::setZeroYieldDayOnMidnight(bool enabled) +{ + _zeroYieldDayOnMidnight = enabled; +} + +bool InverterAbstract::getZeroYieldDayOnMidnight() +{ + return _zeroYieldDayOnMidnight; +} + bool InverterAbstract::sendChangeChannelRequest() { return false; diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 003ccaa5c..d3b8bf535 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -54,6 +54,9 @@ class InverterAbstract { void setZeroValuesIfUnreachable(bool enabled); bool getZeroValuesIfUnreachable(); + void setZeroYieldDayOnMidnight(bool enabled); + bool getZeroYieldDayOnMidnight(); + void clearRxFragmentBuffer(); void addRxFragment(uint8_t fragment[], uint8_t len); uint8_t verifyAllFragments(CommandAbstract* cmd); @@ -95,6 +98,7 @@ class InverterAbstract { uint8_t _reachableThreshold = 3; bool _zeroValuesIfUnreachable = false; + bool _zeroYieldDayOnMidnight = false; std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index 2c373b645..ad75ccea8 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -55,6 +55,10 @@ const FieldId_t runtimeFields[] = { FLD_IAC_3, }; +const FieldId_t dailyProductionFields[] = { + FLD_YD, +}; + StatisticsParser::StatisticsParser() : Parser() { @@ -317,13 +321,23 @@ uint32_t StatisticsParser::getRxFailureCount() } void StatisticsParser::zeroRuntimeData() +{ + zeroFields(runtimeFields); +} + +void StatisticsParser::zeroDailyData() +{ + zeroFields(dailyProductionFields); +} + +void StatisticsParser::zeroFields(const FieldId_t* fields) { // 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); + if (hasChannelFieldValue(t, c, fields[i])) { + setChannelFieldValue(t, c, fields[i], 0); } } } diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index b50bcdb2d..91cf6456b 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -142,8 +142,11 @@ class StatisticsParser : public Parser { uint32_t getRxFailureCount(); void zeroRuntimeData(); + void zeroDailyData(); private: + void zeroFields(const FieldId_t* fields); + uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {}; uint8_t _statisticLength = 0; uint16_t _stringMaxPower[CH_CNT]; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 4a7979ac6..aacac78e6 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -112,6 +112,7 @@ bool ConfigurationClass::write() inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night; inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold; inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; + inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -262,6 +263,7 @@ bool ConfigurationClass::read() 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; + config.Inverter[i].ZeroYieldDayOnMidnight = inv["zero_day"] | 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 93b5958c8..690f0aca7 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -73,6 +73,7 @@ void InverterSettingsClass::init() if (inv != nullptr) { inv->setReachableThreshold(config.Inverter[i].ReachableThreshold); inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable); + inv->setZeroYieldDayOnMidnight(config.Inverter[i].ZeroYieldDayOnMidnight); 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/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 9ca3679a0..90cadf587 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -94,7 +94,7 @@ void MqttHandleInverterClass::loop() } uint32_t lastUpdate = inv->Statistics()->getLastUpdate(); - if (lastUpdate > 0 && (lastUpdate != _lastPublishStats[i] || (inv->getZeroValuesIfUnreachable() && _statsTimeout.occured()))) { + if (lastUpdate > 0 && (lastUpdate != _lastPublishStats[i] || ((inv->getZeroValuesIfUnreachable() || inv->getZeroYieldDayOnMidnight()) && _statsTimeout.occured()))) { _lastPublishStats[i] = lastUpdate; // At first a change of the stats have to occour. Then the stats diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 458fcf415..d5ea9b45a 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -60,6 +60,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night; obj["reachable_threshold"] = config.Inverter[i].ReachableThreshold; obj["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; + obj["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); uint8_t max_channels; @@ -286,6 +287,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.Command_Enable_Night = root["command_enable_night"] | true; inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; + inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false; arrayCount++; } @@ -318,6 +320,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inv->setEnableCommands(inverter.Command_Enable); inv->setReachableThreshold(inverter.ReachableThreshold); inv->setZeroValuesIfUnreachable(inverter.ZeroRuntimeDataIfUnrechable); + inv->setZeroYieldDayOnMidnight(inverter.ZeroYieldDayOnMidnight); 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 6f042dedc..254be15fc 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -474,6 +474,8 @@ "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.", + "ZeroDay": "Nulle Tagesertrag um Mitternacht", + "ZeroDayHint": "Das funktioniert nur wenn der Wechselrichter nicht erreichbar ist. Wenn Daten aus dem Wechselrichter gelesen werden, werden deren Werte verwendet. (Ein Reset erfolgt nur beim Neustarten)", "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 5da435824..2040e5ac9 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -474,6 +474,8 @@ "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.", + "ZeroDay": "Zero daily yield at midnight", + "ZeroDayHint": "This only works if the inverter is unreachable. If data is read from the inverter, it's values will be used. (Reset only occours on power cycle)", "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 609a2e887..0e7e9df01 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -474,6 +474,8 @@ "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.", + "ZeroDay": "Zero daily yield at midnight", + "ZeroDayHint": "This only works if the inverter is unreachable. If data is read from the inverter, it's values will be used. (Reset only occours on power cycle)", "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 3b2b41fc8..d1f199ad1 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -187,6 +187,11 @@ v-model="selectedInverterData.zero_runtime" type="checkbox" :tooltip="$t('inverteradmin.ZeroRuntimeHint')" wide/> + + @@ -263,6 +268,7 @@ declare interface Inverter { command_enable_night: boolean; reachable_threshold: number; zero_runtime: boolean; + zero_day: boolean; channel: Array; }