Skip to content

Commit

Permalink
Merge branch 'inverter-settings' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
helgeerbe committed Mar 9, 2023
2 parents a79f7b2 + 26dc262 commit 43dc10b
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 108 deletions.
263 changes: 263 additions & 0 deletions docs/PowerLimiterInverterStates.drawio

Large diffs are not rendered by default.

Binary file added docs/PowerLimiterInverterStates.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion include/MqttHandleVedirect.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class MqttHandleVedirectClass {
void init();
void loop();
private:
veStruct _kvFrame;
veStruct _kvFrame{};
uint32_t _lastPublish;
};

Expand Down
12 changes: 11 additions & 1 deletion include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
#include <Hoymiles.h>
#include <memory>

enum PowerLimiterStates {
STATE_DISCOVER = 0,
STATE_OFF,
STATE_CONSUME_SOLAR_POWER_ONLY,
STATE_NORMAL_OPERATION
};


class PowerLimiterClass {
public:
void init();
Expand All @@ -18,13 +26,15 @@ class PowerLimiterClass {
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<InverterAbstract> inverter, bool consumeSolarPowerOnly);
void setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, uint32_t newPowerLimit);
uint16_t getDirectSolarPower();
float getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter);
bool isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter);
Expand Down
8 changes: 4 additions & 4 deletions lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -61,13 +61,13 @@ class VeDirectFrameHandler {
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);

Expand All @@ -77,7 +77,7 @@ class VeDirectFrameHandler {
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;
};
Expand Down
235 changes: 133 additions & 102 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,158 +73,190 @@ 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<InverterAbstract> 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);

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;
}

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);

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;
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);

uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit;
inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
_lastRequestedPowerLimit = newPowerLimit;
_lastCommandSent = millis();
return;
}

// 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;
}

// 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;
bool PowerLimiterClass::canUseDirectSolarPower()
{
CONFIG_T& config = Configuration.get();

if (!config.PowerLimiter_SolarPassTroughEnabled
|| !config.Vedirect_Enabled) {
return false;
}

int32_t newPowerLimit = 0;
if (VeDirect.veFrame.PPV < 10.0) {
// Not enough power
return false;
}

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;
}
return true;
}

newPowerLimit -= config.PowerLimiter_TargetPowerConsumption;
int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool consumeSolarPowerOnly)
{
CONFIG_T& config = Configuration.get();

int32_t newPowerLimit = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;

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);
}
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

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] 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;

MessageOutput.printf("[PowerLimiterClass::loop] powerMeter: %d W lastRequestedPowerLimit: %d\r\n",
powerMeter, _lastRequestedPowerLimit);
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<InverterAbstract> 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();
}

bool PowerLimiterClass::canUseDirectSolarPower()
{
CONFIG_T& config = Configuration.get();

if (!config.PowerLimiter_SolarPassTroughEnabled
|| !config.Vedirect_Enabled) {
return false;
}

if (VeDirect.veFrame.PPV < 10.0) {
// Not enough power
return false;
}

return true;
}

uint16_t PowerLimiterClass::getDirectSolarPower()
{
if (!canUseDirectSolarPower()) {
return 0;
}

return round(VeDirect.veFrame.PPV);
return VeDirect.veFrame.PPV;
}

float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter)
Expand Down

0 comments on commit 43dc10b

Please sign in to comment.