OpenDTU-old/src/PowerLimiterInverter.cpp
vaterlangen cf4a59c740 Feature: DPL: support overscaling on all inverters (#1286)
this change allows to support overscaling for all inverters, as the configuration of
inputs (which one is part of a particular MPPT) is now provided from withing the
code. this information is used to implement overscaling for any of the inverters
(which are generally compatible with OpenDTU(-OnBattery)).
2024-11-17 21:43:27 +01:00

312 lines
12 KiB
C++

#include "RestartHelper.h"
#include "MessageOutput.h"
#include "PowerLimiterInverter.h"
#include "PowerLimiterBatteryInverter.h"
#include "PowerLimiterSolarInverter.h"
std::unique_ptr<PowerLimiterInverter> PowerLimiterInverter::create(
bool verboseLogging, PowerLimiterInverterConfig const& config)
{
std::unique_ptr<PowerLimiterInverter> upInverter;
if (config.IsSolarPowered) {
upInverter = std::make_unique<PowerLimiterSolarInverter>(verboseLogging, config);
}
else {
upInverter = std::make_unique<PowerLimiterBatteryInverter>(verboseLogging, config);
}
if (nullptr == upInverter->_spInverter) { return nullptr; }
return std::move(upInverter);
}
PowerLimiterInverter::PowerLimiterInverter(bool verboseLogging, PowerLimiterInverterConfig const& config)
: _config(config)
, _verboseLogging(verboseLogging)
{
_spInverter = Hoymiles.getInverterBySerial(config.Serial);
if (!_spInverter) { return; }
snprintf(_serialStr, sizeof(_serialStr), "%0x%08x",
static_cast<uint32_t>((config.Serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(config.Serial & 0xFFFFFFFF));
snprintf(_logPrefix, sizeof(_logPrefix), "[DPL inverter %s]:", _serialStr);
}
bool PowerLimiterInverter::isEligible() const
{
if (!isReachable() || !isSendingCommandsEnabled()) { return false; }
// after startup, the limit effective at the inverter is not known. the
// respective message to request this info is only sent after a significant
// backoff (4 minutes). this is to avoid error messages to appear in the
// inverter's event log. we will wait until the current limit is known.
if (getCurrentLimitWatts() == 0) { return false; }
// the model-dependent maximum AC power output is only known after the
// first DevInfoSimpleCommand succeeded. we desperately need this info, so
// the inverter is not eligible until this value is known.
if (getInverterMaxPowerWatts() == 0) { return false; }
return true;
}
bool PowerLimiterInverter::update()
{
auto reset = [this]() -> bool {
_oTargetPowerState = std::nullopt;
_oTargetPowerLimitWatts = std::nullopt;
_oUpdateStartMillis = std::nullopt;
return false;
};
// do not reset _updateTimeouts below if no state change requested
if (!_oTargetPowerState.has_value() && !_oTargetPowerLimitWatts.has_value()) {
return reset();
}
if (!_oUpdateStartMillis.has_value()) {
_oUpdateStartMillis = millis();
}
if ((millis() - *_oUpdateStartMillis) > 30 * 1000) {
++_updateTimeouts;
MessageOutput.printf("%s timeout (%d in succession), "
"state transition pending: %s, limit pending: %s\r\n",
_logPrefix, _updateTimeouts,
(_oTargetPowerState.has_value()?"yes":"no"),
(_oTargetPowerLimitWatts.has_value()?"yes":"no"));
// NOTE that this is not always 5 minutes, since this counts timeouts,
// not absolute time. after any timeout, an update cycle ends. a new
// timeout can only happen after starting a new update cycle, which in
// turn is only started if the DPL did calculate a new limit, which in
// turn does not happen while the inverter is unreachable, no matter
// how long (a whole night) that might be.
if (_updateTimeouts >= 10) {
MessageOutput.printf("%s issuing restart command after update "
"timed out repeatedly\r\n", _logPrefix);
_spInverter->sendRestartControlRequest();
}
if (_updateTimeouts >= 20) {
MessageOutput.printf("%s restarting system since inverter is "
"unresponsive\r\n", _logPrefix);
RestartHelper.triggerRestart();
}
return reset();
}
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
auto switchPowerState = [this](bool transitionOn) -> bool {
// no power state transition requested at all
if (!_oTargetPowerState.has_value()) { return false; }
// the transition that may be started is not the one which is requested
if (transitionOn != *_oTargetPowerState) { return false; }
// wait for pending power command(s) to complete
auto lastPowerCommandState = _spInverter->PowerCommand()->getLastPowerCommandSuccess();
if (CMD_PENDING == lastPowerCommandState) {
return true;
}
// we need to wait for statistics that are more recent than
// the last power update command to reliably use isProducing()
auto lastPowerCommandMillis = _spInverter->PowerCommand()->getLastUpdateCommand();
auto lastStatisticsMillis = _spInverter->Statistics()->getLastUpdate();
if ((lastStatisticsMillis - lastPowerCommandMillis) > halfOfAllMillis) { return true; }
if (isProducing() != *_oTargetPowerState) {
MessageOutput.printf("%s %s inverter...\r\n", _logPrefix,
((*_oTargetPowerState)?"Starting":"Stopping"));
_spInverter->sendPowerControlRequest(*_oTargetPowerState);
return true;
}
_oTargetPowerState = std::nullopt; // target power state reached
return false;
};
// we use a lambda function here to be able to use return statements,
// which allows to avoid if-else-indentions and improves code readability
auto updateLimit = [this]() -> bool {
// no limit update requested at all
if (!_oTargetPowerLimitWatts.has_value()) { return false; }
// wait for pending limit command(s) to complete
auto lastLimitCommandState = _spInverter->SystemConfigPara()->getLastLimitCommandSuccess();
if (CMD_PENDING == lastLimitCommandState) {
return true;
}
float newRelativeLimit = static_cast<float>(*_oTargetPowerLimitWatts * 100) / getInverterMaxPowerWatts();
// if no limit command is pending, the SystemConfigPara does report the
// current limit, as the answer by the inverter to a limit command is
// the canonical source that updates the known current limit.
auto currentRelativeLimit = _spInverter->SystemConfigPara()->getLimitPercent();
// we assume having exclusive control over the inverter. if the last
// limit command was successful and sent after we started the last
// update cycle, we should assume *our* requested limit was set.
uint32_t lastLimitCommandMillis = _spInverter->SystemConfigPara()->getLastUpdateCommand();
if ((lastLimitCommandMillis - *_oUpdateStartMillis) < halfOfAllMillis &&
CMD_OK == lastLimitCommandState) {
MessageOutput.printf("%s actual limit is %.1f %% (%.0f W "
"respectively), effective %d ms after update started, "
"requested were %.1f %%\r\n",
_logPrefix, currentRelativeLimit,
(currentRelativeLimit * getInverterMaxPowerWatts() / 100),
(lastLimitCommandMillis - *_oUpdateStartMillis),
newRelativeLimit);
if (std::abs(newRelativeLimit - currentRelativeLimit) > 2.0) {
MessageOutput.printf("%s NOTE: expected limit of %.1f %% "
"and actual limit of %.1f %% mismatch by more than 2 %%, "
"is the DPL in exclusive control over the inverter?\r\n",
_logPrefix, newRelativeLimit, currentRelativeLimit);
}
_oTargetPowerLimitWatts = std::nullopt;
return false;
}
MessageOutput.printf("%s sending limit of %.1f %% (%.0f W "
"respectively), max output is %d W\r\n", _logPrefix,
newRelativeLimit, (newRelativeLimit * getInverterMaxPowerWatts() / 100),
getInverterMaxPowerWatts());
_spInverter->sendActivePowerControlRequest(newRelativeLimit,
PowerLimitControlType::RelativNonPersistent);
return true;
};
// disable power production as soon as possible.
// setting the power limit is less important once the inverter is off.
if (switchPowerState(false)) { return true; }
if (updateLimit()) { return true; }
// enable power production only after setting the desired limit
if (switchPowerState(true)) { return true; }
_updateTimeouts = 0;
return reset();
}
std::optional<uint32_t> PowerLimiterInverter::getLatestStatsMillis() const
{
uint32_t now = millis();
// concerns both power limits and start/stop/restart commands and is
// only updated if a respective response was received from the inverter
auto lastUpdateCmdAge = std::min(
now - _spInverter->SystemConfigPara()->getLastUpdateCommand(),
now - _spInverter->PowerCommand()->getLastUpdateCommand()
);
// we use _oStatsMillis to persist a stats update timestamp, as we are
// looking for the single oldest inverter stats which is still younger than
// the last update command. we shall not just return the actual youngest
// stats timestamp if newer stats arrived while no update command was sent
// in the meantime.
if (_oStatsMillis && lastUpdateCmdAge < (now - *_oStatsMillis)) {
_oStatsMillis.reset();
}
if (!_oStatsMillis) {
auto lastStatsMillis = _spInverter->Statistics()->getLastUpdate();
auto lastStatsAge = now - lastStatsMillis;
if (lastStatsAge > lastUpdateCmdAge) {
return std::nullopt;
}
_oStatsMillis = lastStatsMillis;
}
return _oStatsMillis;
}
uint16_t PowerLimiterInverter::getInverterMaxPowerWatts() const
{
return _spInverter->DevInfo()->getMaxPower();
}
uint16_t PowerLimiterInverter::getConfiguredMaxPowerWatts() const
{
return std::min(getInverterMaxPowerWatts(), _config.UpperPowerLimit);
}
uint16_t PowerLimiterInverter::getCurrentOutputAcWatts() const
{
return _spInverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
}
uint16_t PowerLimiterInverter::getExpectedOutputAcWatts() const
{
if (!_oTargetPowerLimitWatts && !_oTargetPowerState) {
// the inverter's output will not change due to commands being sent
return getCurrentOutputAcWatts();
}
return _expectedOutputAcWatts;
}
void PowerLimiterInverter::setMaxOutput()
{
_oTargetPowerState = true;
setAcOutput(getConfiguredMaxPowerWatts());
}
void PowerLimiterInverter::restart()
{
_spInverter->sendRestartControlRequest();
}
float PowerLimiterInverter::getDcVoltage(uint8_t input)
{
return _spInverter->Statistics()->getChannelFieldValue(TYPE_DC,
static_cast<ChannelNum_t>(input), FLD_UDC);
}
uint16_t PowerLimiterInverter::getCurrentLimitWatts() const
{
auto currentLimitPercent = _spInverter->SystemConfigPara()->getLimitPercent();
return static_cast<uint16_t>(currentLimitPercent * getInverterMaxPowerWatts() / 100);
}
void PowerLimiterInverter::debug() const
{
if (!_verboseLogging) { return; }
MessageOutput.printf(
"%s\r\n"
" %s-powered, %s %d W\r\n"
" lower/current/upper limit: %d/%d/%d W, output capability: %d W\r\n"
" sending commands %s, %s, %s\r\n"
" max reduction production/standby: %d/%d W, max increase: %d W\r\n"
" target limit/output/state: %i W (%s)/%d W/%s, %d update timeouts\r\n",
_logPrefix,
(isSolarPowered()?"solar":"battery"),
(isProducing()?"producing":"standing by at"), getCurrentOutputAcWatts(),
_config.LowerPowerLimit, getCurrentLimitWatts(), _config.UpperPowerLimit,
getInverterMaxPowerWatts(),
(isSendingCommandsEnabled()?"enabled":"disabled"),
(isReachable()?"reachable":"offline"),
(isEligible()?"eligible":"disqualified"),
getMaxReductionWatts(false), getMaxReductionWatts(true), getMaxIncreaseWatts(),
(_oTargetPowerLimitWatts.has_value()?*_oTargetPowerLimitWatts:-1),
(_oTargetPowerLimitWatts.has_value()?"update":"unchanged"),
getExpectedOutputAcWatts(),
(_oTargetPowerState.has_value()?(*_oTargetPowerState?"production":"standby"):"unchanged"),
getUpdateTimeouts()
);
}