mirror of
https://github.com/lumapu/ahoy.git
synced 2025-06-10 06:31:39 +02:00
886 lines
33 KiB
C++
886 lines
33 KiB
C++
//-----------------------------------------------------------------------------
|
|
// 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"] = CfgGroup->battEnabled;
|
|
if (CfgGroup->battEnabled) {
|
|
if (CfgGroup->battSwitch != true) {
|
|
if (CfgGroupInv->dcVoltage > CfgGroup->battVoltageOn) {
|
|
CfgGroup->battSwitch = true;
|
|
mLog["bA"] = "turn on";
|
|
}
|
|
if ((CfgGroupInv->dcVoltage > CfgGroup->battVoltageOff) && (CfgGroupInv->power > 0)) {
|
|
CfgGroup->battSwitch = true;
|
|
mLog["bA"] = "turn on";
|
|
}
|
|
} else {
|
|
if (CfgGroupInv->dcVoltage < CfgGroup->battVoltageOff) {
|
|
CfgGroup->battSwitch = false;
|
|
mLog["bA"] = "turn off";
|
|
}
|
|
}
|
|
mLog["bU"] = ah::round1(CfgGroupInv->dcVoltage);
|
|
} else {
|
|
if (CfgGroup->battSwitch != true) {
|
|
CfgGroup->battSwitch = true;
|
|
mLog["bA"] = "turn on";
|
|
}
|
|
}
|
|
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;
|
|
int16_t xSum = 0.0;
|
|
switch (CfgGroupInv->target) {
|
|
case zeroExportInverterTarget_t::Sum:
|
|
x = mPowermeter.getDataAVG(group).P;
|
|
break;
|
|
case zeroExportInverterTarget_t::L1:
|
|
x = mPowermeter.getDataAVG(group).P1;
|
|
break;
|
|
case zeroExportInverterTarget_t::L2:
|
|
x = mPowermeter.getDataAVG(group).P2;
|
|
break;
|
|
case zeroExportInverterTarget_t::L3:
|
|
x = mPowermeter.getDataAVG(group).P3;
|
|
break;
|
|
case zeroExportInverterTarget_t::L1Sum:
|
|
x = mPowermeter.getDataAVG(group).P1;
|
|
xSum = mPowermeter.getDataAVG(group).P;
|
|
xSum -= mPowermeter.getDataAVG(group).P2;
|
|
xSum -= mPowermeter.getDataAVG(group).P3;
|
|
if (xSum > x) x = xSum;
|
|
break;
|
|
case zeroExportInverterTarget_t::L2Sum:
|
|
x = mPowermeter.getDataAVG(group).P2;
|
|
xSum = mPowermeter.getDataAVG(group).P;
|
|
xSum -= mPowermeter.getDataAVG(group).P1;
|
|
xSum -= mPowermeter.getDataAVG(group).P3;
|
|
if (xSum > x) x = xSum;
|
|
break;
|
|
case zeroExportInverterTarget_t::L3Sum:
|
|
x = mPowermeter.getDataAVG(group).P3;
|
|
xSum = mPowermeter.getDataAVG(group).P;
|
|
xSum -= mPowermeter.getDataAVG(group).P1;
|
|
xSum -= mPowermeter.getDataAVG(group).P2;
|
|
if (xSum > x) x = xSum;
|
|
break;
|
|
default:
|
|
x = w;
|
|
// TODO: ErrorLog
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
|
|
// 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) 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;
|
|
|
|
// TODO: activate
|
|
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);
|
|
|
|
// 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();
|
|
|
|
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
|
|
if (!mCfg->groups [group].enabled) continue;
|
|
|
|
if(!strcmp(mCfg->groups[group].battSoC, "")) continue;
|
|
|
|
mMqtt->subscribeExtern(String(mCfg->groups[group].battSoC).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"]);
|
|
|
|
/// TODO: Receive Message für SoC
|
|
// if ((topicGroup >= 0) && (topicGroup < ZEROEXPORT_MAX_GROUPS)) {
|
|
// if (topic.indexOf("xxx") != -1) {
|
|
|
|
// }
|
|
// }
|
|
|
|
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());
|
|
if (topicGroup != -1)
|
|
mLog["g"] = topicGroup;
|
|
int8_t topicInverter = getInverterFromTopic(topic.c_str());
|
|
if (topicInverter == -1)
|
|
mLog["i"] = topicInverter;
|
|
|
|
// "topic":"ctrl/zero/enabled"
|
|
if (topic.indexOf("ctrl/zero/enabled") != -1) {
|
|
mCfg->enabled = (bool)obj["val"];
|
|
mLog["k"] = "ctrl/zero/enabled";
|
|
mLog["v"] = mCfg->enabled;
|
|
}
|
|
|
|
// "topic":"ctrl/zero/sleep"
|
|
if (topic.indexOf("ctrl/zero/sleep") != -1) {
|
|
mCfg->sleep = (bool)obj["val"];
|
|
mLog["k"] = "ctrl/zero/sleep";
|
|
mLog["v"] = mCfg->sleep;
|
|
}
|
|
|
|
if ((topicGroup >= 0) && (topicGroup < ZEROEXPORT_MAX_GROUPS)) {
|
|
// "topic":"ctrl/zero/groups/+/enabled"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/enabled") != -1) {
|
|
mCfg->groups[topicGroup].enabled = (bool)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/enabled";
|
|
mLog["v"] = mCfg->groups[topicGroup].enabled;
|
|
}
|
|
|
|
// "topic":"ctrl/zero/groups/+/sleep"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/sleep") != -1) {
|
|
mCfg->groups[topicGroup].sleep = (bool)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/sleep";
|
|
mLog["v"] = mCfg->groups[topicGroup].sleep;
|
|
}
|
|
|
|
// 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"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/battery/switch") != -1) {
|
|
mCfg->groups[topicGroup].battSwitch = (bool)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/battery/switch";
|
|
mLog["v"] = mCfg->groups[topicGroup].battSwitch;
|
|
}
|
|
|
|
// "topic":"ctrl/zero/groups/+/advanced/setPoint"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/advanced/setPoint") != -1) {
|
|
mCfg->groups[topicGroup].setPoint = (int16_t)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/advanced/setPoint";
|
|
mLog["v"] = mCfg->groups[topicGroup].setPoint;
|
|
}
|
|
|
|
// "topic":"ctrl/zero/groups/+/advanced/powerTolerance"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/advanced/powerTolerance") != -1) {
|
|
mCfg->groups[topicGroup].powerTolerance = (uint8_t)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/advanced/powerTolerance";
|
|
mLog["v"] = mCfg->groups[topicGroup].powerTolerance;
|
|
}
|
|
|
|
// "topic":"ctrl/zero/groups/+/advanced/powerMax"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/advanced/powerMax") != -1) {
|
|
mCfg->groups[topicGroup].powerMax = (uint16_t)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/advanced/powerMax";
|
|
mLog["v"] = mCfg->groups[topicGroup].powerMax;
|
|
}
|
|
|
|
if ((topicInverter >= 0) && (topicInverter < ZEROEXPORT_GROUP_MAX_INVERTERS)) {
|
|
// "topic":"ctrl/zero/groups/+/inverter/+/enabled"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/inverter/" + String(topicInverter) + "/enabled") != -1) {
|
|
mCfg->groups[topicGroup].inverters[topicInverter].enabled = (bool)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/inverter/" + String(topicInverter) + "/enabled";
|
|
mLog["v"] = mCfg->groups[topicGroup].inverters[topicInverter].enabled;
|
|
}
|
|
|
|
// "topic":"ctrl/zero/groups/+/inverter/+/powerMin"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/inverter/" + String(topicInverter) + "/powerMin") != -1) {
|
|
mCfg->groups[topicGroup].inverters[topicInverter].powerMin = (uint16_t)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/inverter/" + String(topicInverter) + "/powerMin";
|
|
mLog["v"] = mCfg->groups[topicGroup].inverters[topicInverter].powerMin;
|
|
}
|
|
|
|
// "topic":"ctrl/zero/groups/+/inverter/+/powerMax"
|
|
if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/inverter/" + String(topicInverter) + "/powerMax") != -1) {
|
|
mCfg->groups[topicGroup].inverters[topicInverter].powerMax = (uint16_t)obj["val"];
|
|
mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/inverter/" + String(topicInverter) + "/powerMax";
|
|
mLog["v"] = mCfg->groups[topicGroup].inverters[topicInverter].powerMax;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
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) */
|