mirror of https://github.com/lumapu/ahoy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
860 lines
32 KiB
860 lines
32 KiB
//-----------------------------------------------------------------------------
|
|
// 2024 Ahoy, https://ahoydtu.de
|
|
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#if defined(PLUGIN_ZEROEXPORT)
|
|
|
|
#ifndef __ZEROEXPORT__
|
|
#define __ZEROEXPORT__
|
|
|
|
#include <HTTPClient.h>
|
|
#include <base64.h>
|
|
#include <string.h>
|
|
|
|
#include "AsyncJson.h"
|
|
#include "powermeter.h"
|
|
|
|
template <class HMSYSTEM>
|
|
|
|
class ZeroExport {
|
|
public:
|
|
/** ZeroExport
|
|
* constructor
|
|
*/
|
|
ZeroExport() {}
|
|
|
|
/** ~ZeroExport
|
|
* destructor
|
|
*/
|
|
~ZeroExport() {}
|
|
|
|
/** setup
|
|
* Initialisierung
|
|
* @param *cfg
|
|
* @param *sys
|
|
* @param *config
|
|
* @param *api
|
|
* @param *mqtt
|
|
* @returns void
|
|
*/
|
|
void setup(IApp *app, uint32_t *timestamp, zeroExport_t *cfg, HMSYSTEM *sys, settings_t *config, RestApiType *api, PubMqttType *mqtt) {
|
|
mApp = app;
|
|
mTimestamp = timestamp;
|
|
mCfg = cfg;
|
|
mSys = sys;
|
|
mConfig = config;
|
|
mApi = api;
|
|
mMqtt = mqtt;
|
|
|
|
mIsInitialized = mPowermeter.setup(mCfg, mqtt, &mLog);
|
|
}
|
|
|
|
/** loop
|
|
* Arbeitsschleife
|
|
* @param void
|
|
* @returns void
|
|
* @todo emergency
|
|
*/
|
|
void loop(void) {
|
|
if ((!mIsInitialized) || (!mCfg->enabled)) return;
|
|
|
|
mPowermeter.loop();
|
|
// sendLog();
|
|
clearLog();
|
|
|
|
// Takt
|
|
unsigned long Tsp = millis();
|
|
if (mLastRun > (Tsp - 1000)) return;
|
|
mLastRun = Tsp;
|
|
|
|
if (mCfg->debug) DBGPRINTLN(F("Takt:"));
|
|
|
|
// Exit if Queue is empty
|
|
zeroExportQueue_t Queue;
|
|
if (!getQueue(&Queue)) return;
|
|
|
|
if (mCfg->debug) DBGPRINTLN(F("Queue:"));
|
|
|
|
// Load Data from Queue
|
|
uint8_t group = Queue.group;
|
|
uint8_t inv = Queue.inv;
|
|
zeroExportGroup_t *CfgGroup = &mCfg->groups[group];
|
|
zeroExportGroupInverter_t *CfgGroupInv = &CfgGroup->inverters[inv];
|
|
Inverter<> *iv = mSys->getInverterByPos(Queue.id);
|
|
|
|
mLog["g"] = group;
|
|
mLog["i"] = inv;
|
|
|
|
// Check Data->iv
|
|
if (!iv->isAvailable()) {
|
|
if (mCfg->debug) {
|
|
mLog["nA"] = "!isAvailable";
|
|
sendLog();
|
|
}
|
|
clearLog();
|
|
return;
|
|
}
|
|
|
|
// Check Data->waitAck
|
|
if (CfgGroupInv->waitAck > 0) {
|
|
if (mCfg->debug) {
|
|
mLog["wA"] = CfgGroupInv->waitAck;
|
|
sendLog();
|
|
}
|
|
clearLog();
|
|
return;
|
|
}
|
|
|
|
// Calc Data->groupPower
|
|
uint16_t groupPower = 0;
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
groupPower += mCfg->groups[group].inverters[inv].power;
|
|
}
|
|
mLog["gP"] = groupPower;
|
|
|
|
// Calc Data->groupLimit
|
|
uint16_t groupLimit = 0;
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
groupLimit += mCfg->groups[group].inverters[inv].limit;
|
|
}
|
|
mLog["gL"] = groupLimit;
|
|
|
|
// Batteryprotection
|
|
mLog["bEn"] = (uint8_t)CfgGroup->battCfg;
|
|
switch (CfgGroup->battCfg) {
|
|
case zeroExportBatteryCfg::none:
|
|
if (CfgGroup->battSwitch != true) {
|
|
CfgGroup->battSwitch = true;
|
|
mLog["bA"] = "turn on";
|
|
}
|
|
break;
|
|
case zeroExportBatteryCfg::invUdc:
|
|
case zeroExportBatteryCfg::mqttU:
|
|
case zeroExportBatteryCfg::mqttSoC:
|
|
if (CfgGroup->battSwitch != true) {
|
|
if (CfgGroup->battValue > CfgGroup->battLimitOn) {
|
|
CfgGroup->battSwitch = true;
|
|
mLog["bA"] = "turn on";
|
|
}
|
|
if ((CfgGroup->battValue > CfgGroup->battLimitOff) && (CfgGroupInv->power > 0)) {
|
|
CfgGroup->battSwitch = true;
|
|
mLog["bA"] = "turn on";
|
|
}
|
|
} else {
|
|
if (CfgGroup->battValue < CfgGroup->battLimitOff) {
|
|
CfgGroup->battSwitch = false;
|
|
mLog["bA"] = "turn off";
|
|
}
|
|
}
|
|
mLog["bU"] = ah::round1(CfgGroup->battValue);
|
|
break;
|
|
default:
|
|
if (CfgGroup->battSwitch == true) {
|
|
CfgGroup->battSwitch = false;
|
|
mLog["bA"] = "turn off";
|
|
}
|
|
break;
|
|
}
|
|
mLog["bSw"] = CfgGroup->battSwitch;
|
|
|
|
// Controller
|
|
|
|
// Führungsgröße w in Watt
|
|
int16_t w = CfgGroup->setPoint;
|
|
mLog["w"] = w;
|
|
|
|
// Regelgröße x in Watt
|
|
int16_t x = 0.0;
|
|
if (CfgGroup->minimum) {
|
|
x = mPowermeter.getDataMIN(group);
|
|
} else {
|
|
x = mPowermeter.getDataAVG(group);
|
|
}
|
|
mLog["x"] = x;
|
|
|
|
// Regelabweichung e in Watt
|
|
int16_t e = w - x;
|
|
mLog["e"] = e;
|
|
|
|
// Keine Regelung innerhalb der Toleranzgrenzen
|
|
if ((e < CfgGroup->powerTolerance) && (e > -CfgGroup->powerTolerance)) {
|
|
e = 0;
|
|
mLog["eK"] = e;
|
|
sendLog();
|
|
clearLog();
|
|
return;
|
|
}
|
|
|
|
// Regler
|
|
float Kp = CfgGroup->Kp;
|
|
float Ki = CfgGroup->Ki;
|
|
float Kd = CfgGroup->Kd;
|
|
unsigned long Ta = Tsp - CfgGroup->lastRefresh;
|
|
CfgGroup->lastRefresh = Tsp;
|
|
int16_t yP = Kp * e;
|
|
CfgGroup->eSum += e;
|
|
int16_t yI = Ki * Ta * CfgGroup->eSum;
|
|
if (Ta == 0) {
|
|
mLog["Error"] = "Ta = 0";
|
|
sendLog();
|
|
clearLog();
|
|
return;
|
|
}
|
|
int16_t yD = Kd * (e - CfgGroup->eOld) / Ta;
|
|
|
|
if (mCfg->debug) {
|
|
mLog["Kp"] = Kp;
|
|
mLog["Ki"] = Ki;
|
|
mLog["Kd"] = Kd;
|
|
mLog["Ta"] = Ta;
|
|
mLog["yP"] = yP;
|
|
mLog["yI"] = yI;
|
|
mLog["eSum"] = CfgGroup->eSum;
|
|
mLog["yD"] = yD;
|
|
mLog["eOld"] = CfgGroup->eOld;
|
|
}
|
|
|
|
CfgGroup->eOld = e;
|
|
int16_t y = yP + yI + yD;
|
|
|
|
mLog["y"] = y;
|
|
|
|
// Regelbegrenzung
|
|
// TODO: Hier könnte man den maximalen Sprung begrenzen
|
|
|
|
// Stellgröße y in W
|
|
CfgGroupInv->limitNew += y;
|
|
|
|
// Check
|
|
|
|
if (CfgGroupInv->action == zeroExportAction_t::doNone) {
|
|
if ((CfgGroup->battSwitch == true) && (CfgGroupInv->limitNew > CfgGroupInv->powerMin) && (CfgGroupInv->power == 0) && (mCfg->sleep != true) && (CfgGroup->sleep != true)) {
|
|
if (CfgGroupInv->actionTimer < 0) CfgGroupInv->actionTimer = 0;
|
|
if (CfgGroupInv->actionTimer == 0) CfgGroupInv->actionTimer = 1;
|
|
if (CfgGroupInv->actionTimer > 10) {
|
|
CfgGroupInv->action = zeroExportAction_t::doTurnOn;
|
|
mLog["do"] = "doTurnOn";
|
|
}
|
|
}
|
|
if ((CfgGroupInv->turnOff) && (CfgGroupInv->limitNew <= 0) && (CfgGroupInv->power > 0)) {
|
|
if (CfgGroupInv->actionTimer > 0) CfgGroupInv->actionTimer = 0;
|
|
if (CfgGroupInv->actionTimer == 0) CfgGroupInv->actionTimer = -1;
|
|
if (CfgGroupInv->actionTimer < 30) {
|
|
CfgGroupInv->action = zeroExportAction_t::doTurnOff;
|
|
mLog["do"] = "doTurnOff";
|
|
}
|
|
}
|
|
if (((CfgGroup->battSwitch == false) || (mCfg->sleep == true) || (CfgGroup->sleep == true)) && (CfgGroupInv->power > 0)) {
|
|
CfgGroupInv->action = zeroExportAction_t::doTurnOff;
|
|
mLog["do"] = "sleep";
|
|
}
|
|
}
|
|
mLog["doT"] = CfgGroupInv->action;
|
|
|
|
if (CfgGroupInv->action == zeroExportAction_t::doNone) {
|
|
mLog["l"] = CfgGroupInv->limit;
|
|
mLog["ln"] = CfgGroupInv->limitNew;
|
|
|
|
// groupMax
|
|
uint16_t otherIvLimit = groupLimit - CfgGroupInv->limit;
|
|
if ((otherIvLimit + CfgGroupInv->limitNew) > CfgGroup->powerMax) {
|
|
CfgGroupInv->limitNew = CfgGroup->powerMax - otherIvLimit;
|
|
}
|
|
if (mCfg->debug) mLog["gPM"] = CfgGroup->powerMax;
|
|
|
|
// PowerMax
|
|
uint16_t powerMax = 100;
|
|
if (CfgGroupInv->MaxPower > 100) powerMax = CfgGroupInv->MaxPower;
|
|
if (CfgGroupInv->powerMax < powerMax) powerMax = CfgGroupInv->powerMax;
|
|
if (CfgGroupInv->limitNew > powerMax) CfgGroupInv->limitNew = powerMax;
|
|
|
|
// PowerMin
|
|
uint16_t powerMin = CfgGroupInv->MaxPower / 100 * 2;
|
|
if (CfgGroupInv->powerMin > powerMin) powerMin = CfgGroupInv->powerMin;
|
|
if (CfgGroupInv->limitNew < powerMin) CfgGroupInv->limitNew = powerMin;
|
|
|
|
// Sleep
|
|
if (mCfg->sleep || CfgGroup->sleep) CfgGroupInv->limitNew = powerMin;
|
|
|
|
// Standby -> PowerMin
|
|
if (CfgGroupInv->power == 0) CfgGroupInv->limitNew = powerMin;
|
|
|
|
// Mindeständerung ZEROEXPORT_GROUP_WR_LIMIT_MIN_DIFF
|
|
if ((CfgGroupInv->limitNew < (CfgGroupInv->limit + ZEROEXPORT_GROUP_WR_LIMIT_MIN_DIFF)) && (CfgGroupInv->limitNew > (CfgGroupInv->limit - ZEROEXPORT_GROUP_WR_LIMIT_MIN_DIFF))) CfgGroupInv->limitNew = CfgGroupInv->limit;
|
|
|
|
if (CfgGroupInv->limit != CfgGroupInv->limitNew) CfgGroupInv->action = zeroExportAction_t::doActivePowerContr;
|
|
|
|
if ((CfgGroupInv->limit == powerMin) && (CfgGroupInv->power == 0)) {
|
|
CfgGroupInv->action = zeroExportAction_t::doNone;
|
|
if (!mCfg->debug) {
|
|
clearLog();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// CfgGroupInv->actionTimer = 0;
|
|
// TODO: Timer stoppen wenn Limit gesetzt wird.
|
|
mLog["lN"] = CfgGroupInv->limitNew;
|
|
|
|
CfgGroupInv->limit = CfgGroupInv->limitNew;
|
|
}
|
|
|
|
// doAction
|
|
mLog["a"] = CfgGroupInv->action;
|
|
|
|
switch (CfgGroupInv->action) {
|
|
case zeroExportAction_t::doRestart:
|
|
if (iv->setDevControlRequest(Restart)) {
|
|
mApp->triggerTickSend(iv->id);
|
|
CfgGroupInv->waitAck = 120;
|
|
CfgGroupInv->action = zeroExportAction_t::doNone;
|
|
CfgGroupInv->actionTimer = 0;
|
|
CfgGroupInv->actionTimestamp = Tsp;
|
|
}
|
|
break;
|
|
case zeroExportAction_t::doTurnOn:
|
|
if (iv->setDevControlRequest(TurnOn)) {
|
|
mApp->triggerTickSend(iv->id);
|
|
CfgGroupInv->waitAck = 120;
|
|
CfgGroupInv->action = zeroExportAction_t::doNone;
|
|
CfgGroupInv->actionTimer = 0;
|
|
CfgGroupInv->actionTimestamp = Tsp;
|
|
}
|
|
break;
|
|
case zeroExportAction_t::doTurnOff:
|
|
if (iv->setDevControlRequest(TurnOff)) {
|
|
mApp->triggerTickSend(iv->id);
|
|
CfgGroupInv->waitAck = 120;
|
|
CfgGroupInv->action = zeroExportAction_t::doNone;
|
|
CfgGroupInv->actionTimer = 0;
|
|
CfgGroupInv->actionTimestamp = Tsp;
|
|
}
|
|
break;
|
|
case zeroExportAction_t::doActivePowerContr:
|
|
iv->powerLimit[0] = static_cast<uint16_t>(CfgGroupInv->limit * 10.0);
|
|
iv->powerLimit[1] = AbsolutNonPersistent;
|
|
if (iv->setDevControlRequest(ActivePowerContr)) {
|
|
mApp->triggerTickSend(iv->id);
|
|
CfgGroupInv->waitAck = 60;
|
|
CfgGroupInv->action = zeroExportAction_t::doNone;
|
|
CfgGroupInv->actionTimer = 0;
|
|
CfgGroupInv->actionTimestamp = Tsp;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
sendLog();
|
|
|
|
// MQTT - Powermeter
|
|
if (mMqtt->isConnected()) {
|
|
mqttPublish(String("zero/state/groups/" + String(group) + "/inverter/" + String(inv)).c_str(), mDocLog.as<std::string>().c_str());
|
|
}
|
|
|
|
clearLog();
|
|
|
|
return;
|
|
}
|
|
|
|
/** tickSecond
|
|
* Time pulse every second
|
|
* @param void
|
|
* @returns void
|
|
* @todo Eventuell ein waitAck für alle 3 Set-Befehle
|
|
* @todo Eventuell ein waitAck für alle Inverter einer Gruppe
|
|
*/
|
|
void tickSecond() {
|
|
if ((!mIsInitialized) || (!mCfg->enabled)) return;
|
|
|
|
// Reduce WaitAck every second
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups[group].enabled) continue;
|
|
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
if (!mCfg->groups[group].inverters[inv].enabled) continue;
|
|
|
|
if (mCfg->groups[group].inverters[inv].waitAck > 0) {
|
|
mCfg->groups[group].inverters[inv].waitAck--;
|
|
}
|
|
|
|
if (mCfg->groups[group].inverters[inv].actionTimer > 0) mCfg->groups[group].inverters[inv].actionTimer++;
|
|
if (mCfg->groups[group].inverters[inv].actionTimer < 0) mCfg->groups[group].inverters[inv].actionTimer--;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** tickerMidnight
|
|
* Time pulse Midnicht
|
|
* Reboots Inverter at Midnight to reset YieldDay and clean start environment
|
|
* @param void
|
|
* @returns void
|
|
* @todo activate
|
|
* @todo tickMidnight wird nicht nur um Mitternacht ausgeführt sondern auch beim Reboot von Ahoy.
|
|
* @todo Reboot der Inverter um Mitternacht in Ahoy selbst verschieben mit separater Config-Checkbox
|
|
* @todo Ahoy Config-Checkbox Reboot Inverter at Midnight beim groupInit() automatisch setzen.
|
|
*/
|
|
void tickMidnight(void) {
|
|
if ((!mIsInitialized) || (!mCfg->enabled)) return;
|
|
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups[group].enabled) continue;
|
|
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
if (!mCfg->groups[group].inverters[inv].enabled) continue;
|
|
|
|
mCfg->groups[group].inverters[inv].action = zeroExportAction_t::doRestart;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** eventAckSetLimit
|
|
* Reset waiting time limit
|
|
* @param iv
|
|
* @returns void
|
|
*/
|
|
void eventAckSetLimit(Inverter<> *iv) {
|
|
if ((!mIsInitialized) || (!mCfg->enabled)) return;
|
|
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups[group].enabled) continue;
|
|
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
if (!mCfg->groups[group].inverters[inv].enabled) continue;
|
|
|
|
if (iv->id == (uint8_t)mCfg->groups[group].inverters[inv].id) {
|
|
mLog["g"] = group;
|
|
mLog["i"] = inv;
|
|
mCfg->groups[group].inverters[inv].waitAck = 0;
|
|
mLog["wA"] = mCfg->groups[group].inverters[inv].waitAck;
|
|
if (iv->actPowerLimit != 0xffff) {
|
|
mLog["l"] = mCfg->groups[group].inverters[inv].limit;
|
|
mCfg->groups[group].inverters[inv].limit = iv->actPowerLimit;
|
|
mLog["lF"] = mCfg->groups[group].inverters[inv].limit;
|
|
}
|
|
sendLog();
|
|
clearLog();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** eventAckSetPower
|
|
* Reset waiting time power
|
|
* @param iv
|
|
* @returns void
|
|
*/
|
|
void eventAckSetPower(Inverter<> *iv) {
|
|
if ((!mIsInitialized) || (!mCfg->enabled)) return;
|
|
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups[group].enabled) continue;
|
|
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
if (!mCfg->groups[group].inverters[inv].enabled) continue;
|
|
|
|
if (iv->id == mCfg->groups[group].inverters[inv].id) {
|
|
mLog["g"] = group;
|
|
mLog["i"] = inv;
|
|
mCfg->groups[group].inverters[inv].waitAck = 0;
|
|
mLog["wA"] = mCfg->groups[group].inverters[inv].waitAck;
|
|
sendLog();
|
|
clearLog();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** eventAckSetReboot
|
|
* Reset waiting time reboot
|
|
* @param iv
|
|
* @returns void
|
|
*/
|
|
void eventAckSetReboot(Inverter<> *iv) {
|
|
if ((!mIsInitialized) || (!mCfg->enabled)) return;
|
|
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups[group].enabled) continue;
|
|
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
if (!mCfg->groups[group].inverters[inv].enabled) continue;
|
|
|
|
if (iv->id == mCfg->groups[group].inverters[inv].id) {
|
|
mLog["g"] = group;
|
|
mLog["i"] = inv;
|
|
mCfg->groups[group].inverters[inv].waitAck = 0;
|
|
mLog["wA"] = mCfg->groups[group].inverters[inv].waitAck;
|
|
sendLog();
|
|
clearLog();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** eventNewDataAvailable
|
|
*
|
|
* @param iv
|
|
* @returns void
|
|
*/
|
|
void eventNewDataAvailable(Inverter<> *iv) {
|
|
if ((!mIsInitialized) || (!mCfg->enabled)) return;
|
|
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
zeroExportGroup_t *CfgGroup = &mCfg->groups[group];
|
|
if (!CfgGroup->enabled) continue;
|
|
|
|
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
|
|
zeroExportGroupInverter_t *CfgGroupInv = &CfgGroup->inverters[inv];
|
|
if (!CfgGroupInv->enabled) continue;
|
|
if (CfgGroupInv->id != iv->id) continue;
|
|
|
|
mLog["g"] = group;
|
|
mLog["i"] = inv;
|
|
|
|
// TODO: Ist nach eventAckSetLimit verschoben
|
|
// if (iv->actPowerLimit != 0xffff) {
|
|
// mLog["l"] = mCfg->groups[group].inverters[inv].limit;
|
|
// mCfg->groups[group].inverters[inv].limit = iv->actPowerLimit;
|
|
// mLog["lF"] = mCfg->groups[group].inverters[inv].limit;
|
|
//}
|
|
|
|
// TODO: Es dauert bis getMaxPower übertragen wird.
|
|
if (iv->getMaxPower() > 0) {
|
|
CfgGroupInv->MaxPower = iv->getMaxPower();
|
|
mLog["pM"] = CfgGroupInv->MaxPower;
|
|
}
|
|
|
|
record_t<> *rec;
|
|
rec = iv->getRecordStruct(RealTimeRunData_Debug);
|
|
if (iv->getLastTs(rec) > (millis() - 15000)) {
|
|
CfgGroupInv->power = iv->getChannelFieldValue(CH0, FLD_PAC, rec);
|
|
mLog["p"] = CfgGroupInv->power;
|
|
|
|
CfgGroupInv->dcVoltage = iv->getChannelFieldValue(CH1, FLD_UDC, rec);
|
|
mLog["bU"] = ah::round1(CfgGroupInv->dcVoltage);
|
|
|
|
// Batterieüberwachung - Überwachung über die DC-Spannung am PV-Eingang 1 des Inverters
|
|
if (CfgGroup->battCfg == zeroExportBatteryCfg::invUdc) {
|
|
if ((CfgGroup->battSwitch == false) && (CfgGroup->battValue < CfgGroupInv->dcVoltage)) {
|
|
CfgGroup->battValue = CfgGroupInv->dcVoltage;
|
|
}
|
|
if ((CfgGroup->battSwitch == true) && (CfgGroup->battValue > CfgGroupInv->dcVoltage)) {
|
|
CfgGroup->battValue = CfgGroupInv->dcVoltage;
|
|
}
|
|
}
|
|
|
|
// Fallschirm 2: Für nicht übernommene Limits bzw. nicht regelnde Inverter
|
|
// Bisher ist nicht geklärt ob der Inverter das Limit bestätigt hat
|
|
// Erstmalig aufgetreten bei @knickohr am 28.04.2024 ... l=300 pM=300, p=9
|
|
if (CfgGroupInv->MaxPower > 0) {
|
|
uint16_t limitPercent = 100 / CfgGroupInv->MaxPower * CfgGroupInv->limit;
|
|
uint16_t powerPercent = 100 / CfgGroupInv->MaxPower * CfgGroupInv->power;
|
|
uint16_t delta = abs(limitPercent - powerPercent);
|
|
if ((delta > 10) && (CfgGroupInv->power > 0)) {
|
|
mLog["delta"] = delta;
|
|
unsigned long delay = iv->getLastTs(rec) - CfgGroupInv->actionTimestamp;
|
|
mLog["delay"] = delay;
|
|
if (delay > 30000) {
|
|
CfgGroupInv->action = zeroExportAction_t::doActivePowerContr;
|
|
mLog["do"] = "doActivePowerContr";
|
|
}
|
|
if (delay > 60000) {
|
|
CfgGroupInv->action = zeroExportAction_t::doRestart;
|
|
mLog["do"] = "doRestart";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
zeroExportQueue_t Entry;
|
|
Entry.group = group;
|
|
Entry.inv = inv;
|
|
Entry.id = iv->id;
|
|
putQueue(Entry);
|
|
|
|
sendLog();
|
|
clearLog();
|
|
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
/** onMqttConnect
|
|
* Connect section
|
|
* @returns void
|
|
*/
|
|
void onMqttConnect(void) {
|
|
if (!mCfg->enabled) return;
|
|
|
|
mPowermeter.onMqttConnect();
|
|
|
|
// "topic":"userdefined battSoCTopic"
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups[group].enabled) continue;
|
|
|
|
if ((!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttU) && (!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttSoC)) continue;
|
|
|
|
if (!strcmp(mCfg->groups[group].battTopic, "")) continue;
|
|
|
|
mMqtt->subscribeExtern(String(mCfg->groups[group].battTopic).c_str(), QOS_2);
|
|
}
|
|
}
|
|
|
|
/** onMqttMessage
|
|
* Subscribe section
|
|
* @param
|
|
* @returns void
|
|
*/
|
|
void onMqttMessage(JsonObject obj) {
|
|
if (!mIsInitialized) return;
|
|
|
|
mPowermeter.onMqttMessage(obj);
|
|
|
|
String topic = String(obj["topic"]);
|
|
|
|
// "topic":"userdefined battSoCTopic"
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups[group].enabled) continue;
|
|
|
|
if ((!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttU) && (!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttSoC)) continue;
|
|
|
|
if (!strcmp(mCfg->groups[group].battTopic, "")) continue;
|
|
|
|
if (strcmp(mCfg->groups[group].battTopic, String(topic).c_str())) {
|
|
mCfg->groups[group].battValue = (bool)obj["val"];
|
|
mLog["k"] = mCfg->groups[group].battTopic;
|
|
mLog["v"] = mCfg->groups[group].battValue;
|
|
}
|
|
}
|
|
|
|
// "topic":"ctrl/zero"
|
|
if (topic.indexOf("ctrl/zero") == -1) return;
|
|
|
|
if (mCfg->debug) mLog["d"] = obj;
|
|
|
|
if (obj["path"] == "ctrl" && obj["cmd"] == "zero") {
|
|
int8_t topicGroup = getGroupFromTopic(topic.c_str());
|
|
int8_t topicInverter = getInverterFromTopic(topic.c_str());
|
|
|
|
if (topicGroup != -1) mLog["g"] = topicGroup;
|
|
if (topicInverter == -1) mLog["i"] = topicInverter;
|
|
|
|
mLog["k"] = topic;
|
|
|
|
// "topic":"ctrl/zero/enabled"
|
|
if (topic.indexOf("ctrl/zero/enabled") != -1) mCfg->enabled = mLog["v"] = (bool)obj["val"];
|
|
|
|
// "topic":"ctrl/zero/sleep"
|
|
else if (topic.indexOf("ctrl/zero/sleep") != -1) mCfg->sleep = mLog["v"] = (bool)obj["val"];
|
|
|
|
else if ((topicGroup >= 0) && (topicGroup < ZEROEXPORT_MAX_GROUPS))
|
|
{
|
|
String stopicGroup = String(topicGroup);
|
|
|
|
// "topic":"ctrl/zero/groups/+/enabled"
|
|
if (topic.endsWith("/enabled")) mCfg->groups[topicGroup].enabled = mLog["v"] = (bool)obj["val"];
|
|
|
|
// "topic":"ctrl/zero/groups/+/sleep"
|
|
else if (topic.endsWith("/sleep")) mCfg->groups[topicGroup].sleep = mLog["v"] = (bool)obj["val"];
|
|
|
|
// Auf Eis gelegt, dafür 2 Gruppen mehr
|
|
// 0.8.103008.2
|
|
// // "topic":"ctrl/zero/groups/+/pm_ip"
|
|
// if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/pm_ip") != -1) {
|
|
// snprintf(mCfg->groups[topicGroup].pm_url, ZEROEXPORT_GROUP_MAX_LEN_PM_URL, "%s", obj[F("val")].as<const char *>());
|
|
/// TODO:
|
|
// snprintf(mCfg->groups[topicGroup].pm_url, ZEROEXPORT_GROUP_MAX_LEN_PM_URL, "%s", obj[F("val")].as<const char *>());
|
|
// strncpy(mCfg->groups[topicGroup].pm_url, obj[F("val")], ZEROEXPORT_GROUP_MAX_LEN_PM_URL);
|
|
// strncpy(mCfg->groups[topicGroup].pm_url, String(obj[F("val")]).c_str(), ZEROEXPORT_GROUP_MAX_LEN_PM_URL);
|
|
// snprintf(mCfg->groups[topicGroup].pm_url, ZEROEXPORT_GROUP_MAX_LEN_PM_URL, "%s", String(obj[F("val")]).c_str());
|
|
// mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/pm_ip";
|
|
// mLog["v"] = mCfg->groups[topicGroup].pm_url;
|
|
// }
|
|
//
|
|
// // "topic":"ctrl/zero/groups/+/pm_jsonPath"
|
|
// if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/pm_jsonPath") != -1) {
|
|
/// TODO:
|
|
// snprintf(mCfg->groups[topicGroup].pm_jsonPath, ZEROEXPORT_GROUP_MAX_LEN_PM_JSONPATH, "%s", obj[F("val")].as<const char *>());
|
|
// mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/pm_jsonPath";
|
|
// mLog["v"] = mCfg->groups[topicGroup].pm_jsonPath;
|
|
// }
|
|
|
|
// "topic":"ctrl/zero/groups/+/battery/switch"
|
|
else if (topic.endsWith("/battery/switch")) mCfg->groups[topicGroup].battSwitch = mLog["v"] = (bool)obj["val"];
|
|
|
|
else if (topic.indexOf("/advanced/") != -1)
|
|
{
|
|
// "topic":"ctrl/zero/groups/+/advanced/setPoint"
|
|
if (topic.endsWith("/setPoint")) mCfg->groups[topicGroup].setPoint = mLog["v"] = (int16_t)obj["val"];
|
|
|
|
// "topic":"ctrl/zero/groups/+/advanced/powerTolerance"
|
|
else if (topic.endsWith("/powerTolerance")) mCfg->groups[topicGroup].powerTolerance = mLog["v"] = (uint8_t)obj["val"];
|
|
|
|
// "topic":"ctrl/zero/groups/+/advanced/powerMax"
|
|
else if (topic.endsWith("/powerMax")) mCfg->groups[topicGroup].powerMax = mLog["v"] = (uint16_t)obj["val"];
|
|
}
|
|
else if (topic.indexOf("/inverter/") != -1)
|
|
{
|
|
if ((topicInverter >= 0) && (topicInverter < ZEROEXPORT_GROUP_MAX_INVERTERS))
|
|
{
|
|
// "topic":"ctrl/zero/groups/+/inverter/+/enabled"
|
|
if (topic.endsWith("/enabled")) mCfg->groups[topicGroup].inverters[topicInverter].enabled = mLog["v"] = (bool)obj["val"];
|
|
|
|
// "topic":"ctrl/zero/groups/+/inverter/+/powerMin"
|
|
else if (topic.endsWith("/powerMin")) mCfg->groups[topicGroup].inverters[topicInverter].powerMin = mLog["v"] = (uint16_t)obj["val"];
|
|
|
|
// "topic":"ctrl/zero/groups/+/inverter/+/powerMax"
|
|
else if (topic.endsWith("/powerMax")) mCfg->groups[topicGroup].inverters[topicInverter].powerMax = mLog["v"] = (uint16_t)obj["val"];
|
|
else mLog["k"] = "error";
|
|
}
|
|
}
|
|
else {
|
|
mLog["k"] = "error";
|
|
}
|
|
}
|
|
}
|
|
|
|
sendLog();
|
|
clearLog();
|
|
return;
|
|
}
|
|
|
|
private:
|
|
/** putQueue
|
|
* Fügt einen Eintrag zur Queue hinzu.
|
|
* @param item
|
|
* @returns true/false
|
|
*/
|
|
bool putQueue(zeroExportQueue_t item) {
|
|
if ((mQueueIdxWrite + 1) % ZEROEXPORT_MAX_QUEUE_ENTRIES == mQueueIdxRead) return false;
|
|
mQueue[mQueueIdxWrite] = item;
|
|
mQueueIdxWrite = (mQueueIdxWrite + 1) % ZEROEXPORT_MAX_QUEUE_ENTRIES;
|
|
return true;
|
|
}
|
|
|
|
/** getQueue
|
|
* Holt einen Eintrag aus der Queue.
|
|
* @param *value
|
|
* @returns true/false
|
|
*/
|
|
bool getQueue(zeroExportQueue_t *value) {
|
|
if (mQueueIdxRead == mQueueIdxWrite) return false;
|
|
*value = mQueue[mQueueIdxRead];
|
|
mQueueIdxRead = (mQueueIdxRead + 1) % ZEROEXPORT_MAX_QUEUE_ENTRIES;
|
|
return true;
|
|
}
|
|
|
|
/** getGroupFromTopic
|
|
* Extahiert die Gruppe aus dem mqttTopic.
|
|
* @param *topic
|
|
* @returns group
|
|
*/
|
|
int8_t getGroupFromTopic(const char *topic) {
|
|
const char *pGroupSection = strstr(topic, "groups/");
|
|
if (pGroupSection == NULL) return -1;
|
|
pGroupSection += 7;
|
|
char strGroup[3];
|
|
uint8_t digitsCopied = 0;
|
|
while (*pGroupSection != '/' && digitsCopied < 2) strGroup[digitsCopied++] = *pGroupSection++;
|
|
strGroup[digitsCopied] = '\0';
|
|
int8_t group = atoi(strGroup);
|
|
mLog["getGroupFromTopic"] = group;
|
|
return group;
|
|
}
|
|
|
|
/** getInverterFromTopic
|
|
* Extrahiert dden Inverter aus dem mqttTopic
|
|
* @param *topic
|
|
* @returns inv
|
|
*/
|
|
int8_t getInverterFromTopic(const char *topic) {
|
|
const char *pInverterSection = strstr(topic, "inverters/");
|
|
if (pInverterSection == NULL) return -1;
|
|
pInverterSection += 10;
|
|
char strInverter[3];
|
|
uint8_t digitsCopied = 0;
|
|
while (*pInverterSection != '/' && digitsCopied < 2) strInverter[digitsCopied++] = *pInverterSection++;
|
|
strInverter[digitsCopied] = '\0';
|
|
int8_t inverter = atoi(strInverter);
|
|
return inverter;
|
|
}
|
|
|
|
/** mqttSubscribe
|
|
* when a MQTT Msg is needed to subscribe, then a publish is leading
|
|
* @param gr
|
|
* @param payload
|
|
* @returns void
|
|
*/
|
|
void mqttSubscribe(String gr, String payload) {
|
|
mqttPublish(gr, payload);
|
|
mMqtt->subscribe(gr.c_str(), QOS_2);
|
|
}
|
|
|
|
/** mqttPublish
|
|
* when a MQTT Msg is needed to Publish, but not to subscribe.
|
|
* @param gr
|
|
* @param payload
|
|
* @param retain
|
|
* @returns void
|
|
*/
|
|
void mqttPublish(String gr, String payload, bool retain = false) {
|
|
mMqtt->publish(gr.c_str(), payload.c_str(), retain);
|
|
}
|
|
|
|
/** sendLog
|
|
* Sendet den LogSpeicher über Webserial und/oder MQTT
|
|
*/
|
|
void sendLog(void) {
|
|
// Log over Webserial
|
|
if (mCfg->log_over_webserial) {
|
|
DPRINTLN(DBG_INFO, String("ze: ") + mDocLog.as<String>());
|
|
}
|
|
|
|
// Log over MQTT
|
|
if (mCfg->log_over_mqtt) {
|
|
if (mMqtt->isConnected()) {
|
|
mMqtt->publish("zero/log", mDocLog.as<std::string>().c_str(), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** clearLog
|
|
* Löscht den LogSpeicher
|
|
*/
|
|
void clearLog(void) {
|
|
mDocLog.clear();
|
|
}
|
|
|
|
// private member variables
|
|
bool mIsInitialized = false;
|
|
|
|
IApp *mApp = nullptr;
|
|
uint32_t *mTimestamp = nullptr;
|
|
zeroExport_t *mCfg = nullptr;
|
|
settings_t *mConfig = nullptr;
|
|
HMSYSTEM *mSys = nullptr;
|
|
RestApiType *mApi = nullptr;
|
|
|
|
zeroExportQueue_t mQueue[ZEROEXPORT_MAX_QUEUE_ENTRIES];
|
|
uint8_t mQueueIdxWrite = 0;
|
|
uint8_t mQueueIdxRead = 0;
|
|
|
|
unsigned long mLastRun = 0;
|
|
|
|
StaticJsonDocument<5000> mDocLog;
|
|
JsonObject mLog = mDocLog.to<JsonObject>();
|
|
|
|
powermeter mPowermeter;
|
|
|
|
PubMqttType *mMqtt = nullptr;
|
|
bool mIsSubscribed = false;
|
|
StaticJsonDocument<512> mqttDoc; // DynamicJsonDocument mqttDoc(512);
|
|
JsonObject mqttObj = mqttDoc.to<JsonObject>();
|
|
};
|
|
|
|
#endif /*__ZEROEXPORT__*/
|
|
|
|
#endif /* #if defined(PLUGIN_ZEROEXPORT) */
|
|
|