diff --git a/docs/PowerLimiterInverterStates.drawio b/docs/PowerLimiterInverterStates.drawio
new file mode 100644
index 00000000..3d547018
--- /dev/null
+++ b/docs/PowerLimiterInverterStates.drawio
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/PowerLimiterInverterStates.png b/docs/PowerLimiterInverterStates.png
new file mode 100644
index 00000000..3a94dcc1
Binary files /dev/null and b/docs/PowerLimiterInverterStates.png differ
diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h
index 087ec621..9011b01b 100644
--- a/include/MqttHandleVedirect.h
+++ b/include/MqttHandleVedirect.h
@@ -18,7 +18,7 @@ public:
void init();
void loop();
private:
- veStruct _kvFrame;
+ veStruct _kvFrame{};
uint32_t _lastPublish;
};
diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h
index 44160fec..844bf20e 100644
--- a/include/PowerLimiter.h
+++ b/include/PowerLimiter.h
@@ -7,6 +7,14 @@
#include
#include
+enum PowerLimiterStates {
+ STATE_DISCOVER = 0,
+ STATE_OFF,
+ STATE_CONSUME_SOLAR_POWER_ONLY,
+ STATE_NORMAL_OPERATION
+};
+
+
class PowerLimiterClass {
public:
void init();
@@ -18,13 +26,15 @@ private:
uint32_t _lastLoop;
uint32_t _lastPowerMeterUpdate;
uint16_t _lastRequestedPowerLimit;
- bool _consumeSolarPowerOnly;
+ u_int8_t _plState = STATE_DISCOVER;
float _powerMeter1Power;
float _powerMeter2Power;
float _powerMeter3Power;
bool canUseDirectSolarPower();
+ int32_t calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly);
+ void setNewPowerLimit(std::shared_ptr inverter, uint32_t newPowerLimit);
uint16_t getDirectSolarPower();
float getLoadCorrectedVoltage(std::shared_ptr inverter);
bool isStartThresholdReached(std::shared_ptr inverter);
diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h
index 6ac4c700..fc732c0c 100644
--- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h
+++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h
@@ -37,7 +37,7 @@ typedef struct {
double V; // battery voltage in V
double I; // battery current in A
double VPV; // panel voltage in V
- double PPV; // panel power in W
+ uint16_t PPV; // panel power in W
double H19; // yield total kWh
double H20; // yield today kWh
uint16_t H21; // maximum power today W
@@ -61,13 +61,13 @@ public:
String getOrAsString(uint32_t offReason); // off reason as string
String getMpptAsString(uint8_t mppt); // state of mppt as string
- veStruct veFrame; // public map for received name and value pairs
+ veStruct veFrame{}; // public struct for received name and value pairs
private:
void setLastUpdate(); // set timestampt after successful frame read
void rxData(uint8_t inbyte); // byte of serial data
void textRxEvent(char *, char *);
- void frameEndEvent(bool); // copy temp map to public map
+ void frameEndEvent(bool); // copy temp struct to public struct
void logE(const char *, const char *);
bool hexRxEvent(uint8_t);
@@ -77,7 +77,7 @@ private:
char * _textPointer; // pointer to the private buffer we're writing to, name or value
char _name[VE_MAX_VALUE_LEN]; // buffer for the field name
char _value[VE_MAX_VALUE_LEN]; // buffer for the field value
- veStruct _tmpFrame; // private struct for received name and value pairs
+ veStruct _tmpFrame{}; // private struct for received name and value pairs
unsigned long _pollInterval;
unsigned long _lastPoll;
};
diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp
index 0b2cac15..c7a4d8c3 100644
--- a/src/PowerLimiter.cpp
+++ b/src/PowerLimiter.cpp
@@ -39,8 +39,7 @@ void PowerLimiterClass::init()
MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter3, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
}
- _consumeSolarPowerOnly = true;
- _lastCommandSent = 0;
+ _lastCommandSent = 0;
_lastLoop = 0;
_lastPowerMeterUpdate = 0;
_lastRequestedPowerLimit = 0;
@@ -74,132 +73,117 @@ void PowerLimiterClass::loop()
|| !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();
std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId);
-
if (inverter == nullptr || !inverter->isReachable()) {
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);
if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000) {
return;
}
- float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF);
- uint32_t victronChargePower = this->getDirectSolarPower();
-
- MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s \r\n", victronChargePower, efficency, _consumeSolarPowerOnly ? "true" : "false");
-
if (millis() - _lastPowerMeterUpdate < (30 * 1000)) {
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());
}
- int32_t powerMeter = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
- if (inverter->isProducing()) {
- float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
- float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor);
+ 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);
+ MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter...");
+ inverter->sendPowerControlRequest(Hoymiles.getRadio(), false);
- if ((_consumeSolarPowerOnly && isStartThresholdReached(inverter))) {
- // The battery is full enough again, use the full battery power from now on.
- _consumeSolarPowerOnly = false;
- } else if (!_consumeSolarPowerOnly && isStopThresholdReached(inverter) && canUseDirectSolarPower()) {
- // The battery voltage dropped too low
- _consumeSolarPowerOnly = true;
- }
+ uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit;
+ inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
+ _lastRequestedPowerLimit = newPowerLimit;
+ _lastCommandSent = millis();
+ return;
+ }
- if (isStopThresholdReached(inverter)
- || (_consumeSolarPowerOnly && !canUseDirectSolarPower())) {
- // DC voltage too low, stop the inverter
- MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %.2f Corrected DC voltage: %.2f...\r\n",
- dcVoltage, correctedDcVoltage);
- MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter...");
- inverter->sendPowerControlRequest(Hoymiles.getRadio(), false);
+ // do nothing if battery is empty
+ if (isStopThresholdReached(inverter))
+ return;
+ // check for possible state changes
+ if (isStartThresholdReached(inverter) && calcPowerLimit(inverter, false) >= config.PowerLimiter_LowerPowerLimit) {
+ _plState = STATE_NORMAL_OPERATION;
+ }
+ else if (canUseDirectSolarPower() && calcPowerLimit(inverter, true) >= config.PowerLimiter_LowerPowerLimit) {
+ _plState = STATE_CONSUME_SOLAR_POWER_ONLY;
+ }
- uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit;
- inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
- _lastRequestedPowerLimit = newPowerLimit;
-
- _lastCommandSent = millis();
- _consumeSolarPowerOnly = false;
-
- return;
- }
- } else {
- if ((isStartThresholdReached(inverter) || (canUseDirectSolarPower() && (!isStopThresholdReached(inverter))))
- && powerMeter >= config.PowerLimiter_LowerPowerLimit) {
- // DC voltage high enough, start the inverter
- MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
- _lastCommandSent = millis();
- inverter->sendPowerControlRequest(Hoymiles.getRadio(), true);
-
- // In this mode, the inverter should consume the current solar power only
- // and not drain additional power from the battery
- if (!isStartThresholdReached(inverter)) {
- _consumeSolarPowerOnly = true;
+ // inverter on on state change
+ if (_plState != STATE_OFF) {
+ // DC voltage high enough, start the inverter
+ MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
+ inverter->sendPowerControlRequest(Hoymiles.getRadio(), true);
+ _lastCommandSent = millis();
+ return;
+ }
+ else
+ return;
+ break;
+ case STATE_CONSUME_SOLAR_POWER_ONLY: {
+ int32_t newPowerLimit = calcPowerLimit(inverter, true);
+ if (!inverter->isProducing()
+ || isStopThresholdReached(inverter)
+ || newPowerLimit < config.PowerLimiter_LowerPowerLimit) {
+ _plState = STATE_OFF;
+ break;
+ }
+ else if (!canUseDirectSolarPower() || isStartThresholdReached(inverter)) {
+ _plState = STATE_NORMAL_OPERATION;
+ break;
+ }
+ setNewPowerLimit(inverter, newPowerLimit);
+ return;
+ break;
}
+ case STATE_NORMAL_OPERATION: {
+ int32_t newPowerLimit = calcPowerLimit(inverter, false);
+ if (!inverter->isProducing()
+ || isStopThresholdReached(inverter)
+ || newPowerLimit < config.PowerLimiter_LowerPowerLimit) {
+ _plState = STATE_OFF;
+ break;
+ }
+ // check if grid power consumption is within the upper an lower threshold of the target consumption
+ else if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
+ newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) {
+ return;
+ }
+ setNewPowerLimit(inverter, newPowerLimit);
+ return;
+ break;
+ }
}
-
- return;
}
-
- int32_t newPowerLimit = 0;
-
- if (millis() - _lastPowerMeterUpdate < (30 * 1000)) {
- newPowerLimit = powerMeter;
- // check if grid power consumption is within the upper an lower threshold of the target consumption
- if (!_consumeSolarPowerOnly &&
- newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
- newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis))
- return;
- else {
- 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;
- }
-
- newPowerLimit -= config.PowerLimiter_TargetPowerConsumption;
-
- uint16_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit;
- if (_consumeSolarPowerOnly && (upperPowerLimit > victronChargePower)) {
- // Battery voltage too low, use Victron solar power (corrected by efficency factor) only
- upperPowerLimit = victronChargePower * (efficency / 100.0);
- }
-
- if (newPowerLimit > upperPowerLimit)
- newPowerLimit = upperPowerLimit;
- else if (newPowerLimit < (uint16_t)config.PowerLimiter_LowerPowerLimit) {
- newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit;
- // stop the inverter
- MessageOutput.println("[PowerLimiterClass::loop] Power limit below lower power limit. Stopping inverter...");
- inverter->sendPowerControlRequest(Hoymiles.getRadio(), false);
-
- }
-
- MessageOutput.printf("[PowerLimiterClass::loop] powerMeter: %d W lastRequestedPowerLimit: %d\r\n",
- powerMeter, _lastRequestedPowerLimit);
- }
- } 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] Limit Non-Persistent: %d W\r\n", newPowerLimit);
- inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
- _lastRequestedPowerLimit = newPowerLimit;
-
- _lastCommandSent = millis();
}
bool PowerLimiterClass::canUseDirectSolarPower()
@@ -219,13 +203,60 @@ bool PowerLimiterClass::canUseDirectSolarPower()
return true;
}
+int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly)
+{
+ CONFIG_T& config = Configuration.get();
+
+ int32_t newPowerLimit = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
+
+ float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF);
+ uint32_t victronChargePower = this->getDirectSolarPower();
+ uint32_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 \r\n", victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false");
+
+ if (millis() - _lastPowerMeterUpdate < (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;
+ }
+
+ newPowerLimit -= config.PowerLimiter_TargetPowerConsumption;
+
+ uint16_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;
+ } 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;
+ }
+ return newPowerLimit;
+}
+
+void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, uint32_t newPowerLimit)
+{
+ MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit);
+ inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
+ _lastRequestedPowerLimit = newPowerLimit;
+ _lastCommandSent = millis();
+}
+
uint16_t PowerLimiterClass::getDirectSolarPower()
{
if (!canUseDirectSolarPower()) {
return 0;
}
- return round(VeDirect.veFrame.PPV);
+ return VeDirect.veFrame.PPV;
}
float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr inverter)