Skip to content

Commit

Permalink
Feature: Added option to set runtime values to zero when inverter bec…
Browse files Browse the repository at this point in the history
…ames unreachable
  • Loading branch information
tbnobody committed Sep 2, 2023
1 parent 4f85d52 commit 6127fbe
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 0 deletions.
1 change: 1 addition & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ struct INVERTER_CONFIG_T {
bool Command_Enable;
bool Command_Enable_Night;
uint8_t ReachableThreshold;
bool ZeroRuntimeDataIfUnrechable;
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
};

Expand Down
5 changes: 5 additions & 0 deletions lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment
void RealTimeRunDataCommand::gotTimeout(InverterAbstract* inverter)
{
inverter->Statistics()->incrementRxFailureCount();

if (inverter->getZeroValuesIfUnreachable() && !inverter->isReachable()) {
Hoymiles.getMessageOutput()->println("Set runtime data to zero");
inverter->Statistics()->zeroRuntimeData();
}
}
10 changes: 10 additions & 0 deletions lib/Hoymiles/src/inverters/InverterAbstract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ uint8_t InverterAbstract::getReachableThreshold()
return _reachableThreshold;
}

void InverterAbstract::setZeroValuesIfUnreachable(bool enabled)
{
_zeroValuesIfUnreachable = enabled;
}

bool InverterAbstract::getZeroValuesIfUnreachable()
{
return _zeroValuesIfUnreachable;
}

bool InverterAbstract::sendChangeChannelRequest()
{
return false;
Expand Down
5 changes: 5 additions & 0 deletions lib/Hoymiles/src/inverters/InverterAbstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class InverterAbstract {
void setReachableThreshold(uint8_t threshold);
uint8_t getReachableThreshold();

void setZeroValuesIfUnreachable(bool enabled);
bool getZeroValuesIfUnreachable();

void clearRxFragmentBuffer();
void addRxFragment(uint8_t fragment[], uint8_t len);
uint8_t verifyAllFragments(CommandAbstract* cmd);
Expand Down Expand Up @@ -91,6 +94,8 @@ class InverterAbstract {

uint8_t _reachableThreshold = 3;

bool _zeroValuesIfUnreachable = false;

std::unique_ptr<AlarmLogParser> _alarmLogParser;
std::unique_ptr<DevInfoParser> _devInfoParser;
std::unique_ptr<PowerCommandParser> _powerCommandParser;
Expand Down
77 changes: 77 additions & 0 deletions lib/Hoymiles/src/parser/StatisticsParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ const calcFunc_t calcFunctions[] = {
{ CALC_IRR_CH, &calcIrradiation }
};

const FieldId_t runtimeFields[] = {
FLD_UDC,
FLD_IDC,
FLD_PDC,
FLD_UAC,
FLD_IAC,
FLD_PAC,
FLD_F,
FLD_T,
FLD_PF,
FLD_Q,
FLD_UAC_1N,
FLD_UAC_2N,
FLD_UAC_3N,
FLD_UAC_12,
FLD_UAC_23,
FLD_UAC_31,
FLD_IAC_1,
FLD_IAC_2,
FLD_IAC_3,
};

StatisticsParser::StatisticsParser()
: Parser()
{
Expand Down Expand Up @@ -150,6 +172,47 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch
return 0;
}

bool StatisticsParser::setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value)
{
const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId);
fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId);

if (pos == NULL) {
return false;
}

uint8_t ptr = pos->start + pos->num - 1;
uint8_t end = pos->start;
uint16_t div = pos->div;

if (CMD_CALC == div) {
return false;
}

if (setting != NULL) {
value -= setting->offset;
}
value *= static_cast<float>(div);

uint32_t val = 0;
if (pos->isSigned && pos->num == 2) {
val = static_cast<uint32_t>(static_cast<int16_t>(value));
} else if (pos->isSigned && pos->num == 4) {
val = static_cast<uint32_t>(static_cast<int32_t>(value));
} else {
val = static_cast<uint32_t>(value);
}

HOY_SEMAPHORE_TAKE();
do {
_payloadStatistic[ptr] = val;
val >>= 8;
} while (--ptr >= end);
HOY_SEMAPHORE_GIVE();

return true;
}

String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId)
{
return String(
Expand Down Expand Up @@ -253,6 +316,20 @@ uint32_t StatisticsParser::getRxFailureCount()
return _rxFailureCount;
}

void StatisticsParser::zeroRuntimeData()
{
// Loop all channels
for (auto& t : getChannelTypes()) {
for (auto& c : getChannelsByType(t)) {
for (uint8_t i = 0; i < (sizeof(runtimeFields) / sizeof(runtimeFields[0])); i++) {
if (hasChannelFieldValue(t, c, runtimeFields[i])) {
setChannelFieldValue(t, c, runtimeFields[i], 0);
}
}
}
}
}

static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0)
{
float yield = 0;
Expand Down
4 changes: 4 additions & 0 deletions lib/Hoymiles/src/parser/StatisticsParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ class StatisticsParser : public Parser {
const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId);
uint8_t getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId);

bool setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value);

float getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId);
void setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset);

Expand All @@ -139,6 +141,8 @@ class StatisticsParser : public Parser {
void incrementRxFailureCount();
uint32_t getRxFailureCount();

void zeroRuntimeData();

private:
uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {};
uint8_t _statisticLength = 0;
Expand Down
2 changes: 2 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ bool ConfigurationClass::write()
inv["command_enable"] = config.Inverter[i].Command_Enable;
inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night;
inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold;
inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable;

JsonArray channel = inv.createNestedArray("channel");
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
Expand Down Expand Up @@ -260,6 +261,7 @@ bool ConfigurationClass::read()
config.Inverter[i].Command_Enable = inv["command_enable"] | true;
config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true;
config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD;
config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false;

JsonArray channel = inv["channel"];
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
Expand Down
1 change: 1 addition & 0 deletions src/InverterSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ void InverterSettingsClass::init()

if (inv != nullptr) {
inv->setReachableThreshold(config.Inverter[i].ReachableThreshold);
inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable);
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower);
inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast<ChannelNum_t>(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset);
Expand Down
3 changes: 3 additions & 0 deletions src/WebApi_inverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
obj["command_enable"] = config.Inverter[i].Command_Enable;
obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night;
obj["reachable_threshold"] = config.Inverter[i].ReachableThreshold;
obj["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable;

auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
uint8_t max_channels;
Expand Down Expand Up @@ -284,6 +285,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
inverter.Command_Enable = root["command_enable"] | true;
inverter.Command_Enable_Night = root["command_enable_night"] | true;
inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD;
inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false;

arrayCount++;
}
Expand Down Expand Up @@ -315,6 +317,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
inv->setEnablePolling(inverter.Poll_Enable);
inv->setEnableCommands(inverter.Command_Enable);
inv->setReachableThreshold(inverter.ReachableThreshold);
inv->setZeroValuesIfUnreachable(inverter.ZeroRuntimeDataIfUnrechable);
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower);
inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast<ChannelNum_t>(c), FLD_YT, inverter.channel[c].YieldTotalOffset);
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@
"InverterHint": "*) Geben Sie die W<sub>p</sub> des Ports ein, um die Einstrahlung zu errechnen.",
"ReachableThreshold": "Erreichbarkeit Schwellenwert:",
"ReachableThresholdHint": "Legt fest, wie viele Anfragen fehlschlagen dürfen, bis der Wechselrichter als unerreichbar eingestuft wird.",
"ZeroRuntime": "Nulle Laufzeit Daten",
"ZeroRuntimeHint": "Nulle Laufzeit Daten (keine Ertragsdaten), wenn der Wechselrichter nicht erreichbar ist.",
"Cancel": "@:maintenancereboot.Cancel",
"Save": "@:dtuadmin.Save",
"DeleteMsg": "Soll der Wechselrichter \"{name}\" mit der Seriennummer {serial} wirklich gelöscht werden?",
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@
"InverterHint": "*) Enter the W<sub>p</sub> of the channel to calculate irradiation.",
"ReachableThreshold": "Reachable Threshold:",
"ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.",
"ZeroRuntime": "Zero runtime data",
"ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.",
"Cancel": "@:maintenancereboot.Cancel",
"Save": "@:dtuadmin.Save",
"DeleteMsg": "Are you sure you want to delete the inverter \"{name}\" with serial number {serial}?",
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@
"InverterHint": "*) Entrez le W<sub>p</sub> du canal pour calculer l'irradiation.",
"ReachableThreshold": "Reachable Threshold:",
"ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.",
"ZeroRuntime": "Zero runtime data",
"ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.",
"Cancel": "@:maintenancereboot.Cancel",
"Save": "@:dtuadmin.Save",
"DeleteMsg": "Êtes-vous sûr de vouloir supprimer l'onduleur \"{name}\" avec le numéro de série \"{serial}\" ?",
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/views/InverterAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@
v-model="selectedInverterData.reachable_threshold"
type="number" min="1" max="100"
:tooltip="$t('inverteradmin.ReachableThresholdHint')" wide />

<InputElement :label="$t('inverteradmin.ZeroRuntime')"
v-model="selectedInverterData.zero_runtime"
type="checkbox"
:tooltip="$t('inverteradmin.ZeroRuntimeHint')" wide/>
</div>
</div>
</form>
Expand Down Expand Up @@ -257,6 +262,7 @@ declare interface Inverter {
command_enable: boolean;
command_enable_night: boolean;
reachable_threshold: number;
zero_runtime: boolean;
channel: Array<Channel>;
}
Expand Down

0 comments on commit 6127fbe

Please sign in to comment.