From 9521deea2520e0a36bad35cbbea1601860860e96 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Fri, 7 Apr 2023 18:16:26 +0200 Subject: [PATCH 01/30] refactoring calcPowerLimit and setPowerLimit --- src/PowerLimiter.cpp | 95 +++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 539bc11b0..fe6916b56 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -169,6 +169,30 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); + // Safety check, return on too old power meter values + if ((millis() - PowerMeter.getLastPowerMeterUpdate()) > (30 * 1000)) { + // If the power meter values are older than 30 seconds, + // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. + MessageOutput.println("[PowerLimiterClass::loop] Power Meter values too old. Using lower limit"); + return config.PowerLimiter_LowerPowerLimit; + } + + // check if grid power consumption is within the limits of the target consumption + hysteresis + if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && + newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { + // The values have not changed much. We just use the old setting + MessageOutput.println("[PowerLimiterClass::loop] reusing old limit"); + return _lastRequestedPowerLimit; + } + + if (config.PowerLimiter_IsInverterBehindPowerMeter) { + // If the inverter the behind the power meter (part of measurement), + // the produced power of this inverter has also to be taken into account. + // We don't use FLD_PAC from the statistics, because that + // data might be too old and unrelieable. + newPowerLimit += _lastRequestedPowerLimit; + } + float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); int32_t victronChargePower = this->getDirectSolarPower(); int32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0 @@ -176,53 +200,50 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); - // Safety check: Are the power meter values not too old? - if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { - if (config.PowerLimiter_IsInverterBehindPowerMeter) { - // If the inverter the behind the power meter (part of measurement), - // the produced power of this inverter has also to be taken into account. - // We don't use FLD_PAC from the statistics, because that - // data might be too old and unrelieable. - newPowerLimit += _lastRequestedPowerLimit; - } + // We're not trying to hit 0 exactly but take an offset into account + // This means we never fully compensate the used power with the inverter + newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; - newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; + int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; + if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { + // Battery voltage too low, use Victron solar power (corrected by efficency factor) only + upperPowerLimit = adjustedVictronChargePower; + } - int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; - if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { - // Battery voltage too low, use Victron solar power (corrected by efficency factor) only - upperPowerLimit = adjustedVictronChargePower; - } + if (newPowerLimit > upperPowerLimit) + newPowerLimit = upperPowerLimit; - if (newPowerLimit > upperPowerLimit) - newPowerLimit = upperPowerLimit; - } else { - // If the power meter values are older than 30 seconds, - // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. - newPowerLimit = config.PowerLimiter_LowerPowerLimit; - } MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); return newPowerLimit; } void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit) { - if(_lastRequestedPowerLimit != newPowerLimit) { - CONFIG_T& config = Configuration.get(); - - // if limit too low turn inverter offf - if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { - if (inverter->isProducing()) { - MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); - _lastCommandSent = millis(); - } - newPowerLimit = config.PowerLimiter_LowerPowerLimit; - } else if (!inverter->isProducing()) { - MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); + CONFIG_T& config = Configuration.get(); + + // Start the inverter in case it's inactive and if the requested power is high enough + if (!inverter->isProducing() && newPowerLimit > config.PowerLimiter_LowerPowerLimit) { + MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); + _lastCommandSent = millis(); + } + + // Stop the inverter if limit is below threshold. + // We'll also set the power limit to the lower value in this case + if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + if (inverter->isProducing()) { + MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); _lastCommandSent = millis(); - } + } + newPowerLimit = config.PowerLimiter_LowerPowerLimit; + } + + // Set the actual limit. We'll only do this is if the limit is in the right range + // and differs from the last requested value + if( _lastRequestedPowerLimit != newPowerLimit && + /* newPowerLimit > config.PowerLimiter_LowerPowerLimit && --> This will always be true given the check above, kept for code readability */ + newPowerLimit < config.PowerLimiter_UpperPowerLimit ) { MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit); inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); _lastRequestedPowerLimit = newPowerLimit; From f84bdf728715c074d371bfd59aa67a01b340f67f Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Fri, 7 Apr 2023 19:01:00 +0200 Subject: [PATCH 02/30] adding missing statement from merge --- src/PowerLimiter.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 9ea312dc0..1ca1f674b 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -174,7 +174,8 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); // Safety check, return on too old power meter values - if ((millis() - PowerMeter.getLastPowerMeterUpdate()) > (30 * 1000)) { + if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000) + && millis() - inverter->Statistics()->getLastUpdate() < (15 * 1000)) { // If the power meter values are older than 30 seconds, // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. MessageOutput.println("[PowerLimiterClass::loop] Power Meter values too old. Using lower limit"); @@ -193,7 +194,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // If the inverter the behind the power meter (part of measurement), // the produced power of this inverter has also to be taken into account. // We don't use FLD_PAC from the statistics, because that - // data might be too old and unrelieable. + // data might be too old and unreliable. float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); newPowerLimit += static_cast(acPower); } @@ -211,7 +212,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { - // Battery voltage too low, use Victron solar power (corrected by efficency factor) only + // Battery voltage too low, use Victron solar power (corrected by efficiency factor) only upperPowerLimit = adjustedVictronChargePower; } From 734d34b7a878e0f9a20e68399a9bfbefff24611e Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Fri, 7 Apr 2023 19:08:27 +0200 Subject: [PATCH 03/30] make calcPowerLimit respect DTU poll interval --- src/PowerLimiter.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 1ca1f674b..a01ca39d6 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -175,8 +175,9 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // Safety check, return on too old power meter values if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000) - && millis() - inverter->Statistics()->getLastUpdate() < (15 * 1000)) { - // If the power meter values are older than 30 seconds, + && millis() - inverter->Statistics()->getLastUpdate() < (config.Dtu_PollInterval * 3 * 1000)) { + // If the power meter values are older than 30 seconds, + // and the Inverter Stats are older then 3x the poll interval // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. MessageOutput.println("[PowerLimiterClass::loop] Power Meter values too old. Using lower limit"); return config.PowerLimiter_LowerPowerLimit; From 869d8e6d8be6e838529a990dc58e8d06a50fde7e Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Fri, 7 Apr 2023 20:37:26 +0200 Subject: [PATCH 04/30] fixing a bug introduced in merge --- src/PowerLimiter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index a01ca39d6..42c8c966d 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -174,8 +174,8 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); // Safety check, return on too old power meter values - if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000) - && millis() - inverter->Statistics()->getLastUpdate() < (config.Dtu_PollInterval * 3 * 1000)) { + if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000) + && (millis() - inverter->Statistics()->getLastUpdate()) > (config.Dtu_PollInterval * 3 * 1000)) { // If the power meter values are older than 30 seconds, // and the Inverter Stats are older then 3x the poll interval // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. From e3964f8bbe54a91a1b5d9ec5cb9af7b58caf0c9e Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 9 Apr 2023 21:55:45 +0200 Subject: [PATCH 05/30] typo --- include/Configuration.h | 2 +- src/Configuration.cpp | 4 ++-- src/WebApi_powerlimiter.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 2894b3e04..6c62598e5 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -128,7 +128,7 @@ struct CONFIG_T { POWERMETER_HTTP_PHASE_CONFIG_T Powermeter_Http_Phase[POWERMETER_MAX_PHASES]; bool PowerLimiter_Enabled; - bool PowerLimiter_SolarPassTroughEnabled; + bool PowerLimiter_SolarPassThroughEnabled; uint8_t PowerLimiter_BatteryDrainStategy; uint32_t PowerLimiter_Interval; bool PowerLimiter_IsInverterBehindPowerMeter; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 7b6be1715..e18ae12aa 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -140,7 +140,7 @@ bool ConfigurationClass::write() JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); powerlimiter["enabled"] = config.PowerLimiter_Enabled; - powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled; + powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassThroughEnabled; powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy; powerlimiter["interval"] = config.PowerLimiter_Interval; powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter_IsInverterBehindPowerMeter; @@ -329,7 +329,7 @@ bool ConfigurationClass::read() JsonObject powerlimiter = doc["powerlimiter"]; config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; - config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED; + config.PowerLimiter_SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED; config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; config.PowerLimiter_Interval = POWERLIMITER_INTERVAL; config.PowerLimiter_IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index c1015240b..56fb0cf60 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -38,7 +38,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) const CONFIG_T& config = Configuration.get(); root[F("enabled")] = config.PowerLimiter_Enabled; - root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled; + root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassThroughEnabled; root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy; root[F("is_inverter_behind_powermeter")] = config.PowerLimiter_IsInverterBehindPowerMeter; root[F("inverter_id")] = config.PowerLimiter_InverterId; @@ -119,7 +119,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.PowerLimiter_Enabled = root[F("enabled")].as(); - config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as(); + config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passtrough_enabled")].as(); config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); config.PowerLimiter_InverterId = root[F("inverter_id")].as(); From 86ecc62b330d03415dde2a1138846d19b812ae07 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 9 Apr 2023 21:58:41 +0200 Subject: [PATCH 06/30] fixing mppt string for SmartSolar 150/48 --- lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 2b0c8cacf..676b265d7 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -391,7 +391,7 @@ String VeDirectFrameHandler::getPidAsString(uint16_t pid) strPID = "SmartSolar MPPT 100|50"; break; case 0XA058: - strPID = "SmartSolar MPPT 100|35"; + strPID = "SmartSolar MPPT 150|35"; break; case 0XA059: strPID = "SmartSolar MPPT 150|10 rev2"; From 28e204fd80cd140140f068ec0e0953bc3d477235 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 9 Apr 2023 22:06:47 +0200 Subject: [PATCH 07/30] typo --- src/PowerLimiter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 42c8c966d..ba1e13f02 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -154,7 +154,7 @@ bool PowerLimiterClass::canUseDirectSolarPower() { CONFIG_T& config = Configuration.get(); - if (!config.PowerLimiter_SolarPassTroughEnabled + if (!config.PowerLimiter_SolarPassThroughEnabled || !config.Vedirect_Enabled) { return false; } From 406332f6cd8d532fc3e5d5d91a0e791aed3ea4d9 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 9 Apr 2023 23:09:16 +0200 Subject: [PATCH 08/30] fixed linting issues, pushing to github for test run --- webapp/src/views/PowerMeterAdminView.vue | 32 ++++++++---------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index c227afd85..7aee92e82 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -101,6 +101,7 @@ @@ -188,7 +189,7 @@ import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; import InputElement from '@/components/InputElement.vue'; import { handleResponse, authHeader } from '@/utils/authentication'; -import { BIconInfoCircle } from 'bootstrap-icons-vue'; +// import { BIconInfoCircle } from 'bootstrap-icons-vue'; import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig"; export default defineComponent({ @@ -197,20 +198,9 @@ export default defineComponent({ BootstrapAlert, CardElement, InputElement, - BIconInfoCircle, + // BIconInfoCircle, }, data() { - const people: { name: string; age: number; }[] = [ - { - age: 27, - name: 'Tim' - }, - { - age: 28, - name: 'Bob' - } -]; - return { dataLoading: true, powerMeterConfigList: {} as PowerMeterConfig, @@ -223,7 +213,7 @@ export default defineComponent({ alertMessage: "", alertType: "info", showAlert: false, - testHttpRequestAlert: <{ message: string; type: string; show: boolean; }[]> [ ], + testHttpRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[] }; }, created() { @@ -238,14 +228,14 @@ export default defineComponent({ this.powerMeterConfigList = data; this.dataLoading = false; - type MyType = { - id: number; - name: string; - } + // type MyType = { + // id: number; + // name: string; + // } - type MyGroupType = { - [key:string]: MyType; - } + // type MyGroupType = { + // [key:string]: MyType; + // } for (var i = 0; i < this.powerMeterConfigList.http_phases.length; i++) { this.testHttpRequestAlert.push({ From 4bff31e3b1cbc8afda162ab5bbb7e7bd0396bae3 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Wed, 12 Apr 2023 06:45:41 +0200 Subject: [PATCH 09/30] adding option to disable power limiter via mqtt --- README.md | 6 + include/PowerLimiter.h | 3 + src/Configuration.cpp | 850 ++++++++++++++++----------------- src/MqttHandlePowerLimiter.cpp | 83 ++++ src/PowerLimiter.cpp | 12 +- src/WebApi_powerlimiter.cpp | 290 +++++------ 6 files changed, 673 insertions(+), 571 deletions(-) create mode 100644 src/MqttHandlePowerLimiter.cpp diff --git a/README.md b/README.md index 887e5ab37..eae353357 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,12 @@ Topics for 3 phases of a power meter is configurable. Given is an example for th | huawei/output_temp | R | Output air temperature | °C | | huawei/efficiency | R | Efficiency | Percentage | +## Power Limiter topics +| Topic | R / W | Description | Value / Unit | +| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | +| powerlimiter/cmd/disable | W | Power Limiter disable override for external PL control | 0 / 1 | +| powerlimiter/status/disabled | R | Power Limiter disable override status | 0 / 1 | + ## Currently supported Inverters * Hoymiles HM-300 * Hoymiles HM-350 diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 96baaf7c1..6e9357a1f 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -26,6 +26,8 @@ class PowerLimiterClass { void loop(); plStates getPowerLimiterState(); int32_t getLastRequestedPowewrLimit(); + void setDisable(bool disable); + bool getDisable(); private: uint32_t _lastCommandSent = 0; @@ -33,6 +35,7 @@ class PowerLimiterClass { int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; plStates _plState = STATE_DISCOVER; + bool _disabled = false; float _powerMeter1Power; float _powerMeter2Power; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 7b6be1715..dca9921af 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -1,425 +1,425 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ -#include "Configuration.h" -#include "MessageOutput.h" -#include "defaults.h" -#include -#include - -CONFIG_T config; - -void ConfigurationClass::init() -{ - memset(&config, 0x0, sizeof(config)); -} - -bool ConfigurationClass::write() -{ - File f = LittleFS.open(CONFIG_FILENAME, "w"); - if (!f) { - return false; - } - config.Cfg_SaveCount++; - - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - - JsonObject cfg = doc.createNestedObject("cfg"); - cfg["version"] = config.Cfg_Version; - cfg["save_count"] = config.Cfg_SaveCount; - - JsonObject wifi = doc.createNestedObject("wifi"); - wifi["ssid"] = config.WiFi_Ssid; - wifi["password"] = config.WiFi_Password; - wifi["ip"] = IPAddress(config.WiFi_Ip).toString(); - wifi["netmask"] = IPAddress(config.WiFi_Netmask).toString(); - wifi["gateway"] = IPAddress(config.WiFi_Gateway).toString(); - wifi["dns1"] = IPAddress(config.WiFi_Dns1).toString(); - wifi["dns2"] = IPAddress(config.WiFi_Dns2).toString(); - wifi["dhcp"] = config.WiFi_Dhcp; - wifi["hostname"] = config.WiFi_Hostname; - - JsonObject ntp = doc.createNestedObject("ntp"); - ntp["server"] = config.Ntp_Server; - ntp["timezone"] = config.Ntp_Timezone; - ntp["timezone_descr"] = config.Ntp_TimezoneDescr; - ntp["latitude"] = config.Ntp_Latitude; - ntp["longitude"] = config.Ntp_Longitude; - - JsonObject mqtt = doc.createNestedObject("mqtt"); - mqtt["enabled"] = config.Mqtt_Enabled; - mqtt["hostname"] = config.Mqtt_Hostname; - mqtt["port"] = config.Mqtt_Port; - mqtt["username"] = config.Mqtt_Username; - mqtt["password"] = config.Mqtt_Password; - mqtt["topic"] = config.Mqtt_Topic; - mqtt["retain"] = config.Mqtt_Retain; - mqtt["publish_interval"] = config.Mqtt_PublishInterval; - - JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); - mqtt_lwt["topic"] = config.Mqtt_LwtTopic; - mqtt_lwt["value_online"] = config.Mqtt_LwtValue_Online; - mqtt_lwt["value_offline"] = config.Mqtt_LwtValue_Offline; - - JsonObject mqtt_tls = mqtt.createNestedObject("tls"); - mqtt_tls["enabled"] = config.Mqtt_Tls; - mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert; - - JsonObject mqtt_hass = mqtt.createNestedObject("hass"); - mqtt_hass["enabled"] = config.Mqtt_Hass_Enabled; - mqtt_hass["retain"] = config.Mqtt_Hass_Retain; - mqtt_hass["topic"] = config.Mqtt_Hass_Topic; - mqtt_hass["individual_panels"] = config.Mqtt_Hass_IndividualPanels; - mqtt_hass["expire"] = config.Mqtt_Hass_Expire; - - JsonObject dtu = doc.createNestedObject("dtu"); - dtu["serial"] = config.Dtu_Serial; - dtu["poll_interval"] = config.Dtu_PollInterval; - dtu["pa_level"] = config.Dtu_PaLevel; - - JsonObject security = doc.createNestedObject("security"); - security["password"] = config.Security_Password; - security["allow_readonly"] = config.Security_AllowReadonly; - - JsonObject device = doc.createNestedObject("device"); - device["pinmapping"] = config.Dev_PinMapping; - - JsonObject display = device.createNestedObject("display"); - display["powersafe"] = config.Display_PowerSafe; - display["screensaver"] = config.Display_ScreenSaver; - display["rotation"] = config.Display_Rotation; - display["contrast"] = config.Display_Contrast; - - JsonArray inverters = doc.createNestedArray("inverters"); - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - JsonObject inv = inverters.createNestedObject(); - inv["serial"] = config.Inverter[i].Serial; - inv["name"] = config.Inverter[i].Name; - inv["poll_enable"] = config.Inverter[i].Poll_Enable; - inv["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; - inv["command_enable"] = config.Inverter[i].Command_Enable; - inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night; - - JsonArray channel = inv.createNestedArray("channel"); - for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - 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; - } - } - - JsonObject vedirect = doc.createNestedObject("vedirect"); - vedirect["enabled"] = config.Vedirect_Enabled; - vedirect["updates_only"] = config.Vedirect_UpdatesOnly; - vedirect["poll_interval"] = config.Vedirect_PollInterval; - - JsonObject powermeter = doc.createNestedObject("powermeter"); - powermeter["enabled"] = config.PowerMeter_Enabled; - powermeter["interval"] = config.PowerMeter_Interval; - powermeter["source"] = config.PowerMeter_Source; - powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter_MqttTopicPowerMeter1; - powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter_MqttTopicPowerMeter2; - powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter_MqttTopicPowerMeter3; - powermeter["sdmbaudrate"] = config.PowerMeter_SdmBaudrate; - powermeter["sdmaddress"] = config.PowerMeter_SdmAddress; - powermeter["http_individual_requests"] = config.PowerMeter_HttpIndividualRequests; - - JsonArray powermeter_http_phases = powermeter.createNestedArray("http_phases"); - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - JsonObject powermeter_phase = powermeter_http_phases.createNestedObject(); - - powermeter_phase["enabled"] = config.Powermeter_Http_Phase[i].Enabled; - powermeter_phase["url"] = config.Powermeter_Http_Phase[i].Url; - powermeter_phase["header_key"] = config.Powermeter_Http_Phase[i].HeaderKey; - powermeter_phase["header_value"] = config.Powermeter_Http_Phase[i].HeaderValue; - powermeter_phase["timeout"] = config.Powermeter_Http_Phase[i].Timeout; - powermeter_phase["json_path"] = config.Powermeter_Http_Phase[i].JsonPath; - } - - JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); - powerlimiter["enabled"] = config.PowerLimiter_Enabled; - powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled; - powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy; - powerlimiter["interval"] = config.PowerLimiter_Interval; - powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter_IsInverterBehindPowerMeter; - powerlimiter["inverter_id"] = config.PowerLimiter_InverterId; - powerlimiter["inverter_channel_id"] = config.PowerLimiter_InverterChannelId; - powerlimiter["target_power_consumption"] = config.PowerLimiter_TargetPowerConsumption; - powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter_TargetPowerConsumptionHysteresis; - powerlimiter["lower_power_limit"] = config.PowerLimiter_LowerPowerLimit; - powerlimiter["upper_power_limit"] = config.PowerLimiter_UpperPowerLimit; - powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter_BatterySocStartThreshold; - powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter_BatterySocStopThreshold; - powerlimiter["voltage_start_threshold"] = config.PowerLimiter_VoltageStartThreshold; - powerlimiter["voltage_stop_threshold"] = config.PowerLimiter_VoltageStopThreshold; - powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter_VoltageLoadCorrectionFactor; - - JsonObject battery = doc.createNestedObject("battery"); - battery["enabled"] = config.Battery_Enabled; - - JsonObject huawei = doc.createNestedObject("huawei"); - huawei["enabled"] = config.Huawei_Enabled; - - // Serialize JSON to file - if (serializeJson(doc, f) == 0) { - MessageOutput.println("Failed to write file"); - return false; - } - - f.close(); - return true; -} - -bool ConfigurationClass::read() -{ - File f = LittleFS.open(CONFIG_FILENAME, "r", false); - - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); - if (error) { - MessageOutput.println("Failed to read file, using default configuration"); - } - - JsonObject cfg = doc["cfg"]; - config.Cfg_Version = cfg["version"] | CONFIG_VERSION; - config.Cfg_SaveCount = cfg["save_count"] | 0; - - JsonObject wifi = doc["wifi"]; - strlcpy(config.WiFi_Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi_Ssid)); - strlcpy(config.WiFi_Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi_Password)); - strlcpy(config.WiFi_Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi_Hostname)); - - IPAddress wifi_ip; - wifi_ip.fromString(wifi["ip"] | ""); - config.WiFi_Ip[0] = wifi_ip[0]; - config.WiFi_Ip[1] = wifi_ip[1]; - config.WiFi_Ip[2] = wifi_ip[2]; - config.WiFi_Ip[3] = wifi_ip[3]; - - IPAddress wifi_netmask; - wifi_netmask.fromString(wifi["netmask"] | ""); - config.WiFi_Netmask[0] = wifi_netmask[0]; - config.WiFi_Netmask[1] = wifi_netmask[1]; - config.WiFi_Netmask[2] = wifi_netmask[2]; - config.WiFi_Netmask[3] = wifi_netmask[3]; - - IPAddress wifi_gateway; - wifi_gateway.fromString(wifi["gateway"] | ""); - config.WiFi_Gateway[0] = wifi_gateway[0]; - config.WiFi_Gateway[1] = wifi_gateway[1]; - config.WiFi_Gateway[2] = wifi_gateway[2]; - config.WiFi_Gateway[3] = wifi_gateway[3]; - - IPAddress wifi_dns1; - wifi_dns1.fromString(wifi["dns1"] | ""); - config.WiFi_Dns1[0] = wifi_dns1[0]; - config.WiFi_Dns1[1] = wifi_dns1[1]; - config.WiFi_Dns1[2] = wifi_dns1[2]; - config.WiFi_Dns1[3] = wifi_dns1[3]; - - IPAddress wifi_dns2; - wifi_dns2.fromString(wifi["dns2"] | ""); - config.WiFi_Dns2[0] = wifi_dns2[0]; - config.WiFi_Dns2[1] = wifi_dns2[1]; - config.WiFi_Dns2[2] = wifi_dns2[2]; - config.WiFi_Dns2[3] = wifi_dns2[3]; - - config.WiFi_Dhcp = wifi["dhcp"] | WIFI_DHCP; - - JsonObject ntp = doc["ntp"]; - strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server)); - strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); - strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); - config.Ntp_Latitude = ntp["latitude"] | NTP_LATITUDE; - config.Ntp_Longitude = ntp["longitude"] | NTP_LONGITUDE; - - JsonObject mqtt = doc["mqtt"]; - config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED; - strlcpy(config.Mqtt_Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt_Hostname)); - config.Mqtt_Port = mqtt["port"] | MQTT_PORT; - strlcpy(config.Mqtt_Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt_Username)); - strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password)); - strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); - config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; - config.Mqtt_PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; - - JsonObject mqtt_lwt = mqtt["lwt"]; - strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); - strlcpy(config.Mqtt_LwtValue_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); - strlcpy(config.Mqtt_LwtValue_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); - - JsonObject mqtt_tls = mqtt["tls"]; - config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS; - strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); - - JsonObject mqtt_hass = mqtt["hass"]; - config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; - config.Mqtt_Hass_Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN; - config.Mqtt_Hass_Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE; - config.Mqtt_Hass_IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS; - strlcpy(config.Mqtt_Hass_Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); - - JsonObject dtu = doc["dtu"]; - config.Dtu_Serial = dtu["serial"] | DTU_SERIAL; - config.Dtu_PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; - config.Dtu_PaLevel = dtu["pa_level"] | DTU_PA_LEVEL; - - JsonObject security = doc["security"]; - strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); - config.Security_AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; - - JsonObject device = doc["device"]; - strlcpy(config.Dev_PinMapping, device["pinmapping"] | DEV_PINMAPPING, sizeof(config.Dev_PinMapping)); - - JsonObject display = device["display"]; - config.Display_PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE; - config.Display_ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; - config.Display_Rotation = display["rotation"] | DISPLAY_ROTATION; - config.Display_Contrast = display["contrast"] | DISPLAY_CONTRAST; - - JsonArray inverters = doc["inverters"]; - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - JsonObject inv = inverters[i].as(); - config.Inverter[i].Serial = inv["serial"] | 0ULL; - strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); - - config.Inverter[i].Poll_Enable = inv["poll_enable"] | true; - config.Inverter[i].Poll_Enable_Night = inv["poll_enable_night"] | true; - config.Inverter[i].Command_Enable = inv["command_enable"] | true; - config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true; - - 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)); - } - } - - JsonObject vedirect = doc["vedirect"]; - config.Vedirect_Enabled = vedirect["enabled"] | VEDIRECT_ENABLED; - config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; - config.Vedirect_PollInterval = vedirect["poll_interval"] | VEDIRECT_POLL_INTERVAL; - - JsonObject powermeter = doc["powermeter"]; - config.PowerMeter_Enabled = powermeter["enabled"] | POWERMETER_ENABLED; - config.PowerMeter_Interval = powermeter["interval"] | POWERMETER_INTERVAL; - config.PowerMeter_Source = powermeter["source"] | POWERMETER_SOURCE; - strlcpy(config.PowerMeter_MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter1)); - strlcpy(config.PowerMeter_MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter2)); - strlcpy(config.PowerMeter_MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter3)); - config.PowerMeter_SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE; - config.PowerMeter_SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; - config.PowerMeter_HttpIndividualRequests = powermeter["http_individual_requests"] | false; - - JsonArray powermeter_http_phases = powermeter["http_phases"]; - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - JsonObject powermeter_phase = powermeter_http_phases[i].as(); - - config.Powermeter_Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); - strlcpy(config.Powermeter_Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.Powermeter_Http_Phase[i].Url)); - strlcpy(config.Powermeter_Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderKey)); - strlcpy(config.Powermeter_Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderValue)); - config.Powermeter_Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT; - strlcpy(config.Powermeter_Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.Powermeter_Http_Phase[i].JsonPath)); - } - - JsonObject powerlimiter = doc["powerlimiter"]; - config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; - config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED; - config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; - config.PowerLimiter_Interval = POWERLIMITER_INTERVAL; - config.PowerLimiter_IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; - config.PowerLimiter_InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; - config.PowerLimiter_InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; - config.PowerLimiter_TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; - config.PowerLimiter_TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS; - config.PowerLimiter_LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; - config.PowerLimiter_UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; - config.PowerLimiter_BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD; - config.PowerLimiter_BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD; - config.PowerLimiter_VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD; - config.PowerLimiter_VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD; - config.PowerLimiter_VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR; - - JsonObject battery = doc["battery"]; - config.Battery_Enabled = battery["enabled"] | BATTERY_ENABLED; - - JsonObject huawei = doc["huawei"]; - config.Huawei_Enabled = huawei["enabled"] | HUAWEI_ENABLED; - - f.close(); - return true; -} - -void ConfigurationClass::migrate() -{ - File f = LittleFS.open(CONFIG_FILENAME, "r", false); - if (!f) { - MessageOutput.println("Failed to open file, cancel migration"); - return; - } - - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); - if (error) { - MessageOutput.printf("Failed to read file, cancel migration: %s\r\n", error.c_str()); - return; - } - - if (config.Cfg_Version < 0x00011700) { - JsonArray inverters = doc["inverters"]; - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - JsonObject inv = inverters[i].as(); - JsonArray channels = inv["channels"]; - for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - config.Inverter[i].channel[c].MaxChannelPower = channels[c]; - strlcpy(config.Inverter[i].channel[c].Name, "", sizeof(config.Inverter[i].channel[c].Name)); - } - } - } - - if (config.Cfg_Version < 0x00011800) { - JsonObject mqtt = doc["mqtt"]; - config.Mqtt_PublishInterval = mqtt["publish_invterval"]; - } - - f.close(); - - config.Cfg_Version = CONFIG_VERSION; - write(); - read(); -} - -CONFIG_T& ConfigurationClass::get() -{ - return config; -} - -INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() -{ - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - if (config.Inverter[i].Serial == 0) { - return &config.Inverter[i]; - } - } - - return NULL; -} - -INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial) -{ - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - if (config.Inverter[i].Serial == serial) { - return &config.Inverter[i]; - } - } - - return NULL; -} - -ConfigurationClass Configuration; +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "Configuration.h" +#include "MessageOutput.h" +#include "defaults.h" +#include +#include + +CONFIG_T config; + +void ConfigurationClass::init() +{ + memset(&config, 0x0, sizeof(config)); +} + +bool ConfigurationClass::write() +{ + File f = LittleFS.open(CONFIG_FILENAME, "w"); + if (!f) { + return false; + } + config.Cfg_SaveCount++; + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + + JsonObject cfg = doc.createNestedObject("cfg"); + cfg["version"] = config.Cfg_Version; + cfg["save_count"] = config.Cfg_SaveCount; + + JsonObject wifi = doc.createNestedObject("wifi"); + wifi["ssid"] = config.WiFi_Ssid; + wifi["password"] = config.WiFi_Password; + wifi["ip"] = IPAddress(config.WiFi_Ip).toString(); + wifi["netmask"] = IPAddress(config.WiFi_Netmask).toString(); + wifi["gateway"] = IPAddress(config.WiFi_Gateway).toString(); + wifi["dns1"] = IPAddress(config.WiFi_Dns1).toString(); + wifi["dns2"] = IPAddress(config.WiFi_Dns2).toString(); + wifi["dhcp"] = config.WiFi_Dhcp; + wifi["hostname"] = config.WiFi_Hostname; + + JsonObject ntp = doc.createNestedObject("ntp"); + ntp["server"] = config.Ntp_Server; + ntp["timezone"] = config.Ntp_Timezone; + ntp["timezone_descr"] = config.Ntp_TimezoneDescr; + ntp["latitude"] = config.Ntp_Latitude; + ntp["longitude"] = config.Ntp_Longitude; + + JsonObject mqtt = doc.createNestedObject("mqtt"); + mqtt["enabled"] = config.Mqtt_Enabled; + mqtt["hostname"] = config.Mqtt_Hostname; + mqtt["port"] = config.Mqtt_Port; + mqtt["username"] = config.Mqtt_Username; + mqtt["password"] = config.Mqtt_Password; + mqtt["topic"] = config.Mqtt_Topic; + mqtt["retain"] = config.Mqtt_Retain; + mqtt["publish_interval"] = config.Mqtt_PublishInterval; + + JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); + mqtt_lwt["topic"] = config.Mqtt_LwtTopic; + mqtt_lwt["value_online"] = config.Mqtt_LwtValue_Online; + mqtt_lwt["value_offline"] = config.Mqtt_LwtValue_Offline; + + JsonObject mqtt_tls = mqtt.createNestedObject("tls"); + mqtt_tls["enabled"] = config.Mqtt_Tls; + mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert; + + JsonObject mqtt_hass = mqtt.createNestedObject("hass"); + mqtt_hass["enabled"] = config.Mqtt_Hass_Enabled; + mqtt_hass["retain"] = config.Mqtt_Hass_Retain; + mqtt_hass["topic"] = config.Mqtt_Hass_Topic; + mqtt_hass["individual_panels"] = config.Mqtt_Hass_IndividualPanels; + mqtt_hass["expire"] = config.Mqtt_Hass_Expire; + + JsonObject dtu = doc.createNestedObject("dtu"); + dtu["serial"] = config.Dtu_Serial; + dtu["poll_interval"] = config.Dtu_PollInterval; + dtu["pa_level"] = config.Dtu_PaLevel; + + JsonObject security = doc.createNestedObject("security"); + security["password"] = config.Security_Password; + security["allow_readonly"] = config.Security_AllowReadonly; + + JsonObject device = doc.createNestedObject("device"); + device["pinmapping"] = config.Dev_PinMapping; + + JsonObject display = device.createNestedObject("display"); + display["powersafe"] = config.Display_PowerSafe; + display["screensaver"] = config.Display_ScreenSaver; + display["rotation"] = config.Display_Rotation; + display["contrast"] = config.Display_Contrast; + + JsonArray inverters = doc.createNestedArray("inverters"); + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters.createNestedObject(); + inv["serial"] = config.Inverter[i].Serial; + inv["name"] = config.Inverter[i].Name; + inv["poll_enable"] = config.Inverter[i].Poll_Enable; + inv["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; + inv["command_enable"] = config.Inverter[i].Command_Enable; + inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night; + + JsonArray channel = inv.createNestedArray("channel"); + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + 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; + } + } + + JsonObject vedirect = doc.createNestedObject("vedirect"); + vedirect["enabled"] = config.Vedirect_Enabled; + vedirect["updates_only"] = config.Vedirect_UpdatesOnly; + vedirect["poll_interval"] = config.Vedirect_PollInterval; + + JsonObject powermeter = doc.createNestedObject("powermeter"); + powermeter["enabled"] = config.PowerMeter_Enabled; + powermeter["interval"] = config.PowerMeter_Interval; + powermeter["source"] = config.PowerMeter_Source; + powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter_MqttTopicPowerMeter1; + powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter_MqttTopicPowerMeter2; + powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter_MqttTopicPowerMeter3; + powermeter["sdmbaudrate"] = config.PowerMeter_SdmBaudrate; + powermeter["sdmaddress"] = config.PowerMeter_SdmAddress; + powermeter["http_individual_requests"] = config.PowerMeter_HttpIndividualRequests; + + JsonArray powermeter_http_phases = powermeter.createNestedArray("http_phases"); + for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { + JsonObject powermeter_phase = powermeter_http_phases.createNestedObject(); + + powermeter_phase["enabled"] = config.Powermeter_Http_Phase[i].Enabled; + powermeter_phase["url"] = config.Powermeter_Http_Phase[i].Url; + powermeter_phase["header_key"] = config.Powermeter_Http_Phase[i].HeaderKey; + powermeter_phase["header_value"] = config.Powermeter_Http_Phase[i].HeaderValue; + powermeter_phase["timeout"] = config.Powermeter_Http_Phase[i].Timeout; + powermeter_phase["json_path"] = config.Powermeter_Http_Phase[i].JsonPath; + } + + JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); + powerlimiter["enabled"] = config.PowerLimiter_Enabled; + powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled; + powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy; + powerlimiter["interval"] = config.PowerLimiter_Interval; + powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter_IsInverterBehindPowerMeter; + powerlimiter["inverter_id"] = config.PowerLimiter_InverterId; + powerlimiter["inverter_channel_id"] = config.PowerLimiter_InverterChannelId; + powerlimiter["target_power_consumption"] = config.PowerLimiter_TargetPowerConsumption; + powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter_TargetPowerConsumptionHysteresis; + powerlimiter["lower_power_limit"] = config.PowerLimiter_LowerPowerLimit; + powerlimiter["upper_power_limit"] = config.PowerLimiter_UpperPowerLimit; + powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter_BatterySocStartThreshold; + powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter_BatterySocStopThreshold; + powerlimiter["voltage_start_threshold"] = config.PowerLimiter_VoltageStartThreshold; + powerlimiter["voltage_stop_threshold"] = config.PowerLimiter_VoltageStopThreshold; + powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter_VoltageLoadCorrectionFactor; + + JsonObject battery = doc.createNestedObject("battery"); + battery["enabled"] = config.Battery_Enabled; + + JsonObject huawei = doc.createNestedObject("huawei"); + huawei["enabled"] = config.Huawei_Enabled; + + // Serialize JSON to file + if (serializeJson(doc, f) == 0) { + MessageOutput.println("Failed to write file"); + return false; + } + + f.close(); + return true; +} + +bool ConfigurationClass::read() +{ + File f = LittleFS.open(CONFIG_FILENAME, "r", false); + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + MessageOutput.println("Failed to read file, using default configuration"); + } + + JsonObject cfg = doc["cfg"]; + config.Cfg_Version = cfg["version"] | CONFIG_VERSION; + config.Cfg_SaveCount = cfg["save_count"] | 0; + + JsonObject wifi = doc["wifi"]; + strlcpy(config.WiFi_Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi_Ssid)); + strlcpy(config.WiFi_Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi_Password)); + strlcpy(config.WiFi_Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi_Hostname)); + + IPAddress wifi_ip; + wifi_ip.fromString(wifi["ip"] | ""); + config.WiFi_Ip[0] = wifi_ip[0]; + config.WiFi_Ip[1] = wifi_ip[1]; + config.WiFi_Ip[2] = wifi_ip[2]; + config.WiFi_Ip[3] = wifi_ip[3]; + + IPAddress wifi_netmask; + wifi_netmask.fromString(wifi["netmask"] | ""); + config.WiFi_Netmask[0] = wifi_netmask[0]; + config.WiFi_Netmask[1] = wifi_netmask[1]; + config.WiFi_Netmask[2] = wifi_netmask[2]; + config.WiFi_Netmask[3] = wifi_netmask[3]; + + IPAddress wifi_gateway; + wifi_gateway.fromString(wifi["gateway"] | ""); + config.WiFi_Gateway[0] = wifi_gateway[0]; + config.WiFi_Gateway[1] = wifi_gateway[1]; + config.WiFi_Gateway[2] = wifi_gateway[2]; + config.WiFi_Gateway[3] = wifi_gateway[3]; + + IPAddress wifi_dns1; + wifi_dns1.fromString(wifi["dns1"] | ""); + config.WiFi_Dns1[0] = wifi_dns1[0]; + config.WiFi_Dns1[1] = wifi_dns1[1]; + config.WiFi_Dns1[2] = wifi_dns1[2]; + config.WiFi_Dns1[3] = wifi_dns1[3]; + + IPAddress wifi_dns2; + wifi_dns2.fromString(wifi["dns2"] | ""); + config.WiFi_Dns2[0] = wifi_dns2[0]; + config.WiFi_Dns2[1] = wifi_dns2[1]; + config.WiFi_Dns2[2] = wifi_dns2[2]; + config.WiFi_Dns2[3] = wifi_dns2[3]; + + config.WiFi_Dhcp = wifi["dhcp"] | WIFI_DHCP; + + JsonObject ntp = doc["ntp"]; + strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server)); + strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); + strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); + config.Ntp_Latitude = ntp["latitude"] | NTP_LATITUDE; + config.Ntp_Longitude = ntp["longitude"] | NTP_LONGITUDE; + + JsonObject mqtt = doc["mqtt"]; + config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED; + strlcpy(config.Mqtt_Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt_Hostname)); + config.Mqtt_Port = mqtt["port"] | MQTT_PORT; + strlcpy(config.Mqtt_Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt_Username)); + strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password)); + strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); + config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; + config.Mqtt_PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; + + JsonObject mqtt_lwt = mqtt["lwt"]; + strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); + strlcpy(config.Mqtt_LwtValue_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); + strlcpy(config.Mqtt_LwtValue_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); + + JsonObject mqtt_tls = mqtt["tls"]; + config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS; + strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); + + JsonObject mqtt_hass = mqtt["hass"]; + config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; + config.Mqtt_Hass_Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN; + config.Mqtt_Hass_Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE; + config.Mqtt_Hass_IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS; + strlcpy(config.Mqtt_Hass_Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); + + JsonObject dtu = doc["dtu"]; + config.Dtu_Serial = dtu["serial"] | DTU_SERIAL; + config.Dtu_PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; + config.Dtu_PaLevel = dtu["pa_level"] | DTU_PA_LEVEL; + + JsonObject security = doc["security"]; + strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); + config.Security_AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; + + JsonObject device = doc["device"]; + strlcpy(config.Dev_PinMapping, device["pinmapping"] | DEV_PINMAPPING, sizeof(config.Dev_PinMapping)); + + JsonObject display = device["display"]; + config.Display_PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE; + config.Display_ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; + config.Display_Rotation = display["rotation"] | DISPLAY_ROTATION; + config.Display_Contrast = display["contrast"] | DISPLAY_CONTRAST; + + JsonArray inverters = doc["inverters"]; + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters[i].as(); + config.Inverter[i].Serial = inv["serial"] | 0ULL; + strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); + + config.Inverter[i].Poll_Enable = inv["poll_enable"] | true; + config.Inverter[i].Poll_Enable_Night = inv["poll_enable_night"] | true; + config.Inverter[i].Command_Enable = inv["command_enable"] | true; + config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true; + + 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)); + } + } + + JsonObject vedirect = doc["vedirect"]; + config.Vedirect_Enabled = vedirect["enabled"] | VEDIRECT_ENABLED; + config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; + config.Vedirect_PollInterval = vedirect["poll_interval"] | VEDIRECT_POLL_INTERVAL; + + JsonObject powermeter = doc["powermeter"]; + config.PowerMeter_Enabled = powermeter["enabled"] | POWERMETER_ENABLED; + config.PowerMeter_Interval = powermeter["interval"] | POWERMETER_INTERVAL; + config.PowerMeter_Source = powermeter["source"] | POWERMETER_SOURCE; + strlcpy(config.PowerMeter_MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter1)); + strlcpy(config.PowerMeter_MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter2)); + strlcpy(config.PowerMeter_MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter3)); + config.PowerMeter_SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE; + config.PowerMeter_SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; + config.PowerMeter_HttpIndividualRequests = powermeter["http_individual_requests"] | false; + + JsonArray powermeter_http_phases = powermeter["http_phases"]; + for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { + JsonObject powermeter_phase = powermeter_http_phases[i].as(); + + config.Powermeter_Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); + strlcpy(config.Powermeter_Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.Powermeter_Http_Phase[i].Url)); + strlcpy(config.Powermeter_Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderKey)); + strlcpy(config.Powermeter_Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderValue)); + config.Powermeter_Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT; + strlcpy(config.Powermeter_Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.Powermeter_Http_Phase[i].JsonPath)); + } + + JsonObject powerlimiter = doc["powerlimiter"]; + config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; + config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED; + config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; + config.PowerLimiter_Interval = POWERLIMITER_INTERVAL; + config.PowerLimiter_IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; + config.PowerLimiter_InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; + config.PowerLimiter_InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; + config.PowerLimiter_TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; + config.PowerLimiter_TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS; + config.PowerLimiter_LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; + config.PowerLimiter_UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; + config.PowerLimiter_BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD; + config.PowerLimiter_BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD; + config.PowerLimiter_VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD; + config.PowerLimiter_VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD; + config.PowerLimiter_VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR; + + JsonObject battery = doc["battery"]; + config.Battery_Enabled = battery["enabled"] | BATTERY_ENABLED; + + JsonObject huawei = doc["huawei"]; + config.Huawei_Enabled = huawei["enabled"] | HUAWEI_ENABLED; + + f.close(); + return true; +} + +void ConfigurationClass::migrate() +{ + File f = LittleFS.open(CONFIG_FILENAME, "r", false); + if (!f) { + MessageOutput.println("Failed to open file, cancel migration"); + return; + } + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + MessageOutput.printf("Failed to read file, cancel migration: %s\r\n", error.c_str()); + return; + } + + if (config.Cfg_Version < 0x00011700) { + JsonArray inverters = doc["inverters"]; + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters[i].as(); + JsonArray channels = inv["channels"]; + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + config.Inverter[i].channel[c].MaxChannelPower = channels[c]; + strlcpy(config.Inverter[i].channel[c].Name, "", sizeof(config.Inverter[i].channel[c].Name)); + } + } + } + + if (config.Cfg_Version < 0x00011800) { + JsonObject mqtt = doc["mqtt"]; + config.Mqtt_PublishInterval = mqtt["publish_invterval"]; + } + + f.close(); + + config.Cfg_Version = CONFIG_VERSION; + write(); + read(); +} + +CONFIG_T& ConfigurationClass::get() +{ + return config; +} + +INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() +{ + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial == 0) { + return &config.Inverter[i]; + } + } + + return NULL; +} + +INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial) +{ + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial == serial) { + return &config.Inverter[i]; + } + } + + return NULL; +} + +ConfigurationClass Configuration; diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp new file mode 100644 index 000000000..2830d420d --- /dev/null +++ b/src/MqttHandlePowerLimiter.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler, Malte Schmidt and others + */ +#include "MessageOutput.h" +#include "MqttSettings.h" +#include "MqttHandlePowerLimiter.h" +#include "PowerLimiter.h" +#include + +#define TOPIC_SUB_POWER_LIMITER "disable" + +MqttHandlePowerLimiterClass MqttHandlePowerLimiter; + +void MqttHandlePowerLimiterClass::init() +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + String topic = MqttSettings.getPrefix(); + MqttSettings.subscribe(String(topic + "powerlimiter/cmd/" + TOPIC_SUB_POWER_LIMITER).c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + + _lastPublish = millis(); + +} + + +void MqttHandlePowerLimiterClass::loop() +{ + if (!MqttSettings.getConnected() ) { + return; + } + + const CONFIG_T& config = Configuration.get(); + + if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) { + MqttSettings.publish("powerlimiter/status/disabled", String(PowerLimiter.getDisable())); + + yield(); + _lastPublish = millis(); + } +} + + +void MqttHandlePowerLimiterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +{ + const CONFIG_T& config = Configuration.get(); + + char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics + strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* + + char* setting; + char* rest = &token_topic[strlen(config.Mqtt_Topic)]; + + strtok_r(rest, "/", &rest); // Remove "powerlimiter" + strtok_r(rest, "/", &rest); // Remove "cmd" + + setting = strtok_r(rest, "/", &rest); + + if (setting == NULL) { + return; + } + + char* strlimit = new char[len + 1]; + memcpy(strlimit, payload, len); + strlimit[len] = '\0'; + float payload_val = strtof(strlimit, NULL); + delete[] strlimit; + + if (!strcmp(setting, TOPIC_SUB_POWER_LIMITER)) { + MessageOutput.printf("Disable power limter: %f A\r\n", payload_val); + if(payload_val == 1) { + PowerLimiter.setDisable(true); + } + if(payload_val == 0) { + PowerLimiter.setDisable(false); + } + } +} \ No newline at end of file diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 6cde83ddb..38742324e 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -24,11 +24,13 @@ void PowerLimiterClass::loop() CONFIG_T& config = Configuration.get(); if (!config.PowerLimiter_Enabled + || _disabled || !config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { - if (!config.PowerLimiter_Enabled) + if (!config.PowerLimiter_Enabled + || _disabled) _plState = STATE_DISCOVER; // ensure STATE_DISCOVER is set, if PowerLimiter will be enabled. return; } @@ -150,6 +152,14 @@ int32_t PowerLimiterClass::getLastRequestedPowewrLimit() { return _lastRequestedPowerLimit; } +bool PowerLimiterClass::getDisable() { + return _disabled; +} + +void PowerLimiterClass::setDisable(bool disable) { + _disabled = disable; +} + bool PowerLimiterClass::canUseDirectSolarPower() { CONFIG_T& config = Configuration.get(); diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index c1015240b..dad80b7b2 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -1,145 +1,145 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ -#include "WebApi_powerlimiter.h" -#include "VeDirectFrameHandler.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" -#include "Configuration.h" -#include "MqttHandleHass.h" -#include "MqttHandleVedirectHass.h" -#include "MqttSettings.h" -#include "PowerMeter.h" -#include "PowerLimiter.h" -#include "WebApi.h" -#include "helper.h" -#include "WebApi_errors.h" - -void WebApiPowerLimiterClass::init(AsyncWebServer* server) -{ - using std::placeholders::_1; - - _server = server; - - _server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1)); - _server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1)); - _server->on("/api/powerlimiter/config", HTTP_POST, std::bind(&WebApiPowerLimiterClass::onAdminPost, this, _1)); -} - -void WebApiPowerLimiterClass::loop() -{ -} - -void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - const CONFIG_T& config = Configuration.get(); - - root[F("enabled")] = config.PowerLimiter_Enabled; - root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled; - root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy; - root[F("is_inverter_behind_powermeter")] = config.PowerLimiter_IsInverterBehindPowerMeter; - root[F("inverter_id")] = config.PowerLimiter_InverterId; - root[F("inverter_channel_id")] = config.PowerLimiter_InverterChannelId; - root[F("target_power_consumption")] = config.PowerLimiter_TargetPowerConsumption; - root[F("target_power_consumption_hysteresis")] = config.PowerLimiter_TargetPowerConsumptionHysteresis; - root[F("lower_power_limit")] = config.PowerLimiter_LowerPowerLimit; - root[F("upper_power_limit")] = config.PowerLimiter_UpperPowerLimit; - root[F("battery_soc_start_threshold")] = config.PowerLimiter_BatterySocStartThreshold; - root[F("battery_soc_stop_threshold")] = config.PowerLimiter_BatterySocStopThreshold; - root[F("voltage_start_threshold")] = static_cast(config.PowerLimiter_VoltageStartThreshold * 100 +0.5) / 100.0; - root[F("voltage_stop_threshold")] = static_cast(config.PowerLimiter_VoltageStopThreshold * 100 +0.5) / 100.0;; - root[F("voltage_load_correction_factor")] = config.PowerLimiter_VoltageLoadCorrectionFactor; - - response->setLength(); - request->send(response); -} - -void WebApiPowerLimiterClass::onAdminGet(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentials(request)) { - return; - } - - this->onStatus(request); -} - -void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentials(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("enabled") - && root.containsKey("lower_power_limit") - && root.containsKey("inverter_id") - && root.containsKey("inverter_channel_id") - && root.containsKey("target_power_consumption") - && root.containsKey("target_power_consumption_hysteresis") - )) { - retMsg[F("message")] = F("Values are missing!"); - retMsg[F("code")] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); - return; - } - - - CONFIG_T& config = Configuration.get(); - config.PowerLimiter_Enabled = root[F("enabled")].as(); - config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as(); - config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); - config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); - config.PowerLimiter_InverterId = root[F("inverter_id")].as(); - config.PowerLimiter_InverterChannelId = root[F("inverter_channel_id")].as(); - config.PowerLimiter_TargetPowerConsumption = root[F("target_power_consumption")].as(); - config.PowerLimiter_TargetPowerConsumptionHysteresis = root[F("target_power_consumption_hysteresis")].as(); - config.PowerLimiter_LowerPowerLimit = root[F("lower_power_limit")].as(); - config.PowerLimiter_UpperPowerLimit = root[F("upper_power_limit")].as(); - config.PowerLimiter_BatterySocStartThreshold = root[F("battery_soc_start_threshold")].as(); - config.PowerLimiter_BatterySocStopThreshold = root[F("battery_soc_stop_threshold")].as(); - config.PowerLimiter_VoltageStartThreshold = root[F("voltage_start_threshold")].as(); - config.PowerLimiter_VoltageStartThreshold = static_cast(config.PowerLimiter_VoltageStartThreshold * 100) / 100.0; - config.PowerLimiter_VoltageStopThreshold = root[F("voltage_stop_threshold")].as(); - config.PowerLimiter_VoltageStopThreshold = static_cast(config.PowerLimiter_VoltageStopThreshold * 100) / 100.0; - config.PowerLimiter_VoltageLoadCorrectionFactor = root[F("voltage_load_correction_factor")].as(); - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Settings saved!"); - - response->setLength(); - request->send(response); -} +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_powerlimiter.h" +#include "VeDirectFrameHandler.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "MqttHandleHass.h" +#include "MqttHandleVedirectHass.h" +#include "MqttSettings.h" +#include "PowerMeter.h" +#include "PowerLimiter.h" +#include "WebApi.h" +#include "helper.h" +#include "WebApi_errors.h" + +void WebApiPowerLimiterClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1)); + _server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1)); + _server->on("/api/powerlimiter/config", HTTP_POST, std::bind(&WebApiPowerLimiterClass::onAdminPost, this, _1)); +} + +void WebApiPowerLimiterClass::loop() +{ +} + +void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + const CONFIG_T& config = Configuration.get(); + + root[F("enabled")] = config.PowerLimiter_Enabled; + root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled; + root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy; + root[F("is_inverter_behind_powermeter")] = config.PowerLimiter_IsInverterBehindPowerMeter; + root[F("inverter_id")] = config.PowerLimiter_InverterId; + root[F("inverter_channel_id")] = config.PowerLimiter_InverterChannelId; + root[F("target_power_consumption")] = config.PowerLimiter_TargetPowerConsumption; + root[F("target_power_consumption_hysteresis")] = config.PowerLimiter_TargetPowerConsumptionHysteresis; + root[F("lower_power_limit")] = config.PowerLimiter_LowerPowerLimit; + root[F("upper_power_limit")] = config.PowerLimiter_UpperPowerLimit; + root[F("battery_soc_start_threshold")] = config.PowerLimiter_BatterySocStartThreshold; + root[F("battery_soc_stop_threshold")] = config.PowerLimiter_BatterySocStopThreshold; + root[F("voltage_start_threshold")] = static_cast(config.PowerLimiter_VoltageStartThreshold * 100 +0.5) / 100.0; + root[F("voltage_stop_threshold")] = static_cast(config.PowerLimiter_VoltageStopThreshold * 100 +0.5) / 100.0;; + root[F("voltage_load_correction_factor")] = config.PowerLimiter_VoltageLoadCorrectionFactor; + + response->setLength(); + request->send(response); +} + +void WebApiPowerLimiterClass::onAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + this->onStatus(request); +} + +void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("enabled") + && root.containsKey("lower_power_limit") + && root.containsKey("inverter_id") + && root.containsKey("inverter_channel_id") + && root.containsKey("target_power_consumption") + && root.containsKey("target_power_consumption_hysteresis") + )) { + retMsg[F("message")] = F("Values are missing!"); + retMsg[F("code")] = WebApiError::GenericValueMissing; + response->setLength(); + request->send(response); + return; + } + + + CONFIG_T& config = Configuration.get(); + config.PowerLimiter_Enabled = root[F("enabled")].as(); + config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as(); + config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); + config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); + config.PowerLimiter_InverterId = root[F("inverter_id")].as(); + config.PowerLimiter_InverterChannelId = root[F("inverter_channel_id")].as(); + config.PowerLimiter_TargetPowerConsumption = root[F("target_power_consumption")].as(); + config.PowerLimiter_TargetPowerConsumptionHysteresis = root[F("target_power_consumption_hysteresis")].as(); + config.PowerLimiter_LowerPowerLimit = root[F("lower_power_limit")].as(); + config.PowerLimiter_UpperPowerLimit = root[F("upper_power_limit")].as(); + config.PowerLimiter_BatterySocStartThreshold = root[F("battery_soc_start_threshold")].as(); + config.PowerLimiter_BatterySocStopThreshold = root[F("battery_soc_stop_threshold")].as(); + config.PowerLimiter_VoltageStartThreshold = root[F("voltage_start_threshold")].as(); + config.PowerLimiter_VoltageStartThreshold = static_cast(config.PowerLimiter_VoltageStartThreshold * 100) / 100.0; + config.PowerLimiter_VoltageStopThreshold = root[F("voltage_stop_threshold")].as(); + config.PowerLimiter_VoltageStopThreshold = static_cast(config.PowerLimiter_VoltageStopThreshold * 100) / 100.0; + config.PowerLimiter_VoltageLoadCorrectionFactor = root[F("voltage_load_correction_factor")].as(); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); +} From b6edc11eb2e72f575fdd19b962e4883c2129b90d Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Wed, 12 Apr 2023 06:46:38 +0200 Subject: [PATCH 10/30] adding option to disable power limiter via mqtt - adding missing file --- include/MqttHandlePowerLimiter.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 include/MqttHandlePowerLimiter.h diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h new file mode 100644 index 000000000..82d736ea6 --- /dev/null +++ b/include/MqttHandlePowerLimiter.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include + +class MqttHandlePowerLimiterClass { +public: + void init(); + void loop(); + +private: + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + + uint32_t _lastPublishStats; + uint32_t _lastPublish; + +}; + +extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; \ No newline at end of file From a893260de0bfe1a662efb0f2e3ddfccb556011af Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Wed, 5 Apr 2023 20:40:07 +0200 Subject: [PATCH 11/30] refactor state machine --- include/PowerLimiter.h | 8 +-- src/PowerLimiter.cpp | 134 +++++++++++++++-------------------------- 2 files changed, 52 insertions(+), 90 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 96baaf7c1..b1a834e10 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -8,10 +8,8 @@ #include typedef enum { - STATE_DISCOVER = 0, - STATE_OFF, - STATE_CONSUME_SOLAR_POWER_ONLY, - STATE_NORMAL_OPERATION + STATE_PL_SHUTDOWN = 0, + STATE_ACTIVE } plStates; typedef enum { @@ -32,7 +30,7 @@ class PowerLimiterClass { uint32_t _lastLoop = 0; int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; - plStates _plState = STATE_DISCOVER; + plStates _plState = STATE_ACTIVE; float _powerMeter1Power; float _powerMeter2Power; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 42c8c966d..e15ac8a75 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -23,23 +23,45 @@ void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); - if (!config.PowerLimiter_Enabled - || !config.PowerMeter_Enabled + // Run inital checks to make sure we have met the basic conditions + if ( !config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { - if (!config.PowerLimiter_Enabled) - _plState = STATE_DISCOVER; // ensure STATE_DISCOVER is set, if PowerLimiter will be enabled. return; } _lastLoop = millis(); + // Debug state transistions + + + MessageOutput.printf("****************** PL STATE: %i\r\n", _plState); + std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); if (inverter == nullptr || !inverter->isReachable()) { return; } + // Make sure inverter is turned off if PL is disabled by user + // Make sure inverter is turned off when lower battery threshold is reached + // In this case we willbe in some state and want to reach STATE_PL_SHUTDOWN + if ((!config.PowerLimiter_Enabled && _plState != STATE_PL_SHUTDOWN) + || isStopThresholdReached(inverter)) { + if (inverter->isProducing()) { + MessageOutput.printf("PL initiated inverter shutdown.\r\n"); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); + } else { + _plState = STATE_PL_SHUTDOWN; + } + return; + } + + // PL is disabled + if (!config.PowerLimiter_Enabled) { + return; + } + float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); @@ -59,87 +81,26 @@ void PowerLimiterClass::loop() dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); } - while(true) { - switch(_plState) { - case STATE_DISCOVER: - if (!inverter->isProducing() || isStopThresholdReached(inverter)) { - _plState = STATE_OFF; - } - else if (canUseDirectSolarPower()) { - _plState = STATE_CONSUME_SOLAR_POWER_ONLY; - } - else { - _plState = STATE_NORMAL_OPERATION; - } - break; - case STATE_OFF: - // if on turn off - if (inverter->isProducing()) { - MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %.2f Corrected DC voltage: %.2f...\r\n", - dcVoltage, correctedDcVoltage); - setNewPowerLimit(inverter, -1); - return; - } - - // do nothing if battery is empty - if (isStopThresholdReached(inverter)) - return; - // check for possible state changes - if (canUseDirectSolarPower()) { - _plState = STATE_CONSUME_SOLAR_POWER_ONLY; - } - if (isStartThresholdReached(inverter)) { - _plState = STATE_NORMAL_OPERATION; - } - return; - break; - case STATE_CONSUME_SOLAR_POWER_ONLY: { - int32_t newPowerLimit = calcPowerLimit(inverter, true); - if (isStopThresholdReached(inverter)) { - _plState = STATE_OFF; - break; - } - if (isStartThresholdReached(inverter)) { - _plState = STATE_NORMAL_OPERATION; - break; - } - - if (!canUseDirectSolarPower()) { - if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) - _plState = STATE_NORMAL_OPERATION; - else - _plState = STATE_OFF; - break; - } - - setNewPowerLimit(inverter, newPowerLimit); - return; - break; - } - case STATE_NORMAL_OPERATION: { - int32_t newPowerLimit = calcPowerLimit(inverter, false); - if (isStopThresholdReached(inverter)) { - _plState = STATE_OFF; - break; - } - if (!isStartThresholdReached(inverter) && canUseDirectSolarPower() && (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT)) { - _plState = STATE_CONSUME_SOLAR_POWER_ONLY; - break; - } - - // check if grid power consumption is not within the upper and lower threshold of the target consumption - if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) && - _lastRequestedPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - _lastRequestedPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) ) { - return; - } - setNewPowerLimit(inverter, newPowerLimit);; - return; - break; - } - } + // Check if we need to move state away from STATE_PL_SHUTDOWN + if (_plState == STATE_PL_SHUTDOWN) { + + // Allow discharge when start threshold reached + // This is also the trigger for drain strategy: EMPTY_WHEN_FULL + if (isStartThresholdReached(inverter)) { + _plState = STATE_ACTIVE; + } + + // Allow discharge when drain strategy is EMPTY_AT_NIGHT + if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { + _plState = STATE_ACTIVE; + } + + return; } + + int32_t newPowerLimit = calcPowerLimit(inverter, canUseDirectSolarPower()); + MessageOutput.printf("****************************** Powerlimit: %i\r\n", newPowerLimit); + setNewPowerLimit(inverter, newPowerLimit); } plStates PowerLimiterClass::getPowerLimiterState() { @@ -154,7 +115,7 @@ bool PowerLimiterClass::canUseDirectSolarPower() { CONFIG_T& config = Configuration.get(); - if (!config.PowerLimiter_SolarPassTroughEnabled + if (!config.PowerLimiter_SolarPassThroughEnabled || !config.Vedirect_Enabled) { return false; } @@ -167,6 +128,9 @@ bool PowerLimiterClass::canUseDirectSolarPower() return true; } + + + int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly) { CONFIG_T& config = Configuration.get(); From 1e4337e90077acbea14e2f7a332c187fb8f223fb Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 6 Apr 2023 20:06:55 +0200 Subject: [PATCH 12/30] merging --- src/PowerLimiter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index e15ac8a75..b64e01a56 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -22,7 +22,7 @@ void PowerLimiterClass::init() void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); - + // Run inital checks to make sure we have met the basic conditions if ( !config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() From 1b29133ee02b6dc72e8da93e4955aa6796194618 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 6 Apr 2023 09:40:46 +0200 Subject: [PATCH 13/30] merging functionality from PL refactor --- include/PowerLimiter.h | 7 ++++--- src/PowerLimiter.cpp | 45 +++++++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 7ef471781..c3503f15e 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -8,8 +8,8 @@ #include typedef enum { - STATE_PL_SHUTDOWN = 0, - STATE_ACTIVE + SHUTDOWN = 0, + ACTIVE } plStates; typedef enum { @@ -32,8 +32,9 @@ class PowerLimiterClass { uint32_t _lastLoop = 0; int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; - plStates _plState = STATE_ACTIVE; + plStates _plState = ACTIVE; bool _disabled = false; + bool _batteryDischargeEnabled = false; float _powerMeter1Power; float _powerMeter2Power; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index ab9cbb0e5..35f0000b7 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -33,9 +33,7 @@ void PowerLimiterClass::loop() _lastLoop = millis(); - // Debug state transistions - - + // Debug state transistions, TODO: Remove MessageOutput.printf("****************** PL STATE: %i\r\n", _plState); std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); @@ -46,25 +44,25 @@ void PowerLimiterClass::loop() // Make sure inverter is turned off if PL is disabled by user // Make sure inverter is turned off when lower battery threshold is reached // In this case we are in some state and want to reach STATE_PL_SHUTDOWN - if (((!config.PowerLimiter_Enabled || _disabled) && _plState != STATE_PL_SHUTDOWN) + if (((!config.PowerLimiter_Enabled || _disabled) && _plState != SHUTDOWN) || isStopThresholdReached(inverter)) { if (inverter->isProducing()) { MessageOutput.printf("PL initiated inverter shutdown.\r\n"); inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); } else { - _plState = STATE_PL_SHUTDOWN; + _plState = SHUTDOWN; } return; } - // PL is disabled + // If power limiter is disabled if (!config.PowerLimiter_Enabled) { return; } float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); - float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); - float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); + //float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); + //float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); // If the last inverter update is too old, don't do anything. // If the last inverter update was before the last limit updated, don't do anything. @@ -81,24 +79,27 @@ void PowerLimiterClass::loop() dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); } - // Check if we need to move state away from STATE_PL_SHUTDOWN - if (_plState == STATE_PL_SHUTDOWN) { - - // Allow discharge when start threshold reached - // This is also the trigger for drain strategy: EMPTY_WHEN_FULL - if (isStartThresholdReached(inverter)) { - _plState = STATE_ACTIVE; - } + // If we're in shutdown move to active operation + if (_plState == SHUTDOWN) { + _plState = ACTIVE; + } - // Allow discharge when drain strategy is EMPTY_AT_NIGHT - if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { - _plState = STATE_ACTIVE; - } + if (isStopThresholdReached(inverter)) { + // Disable battery discharge when empty + _batteryDischargeEnabled = false; + } else if (!canUseDirectSolarPower() || + config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { + // Enable battery discharge + _batteryDischargeEnabled = true; + } - return; + // This checks if the battery discharge start conditions are met for the EMPTY_WHEN_FULL case + if (isStartThresholdReached(inverter) && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) { + _batteryDischargeEnabled = true; } - int32_t newPowerLimit = calcPowerLimit(inverter, canUseDirectSolarPower()); + int32_t newPowerLimit = calcPowerLimit(inverter, !_batteryDischargeEnabled); + // Debug, TODO: Remove MessageOutput.printf("****************************** Powerlimit: %i\r\n", newPowerLimit); setNewPowerLimit(inverter, newPowerLimit); } From 79834e4d47d8d4d258851054903066f2f2a386cc Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Wed, 12 Apr 2023 12:51:24 +0200 Subject: [PATCH 14/30] adding Mqtt handling to main.cpp --- src/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 4d1dad575..12eb9d650 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include "MqttHandleInverter.h" #include "MqttHandleVedirect.h" #include "MqttHandleHuawei.h" +#include "MqttHandlePowerLimiter.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "NtpSettings.h" @@ -104,6 +105,7 @@ void setup() MqttHandleHass.init(); MqttHandleVedirectHass.init(); MqttHandleHuawei.init(); + MqttHandlePowerLimiter.init(); MessageOutput.println("done"); // Initialize WebApi @@ -212,6 +214,8 @@ void loop() yield(); MqttHandleHuawei.loop(); yield(); + MqttHandlePowerLimiter.loop(); + yield(); WebApi.loop(); yield(); Display.loop(); From 9ff1885d5ac79af63273c31a387cfa5d312e1655 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 13 Apr 2023 07:45:26 +0200 Subject: [PATCH 15/30] Removing un-necessary timestamp, commenting code and cleanups --- include/PowerLimiter.h | 1 - src/PowerLimiter.cpp | 39 ++++++++++++++++----------------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index c3503f15e..fefa9ed0d 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -28,7 +28,6 @@ class PowerLimiterClass { bool getDisable(); private: - uint32_t _lastCommandSent = 0; uint32_t _lastLoop = 0; int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 35f0000b7..1fd3a6371 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -26,24 +26,19 @@ void PowerLimiterClass::loop() // Run inital checks to make sure we have met the basic conditions if (!config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() - || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { return; } _lastLoop = millis(); - // Debug state transistions, TODO: Remove - MessageOutput.printf("****************** PL STATE: %i\r\n", _plState); - std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); if (inverter == nullptr || !inverter->isReachable()) { return; } - // Make sure inverter is turned off if PL is disabled by user - // Make sure inverter is turned off when lower battery threshold is reached - // In this case we are in some state and want to reach STATE_PL_SHUTDOWN + // Make sure inverter is turned off if PL is disabled by user/MQTT + // Make sure inverter is turned off when low battery threshold is reached if (((!config.PowerLimiter_Enabled || _disabled) && _plState != SHUTDOWN) || isStopThresholdReached(inverter)) { if (inverter->isProducing()) { @@ -55,35 +50,34 @@ void PowerLimiterClass::loop() return; } - // If power limiter is disabled - if (!config.PowerLimiter_Enabled) { + // Return if power limiter is disabled + if (!config.PowerLimiter_Enabled || _disabled) { return; } - - float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); - //float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); - //float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); + // At this point the PL is enabled but we could still be in the shutdown state + _plState = ACTIVE; // If the last inverter update is too old, don't do anything. // If the last inverter update was before the last limit updated, don't do anything. - // Also give the Power meter 3 seconds time to recognize power changes because of the last set limit - // and also because the Hoymiles MPPT might not react immediately. + // Also give the Power meter 3 seconds time to recognize power changes after the last set limit + // as the Hoymiles MPPT might not react immediately. if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000 || inverter->Statistics()->getLastUpdate() <= _lastLimitSetTime || PowerMeter.getLastPowerMeterUpdate() <= (_lastLimitSetTime + 3000)) { return; } + // Printout some stats if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { + float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n", dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); } - // If we're in shutdown move to active operation - if (_plState == SHUTDOWN) { - _plState = ACTIVE; - } + // Battery charging cycle conditions + // The battery can only be discharged after a full charge in the + // EMPTY_WHEN_FULL case if (isStopThresholdReached(inverter)) { // Disable battery discharge when empty _batteryDischargeEnabled = false; @@ -98,10 +92,11 @@ void PowerLimiterClass::loop() _batteryDischargeEnabled = true; } + // Calculate and set Power Limit int32_t newPowerLimit = calcPowerLimit(inverter, !_batteryDischargeEnabled); + setNewPowerLimit(inverter, newPowerLimit); // Debug, TODO: Remove MessageOutput.printf("****************************** Powerlimit: %i\r\n", newPowerLimit); - setNewPowerLimit(inverter, newPowerLimit); } plStates PowerLimiterClass::getPowerLimiterState() { @@ -205,7 +200,6 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver if (!inverter->isProducing() && newPowerLimit > config.PowerLimiter_LowerPowerLimit) { MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); - _lastCommandSent = millis(); } // Stop the inverter if limit is below threshold. @@ -214,7 +208,6 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver if (inverter->isProducing()) { MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); - _lastCommandSent = millis(); } newPowerLimit = config.PowerLimiter_LowerPowerLimit; } @@ -223,7 +216,7 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver // and differs from the last requested value if( _lastRequestedPowerLimit != newPowerLimit && /* newPowerLimit > config.PowerLimiter_LowerPowerLimit && --> This will always be true given the check above, kept for code readability */ - newPowerLimit < config.PowerLimiter_UpperPowerLimit ) { + newPowerLimit <= config.PowerLimiter_UpperPowerLimit ) { MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit); inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); _lastRequestedPowerLimit = newPowerLimit; From fc5089e70bc586c3401a3c8b3b0a5e8c9a22be9d Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 13 Apr 2023 09:36:00 +0200 Subject: [PATCH 16/30] resolving merge conflict --- src/PowerLimiter.cpp | 83 +++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index af5ef2943..91bb0eb77 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -173,6 +173,30 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); + // Safety check, return on too old power meter values + if ((millis() - PowerMeter.getLastPowerMeterUpdate()) > (30 * 1000)) { + // If the power meter values are older than 30 seconds, + // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. + MessageOutput.println("[PowerLimiterClass::loop] Power Meter values too old. Using lower limit"); + return config.PowerLimiter_LowerPowerLimit; + } + + // check if grid power consumption is within the limits of the target consumption + hysteresis + if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && + newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { + // The values have not changed much. We just use the old setting + MessageOutput.println("[PowerLimiterClass::loop] reusing old limit"); + return _lastRequestedPowerLimit; + } + + if (config.PowerLimiter_IsInverterBehindPowerMeter) { + // If the inverter the behind the power meter (part of measurement), + // the produced power of this inverter has also to be taken into account. + // We don't use FLD_PAC from the statistics, because that + // data might be too old and unrelieable. + newPowerLimit += _lastRequestedPowerLimit; + } + float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); int32_t victronChargePower = this->getDirectSolarPower(); int32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0 @@ -191,43 +215,46 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve newPowerLimit += static_cast(acPower); } - newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; + int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; + if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { + // Battery voltage too low, use Victron solar power (corrected by efficency factor) only + upperPowerLimit = adjustedVictronChargePower; + } - int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; - if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { - // Battery voltage too low, use Victron solar power (corrected by efficency factor) only - upperPowerLimit = adjustedVictronChargePower; - } + if (newPowerLimit > upperPowerLimit) + newPowerLimit = upperPowerLimit; - if (newPowerLimit > upperPowerLimit) - newPowerLimit = upperPowerLimit; - } else { - // If the power meter values are older than 30 seconds, - // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. - newPowerLimit = config.PowerLimiter_LowerPowerLimit; - } MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); return newPowerLimit; } void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit) { - if(_lastRequestedPowerLimit != newPowerLimit) { - CONFIG_T& config = Configuration.get(); - - // if limit too low turn inverter offf - if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { - if (inverter->isProducing()) { - MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); - _lastCommandSent = millis(); - } - newPowerLimit = config.PowerLimiter_LowerPowerLimit; - } else if (!inverter->isProducing()) { - MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); + CONFIG_T& config = Configuration.get(); + + // Start the inverter in case it's inactive and if the requested power is high enough + if (!inverter->isProducing() && newPowerLimit > config.PowerLimiter_LowerPowerLimit) { + MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); + _lastCommandSent = millis(); + } + + // Stop the inverter if limit is below threshold. + // We'll also set the power limit to the lower value in this case + if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + if (inverter->isProducing()) { + MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); _lastCommandSent = millis(); - } + } + newPowerLimit = config.PowerLimiter_LowerPowerLimit; + } + + // Set the actual limit. We'll only do this is if the limit is in the right range + // and differs from the last requested value + if( _lastRequestedPowerLimit != newPowerLimit && + /* newPowerLimit > config.PowerLimiter_LowerPowerLimit && --> This will always be true given the check above, kept for code readability */ + newPowerLimit < config.PowerLimiter_UpperPowerLimit ) { MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit); inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); _lastRequestedPowerLimit = newPowerLimit; From 9efe076cc23cee66526f0f67fdf038aec380e97c Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 13 Apr 2023 09:37:35 +0200 Subject: [PATCH 17/30] resolving merge conflict adding missing statement from merge fixing a bug introduced in merge --- src/PowerLimiter.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 91bb0eb77..214df5df9 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -174,8 +174,10 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); // Safety check, return on too old power meter values - if ((millis() - PowerMeter.getLastPowerMeterUpdate()) > (30 * 1000)) { - // If the power meter values are older than 30 seconds, + if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000) + && (millis() - inverter->Statistics()->getLastUpdate()) > (config.Dtu_PollInterval * 3 * 1000)) { + // If the power meter values are older than 30 seconds, + // and the Inverter Stats are older then 3x the poll interval // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. MessageOutput.println("[PowerLimiterClass::loop] Power Meter values too old. Using lower limit"); return config.PowerLimiter_LowerPowerLimit; @@ -193,8 +195,9 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // If the inverter the behind the power meter (part of measurement), // the produced power of this inverter has also to be taken into account. // We don't use FLD_PAC from the statistics, because that - // data might be too old and unrelieable. - newPowerLimit += _lastRequestedPowerLimit; + // data might be too old and unreliable. + float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); + newPowerLimit += static_cast(acPower); } float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); @@ -217,7 +220,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { - // Battery voltage too low, use Victron solar power (corrected by efficency factor) only + // Battery voltage too low, use Victron solar power (corrected by efficiency factor) only upperPowerLimit = adjustedVictronChargePower; } From ee82c8c9b874ed2b2d26ac0c86aa3955d4f3a87f Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Wed, 12 Apr 2023 06:45:41 +0200 Subject: [PATCH 18/30] adding option to disable power limiter via mqtt adding option to disable power limiter via mqtt - adding missing file --- README.md | 6 +++ include/MqttHandlePowerLimiter.h | 20 ++++++++ include/PowerLimiter.h | 3 ++ src/MqttHandlePowerLimiter.cpp | 83 ++++++++++++++++++++++++++++++++ src/PowerLimiter.cpp | 12 ++++- 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 include/MqttHandlePowerLimiter.h create mode 100644 src/MqttHandlePowerLimiter.cpp diff --git a/README.md b/README.md index 887e5ab37..eae353357 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,12 @@ Topics for 3 phases of a power meter is configurable. Given is an example for th | huawei/output_temp | R | Output air temperature | °C | | huawei/efficiency | R | Efficiency | Percentage | +## Power Limiter topics +| Topic | R / W | Description | Value / Unit | +| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | +| powerlimiter/cmd/disable | W | Power Limiter disable override for external PL control | 0 / 1 | +| powerlimiter/status/disabled | R | Power Limiter disable override status | 0 / 1 | + ## Currently supported Inverters * Hoymiles HM-300 * Hoymiles HM-350 diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h new file mode 100644 index 000000000..82d736ea6 --- /dev/null +++ b/include/MqttHandlePowerLimiter.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include + +class MqttHandlePowerLimiterClass { +public: + void init(); + void loop(); + +private: + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + + uint32_t _lastPublishStats; + uint32_t _lastPublish; + +}; + +extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; \ No newline at end of file diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 96baaf7c1..6e9357a1f 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -26,6 +26,8 @@ class PowerLimiterClass { void loop(); plStates getPowerLimiterState(); int32_t getLastRequestedPowewrLimit(); + void setDisable(bool disable); + bool getDisable(); private: uint32_t _lastCommandSent = 0; @@ -33,6 +35,7 @@ class PowerLimiterClass { int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; plStates _plState = STATE_DISCOVER; + bool _disabled = false; float _powerMeter1Power; float _powerMeter2Power; diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp new file mode 100644 index 000000000..2830d420d --- /dev/null +++ b/src/MqttHandlePowerLimiter.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler, Malte Schmidt and others + */ +#include "MessageOutput.h" +#include "MqttSettings.h" +#include "MqttHandlePowerLimiter.h" +#include "PowerLimiter.h" +#include + +#define TOPIC_SUB_POWER_LIMITER "disable" + +MqttHandlePowerLimiterClass MqttHandlePowerLimiter; + +void MqttHandlePowerLimiterClass::init() +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + String topic = MqttSettings.getPrefix(); + MqttSettings.subscribe(String(topic + "powerlimiter/cmd/" + TOPIC_SUB_POWER_LIMITER).c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + + _lastPublish = millis(); + +} + + +void MqttHandlePowerLimiterClass::loop() +{ + if (!MqttSettings.getConnected() ) { + return; + } + + const CONFIG_T& config = Configuration.get(); + + if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) { + MqttSettings.publish("powerlimiter/status/disabled", String(PowerLimiter.getDisable())); + + yield(); + _lastPublish = millis(); + } +} + + +void MqttHandlePowerLimiterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +{ + const CONFIG_T& config = Configuration.get(); + + char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics + strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* + + char* setting; + char* rest = &token_topic[strlen(config.Mqtt_Topic)]; + + strtok_r(rest, "/", &rest); // Remove "powerlimiter" + strtok_r(rest, "/", &rest); // Remove "cmd" + + setting = strtok_r(rest, "/", &rest); + + if (setting == NULL) { + return; + } + + char* strlimit = new char[len + 1]; + memcpy(strlimit, payload, len); + strlimit[len] = '\0'; + float payload_val = strtof(strlimit, NULL); + delete[] strlimit; + + if (!strcmp(setting, TOPIC_SUB_POWER_LIMITER)) { + MessageOutput.printf("Disable power limter: %f A\r\n", payload_val); + if(payload_val == 1) { + PowerLimiter.setDisable(true); + } + if(payload_val == 0) { + PowerLimiter.setDisable(false); + } + } +} \ No newline at end of file diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 214df5df9..9bd91c4d4 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -24,11 +24,13 @@ void PowerLimiterClass::loop() CONFIG_T& config = Configuration.get(); if (!config.PowerLimiter_Enabled + || _disabled || !config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { - if (!config.PowerLimiter_Enabled) + if (!config.PowerLimiter_Enabled + || _disabled) _plState = STATE_DISCOVER; // ensure STATE_DISCOVER is set, if PowerLimiter will be enabled. return; } @@ -150,6 +152,14 @@ int32_t PowerLimiterClass::getLastRequestedPowewrLimit() { return _lastRequestedPowerLimit; } +bool PowerLimiterClass::getDisable() { + return _disabled; +} + +void PowerLimiterClass::setDisable(bool disable) { + _disabled = disable; +} + bool PowerLimiterClass::canUseDirectSolarPower() { CONFIG_T& config = Configuration.get(); From 9999fa28e8c15cd6f4ebd40143908ff20c80b090 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Wed, 5 Apr 2023 20:40:07 +0200 Subject: [PATCH 19/30] refactor state machine merging --- include/PowerLimiter.h | 8 +-- src/PowerLimiter.cpp | 136 +++++++++++++++-------------------------- 2 files changed, 52 insertions(+), 92 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 6e9357a1f..7ef471781 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -8,10 +8,8 @@ #include typedef enum { - STATE_DISCOVER = 0, - STATE_OFF, - STATE_CONSUME_SOLAR_POWER_ONLY, - STATE_NORMAL_OPERATION + STATE_PL_SHUTDOWN = 0, + STATE_ACTIVE } plStates; typedef enum { @@ -34,7 +32,7 @@ class PowerLimiterClass { uint32_t _lastLoop = 0; int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; - plStates _plState = STATE_DISCOVER; + plStates _plState = STATE_ACTIVE; bool _disabled = false; float _powerMeter1Power; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 9bd91c4d4..9240ff539 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -22,26 +22,46 @@ void PowerLimiterClass::init() void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); - - if (!config.PowerLimiter_Enabled - || _disabled - || !config.PowerMeter_Enabled + + // Run inital checks to make sure we have met the basic conditions + if ( !config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { - if (!config.PowerLimiter_Enabled - || _disabled) - _plState = STATE_DISCOVER; // ensure STATE_DISCOVER is set, if PowerLimiter will be enabled. return; } _lastLoop = millis(); + // Debug state transistions + + + MessageOutput.printf("****************** PL STATE: %i\r\n", _plState); + std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); if (inverter == nullptr || !inverter->isReachable()) { return; } + // Make sure inverter is turned off if PL is disabled by user + // Make sure inverter is turned off when lower battery threshold is reached + // In this case we willbe in some state and want to reach STATE_PL_SHUTDOWN + if ((!config.PowerLimiter_Enabled && _plState != STATE_PL_SHUTDOWN) + || isStopThresholdReached(inverter)) { + if (inverter->isProducing()) { + MessageOutput.printf("PL initiated inverter shutdown.\r\n"); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); + } else { + _plState = STATE_PL_SHUTDOWN; + } + return; + } + + // PL is disabled + if (!config.PowerLimiter_Enabled) { + return; + } + float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); @@ -61,87 +81,26 @@ void PowerLimiterClass::loop() dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); } - while(true) { - switch(_plState) { - case STATE_DISCOVER: - if (!inverter->isProducing() || isStopThresholdReached(inverter)) { - _plState = STATE_OFF; - } - else if (canUseDirectSolarPower()) { - _plState = STATE_CONSUME_SOLAR_POWER_ONLY; - } - else { - _plState = STATE_NORMAL_OPERATION; - } - break; - case STATE_OFF: - // if on turn off - if (inverter->isProducing()) { - MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %.2f Corrected DC voltage: %.2f...\r\n", - dcVoltage, correctedDcVoltage); - setNewPowerLimit(inverter, -1); - return; - } - - // do nothing if battery is empty - if (isStopThresholdReached(inverter)) - return; - // check for possible state changes - if (canUseDirectSolarPower()) { - _plState = STATE_CONSUME_SOLAR_POWER_ONLY; - } - if (isStartThresholdReached(inverter)) { - _plState = STATE_NORMAL_OPERATION; - } - return; - break; - case STATE_CONSUME_SOLAR_POWER_ONLY: { - int32_t newPowerLimit = calcPowerLimit(inverter, true); - if (isStopThresholdReached(inverter)) { - _plState = STATE_OFF; - break; - } - if (isStartThresholdReached(inverter)) { - _plState = STATE_NORMAL_OPERATION; - break; - } - - if (!canUseDirectSolarPower()) { - if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) - _plState = STATE_NORMAL_OPERATION; - else - _plState = STATE_OFF; - break; - } - - setNewPowerLimit(inverter, newPowerLimit); - return; - break; - } - case STATE_NORMAL_OPERATION: { - int32_t newPowerLimit = calcPowerLimit(inverter, false); - if (isStopThresholdReached(inverter)) { - _plState = STATE_OFF; - break; - } - if (!isStartThresholdReached(inverter) && canUseDirectSolarPower() && (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT)) { - _plState = STATE_CONSUME_SOLAR_POWER_ONLY; - break; - } - - // check if grid power consumption is not within the upper and lower threshold of the target consumption - if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) && - _lastRequestedPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - _lastRequestedPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) ) { - return; - } - setNewPowerLimit(inverter, newPowerLimit);; - return; - break; - } - } + // Check if we need to move state away from STATE_PL_SHUTDOWN + if (_plState == STATE_PL_SHUTDOWN) { + + // Allow discharge when start threshold reached + // This is also the trigger for drain strategy: EMPTY_WHEN_FULL + if (isStartThresholdReached(inverter)) { + _plState = STATE_ACTIVE; + } + + // Allow discharge when drain strategy is EMPTY_AT_NIGHT + if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { + _plState = STATE_ACTIVE; + } + + return; } + + int32_t newPowerLimit = calcPowerLimit(inverter, canUseDirectSolarPower()); + MessageOutput.printf("****************************** Powerlimit: %i\r\n", newPowerLimit); + setNewPowerLimit(inverter, newPowerLimit); } plStates PowerLimiterClass::getPowerLimiterState() { @@ -177,6 +136,9 @@ bool PowerLimiterClass::canUseDirectSolarPower() return true; } + + + int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly) { CONFIG_T& config = Configuration.get(); From ee376827fd7683f6df29648b9687a03c9d0885ad Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 6 Apr 2023 09:40:46 +0200 Subject: [PATCH 20/30] merging functionality from PL refactor --- include/PowerLimiter.h | 7 ++++--- src/PowerLimiter.cpp | 47 +++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 7ef471781..c3503f15e 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -8,8 +8,8 @@ #include typedef enum { - STATE_PL_SHUTDOWN = 0, - STATE_ACTIVE + SHUTDOWN = 0, + ACTIVE } plStates; typedef enum { @@ -32,8 +32,9 @@ class PowerLimiterClass { uint32_t _lastLoop = 0; int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; - plStates _plState = STATE_ACTIVE; + plStates _plState = ACTIVE; bool _disabled = false; + bool _batteryDischargeEnabled = false; float _powerMeter1Power; float _powerMeter2Power; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 9240ff539..b1db679fb 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -33,9 +33,7 @@ void PowerLimiterClass::loop() _lastLoop = millis(); - // Debug state transistions - - + // Debug state transistions, TODO: Remove MessageOutput.printf("****************** PL STATE: %i\r\n", _plState); std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); @@ -45,26 +43,26 @@ void PowerLimiterClass::loop() // Make sure inverter is turned off if PL is disabled by user // Make sure inverter is turned off when lower battery threshold is reached - // In this case we willbe in some state and want to reach STATE_PL_SHUTDOWN - if ((!config.PowerLimiter_Enabled && _plState != STATE_PL_SHUTDOWN) + // In this case we are in some state and want to reach STATE_PL_SHUTDOWN + if (((!config.PowerLimiter_Enabled || _disabled) && _plState != SHUTDOWN) || isStopThresholdReached(inverter)) { if (inverter->isProducing()) { MessageOutput.printf("PL initiated inverter shutdown.\r\n"); inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); } else { - _plState = STATE_PL_SHUTDOWN; + _plState = SHUTDOWN; } return; } - // PL is disabled + // If power limiter is disabled if (!config.PowerLimiter_Enabled) { return; } float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); - float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); - float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); + //float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); + //float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); // If the last inverter update is too old, don't do anything. // If the last inverter update was before the last limit updated, don't do anything. @@ -81,24 +79,27 @@ void PowerLimiterClass::loop() dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); } - // Check if we need to move state away from STATE_PL_SHUTDOWN - if (_plState == STATE_PL_SHUTDOWN) { - - // Allow discharge when start threshold reached - // This is also the trigger for drain strategy: EMPTY_WHEN_FULL - if (isStartThresholdReached(inverter)) { - _plState = STATE_ACTIVE; - } + // If we're in shutdown move to active operation + if (_plState == SHUTDOWN) { + _plState = ACTIVE; + } - // Allow discharge when drain strategy is EMPTY_AT_NIGHT - if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { - _plState = STATE_ACTIVE; - } + if (isStopThresholdReached(inverter)) { + // Disable battery discharge when empty + _batteryDischargeEnabled = false; + } else if (!canUseDirectSolarPower() || + config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { + // Enable battery discharge + _batteryDischargeEnabled = true; + } - return; + // This checks if the battery discharge start conditions are met for the EMPTY_WHEN_FULL case + if (isStartThresholdReached(inverter) && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) { + _batteryDischargeEnabled = true; } - int32_t newPowerLimit = calcPowerLimit(inverter, canUseDirectSolarPower()); + int32_t newPowerLimit = calcPowerLimit(inverter, !_batteryDischargeEnabled); + // Debug, TODO: Remove MessageOutput.printf("****************************** Powerlimit: %i\r\n", newPowerLimit); setNewPowerLimit(inverter, newPowerLimit); } From 01849dc90aebe1e835299f4b0c2b3066248fb557 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Wed, 12 Apr 2023 12:51:24 +0200 Subject: [PATCH 21/30] adding Mqtt handling to main.cpp --- src/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 4d1dad575..12eb9d650 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include "MqttHandleInverter.h" #include "MqttHandleVedirect.h" #include "MqttHandleHuawei.h" +#include "MqttHandlePowerLimiter.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "NtpSettings.h" @@ -104,6 +105,7 @@ void setup() MqttHandleHass.init(); MqttHandleVedirectHass.init(); MqttHandleHuawei.init(); + MqttHandlePowerLimiter.init(); MessageOutput.println("done"); // Initialize WebApi @@ -212,6 +214,8 @@ void loop() yield(); MqttHandleHuawei.loop(); yield(); + MqttHandlePowerLimiter.loop(); + yield(); WebApi.loop(); yield(); Display.loop(); From be7a43fbfb3bf128c50c3f036bfcd70a6f387383 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 13 Apr 2023 07:45:26 +0200 Subject: [PATCH 22/30] Removing un-necessary timestamp, commenting code and cleanups --- include/PowerLimiter.h | 1 - src/PowerLimiter.cpp | 39 ++++++++++++++++----------------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index c3503f15e..fefa9ed0d 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -28,7 +28,6 @@ class PowerLimiterClass { bool getDisable(); private: - uint32_t _lastCommandSent = 0; uint32_t _lastLoop = 0; int32_t _lastRequestedPowerLimit = 0; uint32_t _lastLimitSetTime = 0; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index b1db679fb..2347d53e0 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -26,24 +26,19 @@ void PowerLimiterClass::loop() // Run inital checks to make sure we have met the basic conditions if ( !config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() - || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { return; } _lastLoop = millis(); - // Debug state transistions, TODO: Remove - MessageOutput.printf("****************** PL STATE: %i\r\n", _plState); - std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); if (inverter == nullptr || !inverter->isReachable()) { return; } - // Make sure inverter is turned off if PL is disabled by user - // Make sure inverter is turned off when lower battery threshold is reached - // In this case we are in some state and want to reach STATE_PL_SHUTDOWN + // Make sure inverter is turned off if PL is disabled by user/MQTT + // Make sure inverter is turned off when low battery threshold is reached if (((!config.PowerLimiter_Enabled || _disabled) && _plState != SHUTDOWN) || isStopThresholdReached(inverter)) { if (inverter->isProducing()) { @@ -55,35 +50,34 @@ void PowerLimiterClass::loop() return; } - // If power limiter is disabled - if (!config.PowerLimiter_Enabled) { + // Return if power limiter is disabled + if (!config.PowerLimiter_Enabled || _disabled) { return; } - - float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); - //float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); - //float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); + // At this point the PL is enabled but we could still be in the shutdown state + _plState = ACTIVE; // If the last inverter update is too old, don't do anything. // If the last inverter update was before the last limit updated, don't do anything. - // Also give the Power meter 3 seconds time to recognize power changes because of the last set limit - // and also because the Hoymiles MPPT might not react immediately. + // Also give the Power meter 3 seconds time to recognize power changes after the last set limit + // as the Hoymiles MPPT might not react immediately. if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000 || inverter->Statistics()->getLastUpdate() <= _lastLimitSetTime || PowerMeter.getLastPowerMeterUpdate() <= (_lastLimitSetTime + 3000)) { return; } + // Printout some stats if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { + float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n", dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); } - // If we're in shutdown move to active operation - if (_plState == SHUTDOWN) { - _plState = ACTIVE; - } + // Battery charging cycle conditions + // The battery can only be discharged after a full charge in the + // EMPTY_WHEN_FULL case if (isStopThresholdReached(inverter)) { // Disable battery discharge when empty _batteryDischargeEnabled = false; @@ -98,10 +92,11 @@ void PowerLimiterClass::loop() _batteryDischargeEnabled = true; } + // Calculate and set Power Limit int32_t newPowerLimit = calcPowerLimit(inverter, !_batteryDischargeEnabled); + setNewPowerLimit(inverter, newPowerLimit); // Debug, TODO: Remove MessageOutput.printf("****************************** Powerlimit: %i\r\n", newPowerLimit); - setNewPowerLimit(inverter, newPowerLimit); } plStates PowerLimiterClass::getPowerLimiterState() { @@ -212,7 +207,6 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver if (!inverter->isProducing() && newPowerLimit > config.PowerLimiter_LowerPowerLimit) { MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); - _lastCommandSent = millis(); } // Stop the inverter if limit is below threshold. @@ -221,7 +215,6 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver if (inverter->isProducing()) { MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); - _lastCommandSent = millis(); } newPowerLimit = config.PowerLimiter_LowerPowerLimit; } @@ -230,7 +223,7 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver // and differs from the last requested value if( _lastRequestedPowerLimit != newPowerLimit && /* newPowerLimit > config.PowerLimiter_LowerPowerLimit && --> This will always be true given the check above, kept for code readability */ - newPowerLimit < config.PowerLimiter_UpperPowerLimit ) { + newPowerLimit <= config.PowerLimiter_UpperPowerLimit ) { MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit); inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); _lastRequestedPowerLimit = newPowerLimit; From 690025e5fdc4f805b97f80b2b7bd413241116406 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Thu, 13 Apr 2023 10:06:06 +0200 Subject: [PATCH 23/30] fixing a bug from merging and remove a leftover debug message --- src/PowerLimiter.cpp | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 2347d53e0..fd6b0f1ac 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -22,9 +22,9 @@ void PowerLimiterClass::init() void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); - + // Run inital checks to make sure we have met the basic conditions - if ( !config.PowerMeter_Enabled + if (!config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { return; @@ -95,8 +95,6 @@ void PowerLimiterClass::loop() // Calculate and set Power Limit int32_t newPowerLimit = calcPowerLimit(inverter, !_batteryDischargeEnabled); setNewPowerLimit(inverter, newPowerLimit); - // Debug, TODO: Remove - MessageOutput.printf("****************************** Powerlimit: %i\r\n", newPowerLimit); } plStates PowerLimiterClass::getPowerLimiterState() { @@ -175,16 +173,9 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); - // Safety check: Are the power meter values not too old? - // Are the reported inverter data not too old? - if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000) - && millis() - inverter->Statistics()->getLastUpdate() < (15 * 1000)) { - if (config.PowerLimiter_IsInverterBehindPowerMeter) { - // If the inverter the behind the power meter (part of measurement), - // the produced power of this inverter has also to be taken into account. - float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); - newPowerLimit += static_cast(acPower); - } + // We're not trying to hit 0 exactly but take an offset into account + // This means we never fully compensate the used power with the inverter + newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { @@ -295,4 +286,4 @@ bool PowerLimiterClass::isStopThresholdReached(std::shared_ptr float correctedDcVoltage = getLoadCorrectedVoltage(inverter); return correctedDcVoltage <= config.PowerLimiter_VoltageStopThreshold; -} +} \ No newline at end of file From a306bc13510d2a2d56cf7e2399ff8eddb1e9b955 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 16 Apr 2023 18:28:09 +0200 Subject: [PATCH 24/30] Bugfix for Mqtt enable / disable (was float, uses int now) --- src/MqttHandlePowerLimiter.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index 2830d420d..fdff3ee79 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -65,19 +65,23 @@ void MqttHandlePowerLimiterClass::onMqttMessage(const espMqttClientTypes::Messag return; } - char* strlimit = new char[len + 1]; - memcpy(strlimit, payload, len); - strlimit[len] = '\0'; - float payload_val = strtof(strlimit, NULL); - delete[] strlimit; + char* str = new char[len + 1]; + memcpy(str, payload, len); + str[len] = '\0'; + uint8_t payload_val = atoi(str); + delete[] str; if (!strcmp(setting, TOPIC_SUB_POWER_LIMITER)) { - MessageOutput.printf("Disable power limter: %f A\r\n", payload_val); if(payload_val == 1) { + MessageOutput.println("Power limiter disabled"); PowerLimiter.setDisable(true); - } + return; + } if(payload_val == 0) { + MessageOutput.println("Power limiter enabled"); PowerLimiter.setDisable(false); + return; } + MessageOutput.println("Power limiter enable / disable - unknown command received. Please use 0 or 1"); } } \ No newline at end of file From 0d0a624fe21369e574d8c11884dddc5e2b45a520 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 16 Apr 2023 18:28:49 +0200 Subject: [PATCH 25/30] make sure that PL is enabled if user uses webinterface --- src/WebApi_powerlimiter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 56fb0cf60..dda6c9aac 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -119,6 +119,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.PowerLimiter_Enabled = root[F("enabled")].as(); + PowerLimiter.setDisable(false); // User input clears the PL internal disable flag config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passtrough_enabled")].as(); config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); From 4ddaa7643b82b59e64c074647da177899744fd60 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 16 Apr 2023 18:28:49 +0200 Subject: [PATCH 26/30] make sure that PL is enabled if user uses webinterface --- src/WebApi_powerlimiter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 56fb0cf60..dda6c9aac 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -119,6 +119,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.PowerLimiter_Enabled = root[F("enabled")].as(); + PowerLimiter.setDisable(false); // User input clears the PL internal disable flag config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passtrough_enabled")].as(); config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); From 8c9afbcdc0ed88337bd7c8dc02145583936dad87 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 23 Apr 2023 11:30:08 +0200 Subject: [PATCH 27/30] fix an issue if inverter is behind power meter --- src/PowerLimiter.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index fd6b0f1ac..992850fd9 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -149,14 +149,6 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve return config.PowerLimiter_LowerPowerLimit; } - // check if grid power consumption is within the limits of the target consumption + hysteresis - if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { - // The values have not changed much. We just use the old setting - MessageOutput.println("[PowerLimiterClass::loop] reusing old limit"); - return _lastRequestedPowerLimit; - } - if (config.PowerLimiter_IsInverterBehindPowerMeter) { // If the inverter the behind the power meter (part of measurement), // the produced power of this inverter has also to be taken into account. @@ -165,6 +157,16 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); newPowerLimit += static_cast(acPower); } + + // check if grid power consumption is within the limits of the target consumption + hysteresis + if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && + newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { + // The values have not changed much. We just use the old setting + MessageOutput.println("[PowerLimiterClass::loop] reusing old limit"); + return _lastRequestedPowerLimit; + } + + float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); int32_t victronChargePower = this->getDirectSolarPower(); From 322f532ac015c145f063ca01d68ebdc3c439102f Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Tue, 25 Apr 2023 10:41:35 +0200 Subject: [PATCH 28/30] Proper handling of use solar power only case --- src/PowerLimiter.cpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 992850fd9..f8e0935e4 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -141,12 +141,12 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // Safety check, return on too old power meter values if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000) - && (millis() - inverter->Statistics()->getLastUpdate()) > (config.Dtu_PollInterval * 3 * 1000)) { + && (millis() - inverter->Statistics()->getLastUpdate()) > (config.Dtu_PollInterval * 10 * 1000)) { // If the power meter values are older than 30 seconds, - // and the Inverter Stats are older then 3x the poll interval - // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. - MessageOutput.println("[PowerLimiterClass::loop] Power Meter values too old. Using lower limit"); - return config.PowerLimiter_LowerPowerLimit; + // and the Inverter Stats are older then 10x the poll interval + // set the limit to 0W for safety reasons. + MessageOutput.println("[PowerLimiterClass::loop] Power Meter/Inverter values too old. Using 0W (i.e. disable inverter)"); + return 0; } if (config.PowerLimiter_IsInverterBehindPowerMeter) { @@ -157,16 +157,20 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); newPowerLimit += static_cast(acPower); } - - // check if grid power consumption is within the limits of the target consumption + hysteresis - if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { - // The values have not changed much. We just use the old setting - MessageOutput.println("[PowerLimiterClass::loop] reusing old limit"); - return _lastRequestedPowerLimit; - } + // We're not trying to hit 0 exactly but take an offset into account + // This means we never fully compensate the used power with the inverter + newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; + // Check if the new value is within the limits of the hysteresis and + // if we're not limited to Solar Power only (i.e. we can discharge the battery) + // If things did not change much we just use the old setting + if (newPowerLimit >= (-config.PowerLimiter_TargetPowerConsumptionHysteresis) && + newPowerLimit <= (+config.PowerLimiter_TargetPowerConsumptionHysteresis) && + !consumeSolarPowerOnly ) { + MessageOutput.println("[PowerLimiterClass::loop] reusing old limit"); + return _lastRequestedPowerLimit; + } float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); int32_t victronChargePower = this->getDirectSolarPower(); @@ -175,10 +179,6 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); - // We're not trying to hit 0 exactly but take an offset into account - // This means we never fully compensate the used power with the inverter - newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; - int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { // Battery voltage too low, use Victron solar power (corrected by efficiency factor) only From a8554f97b0380fdf9125fcc446c35b1c67f7c4a2 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Tue, 25 Apr 2023 11:05:08 +0200 Subject: [PATCH 29/30] refactored use solar power code --- src/PowerLimiter.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index f8e0935e4..940cec386 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -172,21 +172,21 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve return _lastRequestedPowerLimit; } - float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); - int32_t victronChargePower = this->getDirectSolarPower(); - int32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0 + // We should use Victron solar power only (corrected by efficiency factor) + if (consumeSolarPowerOnly) { + float efficiency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); + int32_t victronChargePower = this->getDirectSolarPower(); + int32_t adjustedVictronChargePower = victronChargePower * (efficiency > 0.0 ? (efficiency / 100.0) : 1.0); // if inverter is off, use 1.0 - MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", - victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); + MessageOutput.printf("[PowerLimiterClass::loop] Consuming Solar Power Only -> victronChargePower: %d, efficiency: %.2f, powerConsumption: %d \r\n", + victronChargePower, efficiency, newPowerLimit); - int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; - if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { - // Battery voltage too low, use Victron solar power (corrected by efficiency factor) only - upperPowerLimit = adjustedVictronChargePower; + newPowerLimit = adjustedVictronChargePower; } - if (newPowerLimit > upperPowerLimit) - newPowerLimit = upperPowerLimit; + // Respect power limit + if (newPowerLimit > config.PowerLimiter_UpperPowerLimit) + newPowerLimit = config.PowerLimiter_UpperPowerLimit; MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); return newPowerLimit; From 0a0488f73aa6c98dac1dbbcbd6a6cc792280ed80 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Tue, 25 Apr 2023 11:05:08 +0200 Subject: [PATCH 30/30] refactored use solar power code --- src/PowerLimiter.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index f8e0935e4..678f997b3 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -172,21 +172,23 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve return _lastRequestedPowerLimit; } - float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); - int32_t victronChargePower = this->getDirectSolarPower(); - int32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0 - - MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", - victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); - - int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; - if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { - // Battery voltage too low, use Victron solar power (corrected by efficiency factor) only - upperPowerLimit = adjustedVictronChargePower; + // We should use Victron solar power only (corrected by efficiency factor) + if (consumeSolarPowerOnly) { + float efficiency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); + int32_t victronChargePower = this->getDirectSolarPower(); + int32_t adjustedVictronChargePower = victronChargePower * (efficiency > 0.0 ? (efficiency / 100.0) : 1.0); // if inverter is off, use 1.0 + + MessageOutput.printf("[PowerLimiterClass::loop] Consuming Solar Power Only -> victronChargePower: %d, efficiency: %.2f, powerConsumption: %d \r\n", + victronChargePower, efficiency, newPowerLimit); + + // Limit power to solar power only + if (adjustedVictronChargePower < newPowerLimit) + newPowerLimit = adjustedVictronChargePower; } - if (newPowerLimit > upperPowerLimit) - newPowerLimit = upperPowerLimit; + // Respect power limit + if (newPowerLimit > config.PowerLimiter_UpperPowerLimit) + newPowerLimit = config.PowerLimiter_UpperPowerLimit; MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); return newPowerLimit;