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.
 
 
 
 
 
 

1027 lines
43 KiB

#if defined(PLUGIN_ZEROEXPORT)
#ifndef __ZEROEXPORT__
#define __ZEROEXPORT__
#include <HTTPClient.h>
#include <string.h>
#include "AsyncJson.h"
#include "SML.h"
template <class HMSYSTEM>
// TODO: Anbindung an MQTT für Logausgabe zuzüglich DBG-Ausgabe in json. Deshalb alle Debugausgaben ersetzten durch json, dazu sollte ein jsonObject an die Funktion übergeben werden, zu dem die Funktion dann ihren Teil hinzufügt.
// TODO: Powermeter erweitern
// TODO: Der Teil der noch in app.pp steckt komplett hier in die Funktion verschieben.
class ZeroExport {
public:
ZeroExport() {
mIsInitialized = false;
}
void setup(zeroExport_t *cfg, HMSYSTEM *sys, settings_t *config, RestApiType *api) {
mCfg = cfg;
mSys = sys;
mConfig = config;
mApi = api;
// TODO: Sicherheitsreturn weil noch Sicherheitsfunktionen fehlen.
// mIsInitialized = true;
}
void loop(void) {
if ((!mIsInitialized) || (!mCfg->enabled)) {
return;
}
// TODO: Sicherheitsreturn weil noch Sicherheitsfunktionen fehlen.
return;
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
if (!mCfg->groups[group].enabled) {
continue;
}
switch (mCfg->groups[group].state) {
case zeroExportState::RESET:
mCfg->groups[group].lastRun = millis();
mCfg->groups[group].state = zeroExportState::GETPOWERMETER;
break;
case zeroExportState::GETPOWERMETER:
if ((millis() - mCfg->groups[group].lastRun) > (mCfg->groups[group].refresh * 1000UL)) {
if (getPowermeterWatts(group)) {
mCfg->groups[group].state = zeroExportState::GETINVERTERDATA;
} else {
// Wartezeit wenn Keine Daten vom Powermeter
mCfg->groups[group].lastRun = (millis() - (mCfg->groups[group].refresh * 100UL));
}
}
break;
case zeroExportState::GETINVERTERDATA:
if ((millis() - mCfg->groups[group].lastRun) > (mCfg->groups[group].refresh * 1000UL)) {
if (getInverterData(group)) {
mCfg->groups[group].state = zeroExportState::BATTERYPROTECTION;
} else {
// Wartezeit wenn Keine Daten von Wechselrichtern
mCfg->groups[group].lastRun = (millis() - (mCfg->groups[group].refresh * 100UL));
}
}
break;
case zeroExportState::BATTERYPROTECTION:
if (batteryProtection(group)) {
mCfg->groups[group].state = zeroExportState::CONTROL;
//} else {
// Wartezeit
}
break;
case zeroExportState::CONTROL:
if (controller(group)) {
mCfg->groups[group].state = zeroExportState::SETCONTROL;
//} else {
// Wartezeit
}
break;
case zeroExportState::SETCONTROL:
if (setControl(group)) {
mCfg->groups[group].state = zeroExportState::WAITACCEPT;
//} else {
// Wartezeit
}
break;
case zeroExportState::WAITACCEPT:
if (waitAccept(group)) {
mCfg->groups[group].state = zeroExportState::FINISH;
//} else {
// Wartezeit
}
break;
default:
DBGPRINT(String("ze: "));
DBGPRINTLN(mDocLog.as<String>());
mDocLog.clear();
mCfg->groups[group].state = zeroExportState::RESET;
break;
}
}
}
void tickerSecond() {
// TODO: Warten ob benötigt, ggf ein bit setzen, das in der loop() abgearbeitet wird.
if ((!mIsInitialized) || (!mCfg->enabled)) {
return;
}
}
/*
// TODO: Inverter sortieren nach Leistung
// -> Aufsteigend bei Leistungserhöhung
// -> Absteigend bei Leistungsreduzierung
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].waitingTime) {
mCfg->groups[group].inverters[inv].waitingTime--;
continue;
}
// Leistung erhöhen
if (mCfg->groups[group].power < mCfg->groups[group].powerLimitAkt - mCfg->groups[group].powerHyst) {
// mCfg->groups[group].powerLimitAkt = mCfg->groups[group].power
mCfg->groups[group].inverters[inv].waitingTime = ZEROEXPORT_DEF_INV_WAITINGTIME_MS;
return;
}
// Leistung reduzieren
if (mCfg->groups[group].power > mCfg->groups[group].powerLimitAkt + mCfg->groups[group].powerHyst) {
mCfg->groups[group].inverters[inv].waitingTime = ZEROEXPORT_DEF_INV_WAITINGTIME_MS;
return;
}
if ((Power < PowerLimit - Hyst) || (Power > PowerLimit + Hyst)) {
if (Limit < 2%) {
setPower(Off);
setPowerLimit(100%)
} else {
setPower(On);
setPowerLimit(Limit);
mCfg->Inv[inv].waitingTime = ZEROEXPORT_DEF_INV_WAITINGTIME_MS;
}
}
}
*/
private:
/*
// TODO: Vorlage für nachfolgende Funktion getPowermeterWatts. Funktionen erst zusammenführen, wenn keine weiteren Powermeter mehr kommen.
//C2T2-B91B
HTTPClient httpClient;
// TODO: Need to improve here. 2048 for a JSON Obj is to big!?
bool zero()
{
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("Ahoy-Agent");
httpClient.setConnectTimeout(1000);
httpClient.setTimeout(1000);
httpClient.addHeader("Content-Type", "application/json");
httpClient.addHeader("Accept", "application/json");
if (!httpClient.begin(mCfg->monitor_url)) {
DPRINTLN(DBG_INFO, "httpClient.begin failed");
httpClient.end();
return false;
}
if(String(mCfg->monitor_url).endsWith("data.json?node_id=1")){
httpClient.setAuthorization("admin", mCfg->tibber_pw);
}
int httpCode = httpClient.GET();
if (httpCode == HTTP_CODE_OK)
{
String responseBody = httpClient.getString();
DynamicJsonDocument json(2048);
DeserializationError err = deserializeJson(json, responseBody);
// Parse succeeded?
if (err) {
DPRINTLN(DBG_INFO, (F("ZeroExport() JSON error returned: ")));
DPRINTLN(DBG_INFO, String(err.f_str()));
}
// check if it HICHI
if(json.containsKey(F("StatusSNS")) ) {
int index = responseBody.indexOf(String(mCfg->json_path)); // find first match position
responseBody = responseBody.substring(index); // cut it and store it in value
index = responseBody.indexOf(","); // find the first seperation - Bingo!?
mCfg->total_power = responseBody.substring(responseBody.indexOf(":"), index).toDouble();
} else if(json.containsKey(F("emeters"))) {
mCfg->total_power = (double)json[F("total_power")];
} else if(String(mCfg->monitor_url).endsWith("data.json?node_id=1") ) {
tibber_parse();
} else {
DPRINTLN(DBG_INFO, (F("ZeroExport() json error: cant find value in this query: ") + responseBody));
return false;
}
}
else
{
DPRINTLN(DBG_INFO, F("ZeroExport(): Error ") + String(httpCode));
return false;
}
httpClient.end();
return true;
}
void tibber_parse()
{
}
*/
// Powermeter
bool getPowermeterWatts(uint8_t group) {
JsonObject logObj = mLog.createNestedObject("getPowermeterWatts");
logObj["grp"] = group;
bool ret = false;
switch (mCfg->groups[group].pm_type) {
case 1:
ret = getPowermeterWattsShelly(logObj, group);
break;
case 2:
ret = getPowermeterWattsTasmota(logObj, group);
break;
// case 3:
// ret = getPowermeterWattsMqtt(logObj, group);
// break;
// case 4:
// ret = getPowermeterWattsHichi(logObj, group);
// break;
// case 5:
// ret = getPowermeterWattsTibber(logObj, group);
// break;
}
return ret;
}
int getPowermeterWattsShelly(JsonObject logObj, uint8_t group) {
bool ret = false;
HTTPClient http;
// http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setUserAgent("Ahoy-Agent");
// TODO: Ahoy-0.8.850024-zero
// http.setConnectTimeout(1000);
// http.setTimeout(1000);
// http.addHeader("Content-Type", "application/json");
// http.addHeader("Accept", "application/json");
// TODO: Erst aktivieren wenn Timing klar ist, Timeout reduzieren.
http.begin(mCfg->groups[group].pm_url);
if (http.GET() == HTTP_CODE_OK)
{
// Parsing
DynamicJsonDocument doc(2048);
DeserializationError error = deserializeJson(doc, http.getString());
if (error)
{
logObj["error"] = "deserializeJson() failed: " + String(error.c_str());
return ret;
}
// Shelly 3EM
if (doc.containsKey(F("total_power"))) {
mCfg->groups[group].pmPower = doc["total_power"];
ret = true;
// Shelly pro 3EM
} else if (doc.containsKey(F("em:0"))) {
mCfg->groups[group].pmPower = doc["em:0"]["total_act_power"];
ret = true;
// Keine Daten
} else {
mCfg->groups[group].pmPower = 0;
}
// Shelly 3EM
if (doc.containsKey(F("emeters"))) {
mCfg->groups[group].pmPowerL1 = doc["emeters"][0]["power"];
ret = true;
// Shelly pro 3EM
} else if (doc.containsKey(F("em:0"))) {
mCfg->groups[group].pmPowerL1 = doc["em:0"]["a_act_power"];
ret = true;
// Shelly plus1pm plus2pm
} else if (doc.containsKey(F("switch:0"))) {
mCfg->groups[group].pmPowerL1 = doc["switch:0"]["apower"];
mCfg->groups[group].pmPower += mCfg->groups[group].pmPowerL1;
ret = true;
// Shelly Alternative
} else if (doc.containsKey(F("apower"))) {
mCfg->groups[group].pmPowerL1 = doc["apower"];
mCfg->groups[group].pmPower += mCfg->groups[group].pmPowerL1;
ret = true;
// Keine Daten
} else {
mCfg->groups[group].pmPowerL1 = 0;
}
// Shelly 3EM
if (doc.containsKey(F("emeters"))) {
mCfg->groups[group].pmPowerL2 = doc["emeters"][1]["power"];
ret = true;
// Shelly pro 3EM
} else if (doc.containsKey(F("em:0"))) {
mCfg->groups[group].pmPowerL2 = doc["em:0"]["b_act_power"];
ret = true;
// Shelly plus1pm plus2pm
} else if (doc.containsKey(F("switch:1"))) {
mCfg->groups[group].pmPowerL2 = doc["switch.1"]["apower"];
mCfg->groups[group].pmPower += mCfg->groups[group].pmPowerL2;
ret = true;
// // Shelly Alternative
// } else if (doc.containsKey(F("apower"))) {
// mCfg->groups[group].pmPowerL2 = doc["apower"];
// mCfg->groups[group].pmPower += mCfg->groups[group].pmPowerL2;
// ret = true;
// Keine Daten
} else {
mCfg->groups[group].pmPowerL2 = 0;
}
// Shelly 3EM
if (doc.containsKey(F("emeters"))) {
mCfg->groups[group].pmPowerL3 = doc["emeters"][2]["power"];
ret = true;
// Shelly pro 3EM
} else if (doc.containsKey(F("em:0"))) {
mCfg->groups[group].pmPowerL3 = doc["em:0"]["c_act_power"];
ret = true;
// Shelly plus1pm plus2pm
} else if (doc.containsKey(F("switch:2"))) {
mCfg->groups[group].pmPowerL3 = doc["switch:2"]["apower"];
mCfg->groups[group].pmPower += mCfg->groups[group].pmPowerL3;
ret = true;
// // Shelly Alternative
// } else if (doc.containsKey(F("apower"))) {
// mCfg->groups[group].pmPowerL3 = doc["apower"];
// mCfg->groups[group].pmPower += mCfg->groups[group].pmPowerL3;
// ret = true;
// Keine Daten
} else {
mCfg->groups[group].pmPowerL3 = 0;
}
logObj["pmPower"] = mCfg->groups[group].pmPower;
logObj["pmPowerL1"] = mCfg->groups[group].pmPowerL1;
logObj["pmPowerL2"] = mCfg->groups[group].pmPowerL2;
logObj["pmPowerL3"] = mCfg->groups[group].pmPowerL3;
}
http.end();
return ret;
}
int getPowermeterWattsTasmota(JsonObject logObj, uint8_t group) {
// TODO: nicht komplett
bool ret = false;
HTTPClient http;
// http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setUserAgent("Ahoy-Agent");
// TODO: Ahoy-0.8.850024-zero
// http.setConnectTimeout(1000);
// http.setTimeout(1000);
// http.addHeader("Content-Type", "application/json");
// http.addHeader("Accept", "application/json");
// TODO: Erst aktivieren wenn Timing klar ist, Timeout reduzieren.
http.begin(mCfg->groups[group].pm_url);
if (http.GET() == HTTP_CODE_OK)
{
// Parsing
DynamicJsonDocument doc(2048);
DeserializationError error = deserializeJson(doc, http.getString());
if (error)
{
logObj["error"] = "deserializeJson() failed: " + String(error.c_str());
return ret;
}
// TODO: Sum
ret = true;
// TODO: L1
// TODO: L2
// TODO: L3
/*
JsonObject Tasmota_ENERGY = doc["StatusSNS"]["ENERGY"];
int Tasmota_Power = Tasmota_ENERGY["Power"]; // 0
return Tasmota_Power;
*/
/*
String url = "http://" + String(TASMOTA_IP) + "/cm?cmnd=status%2010";
ParsedData = http.get(url).json();
int Watts = ParsedData[TASMOTA_JSON_STATUS][TASMOTA_JSON_PAYLOAD_MQTT_PREFIX][TASMOTA_JSON_POWER_MQTT_LABEL].toInt();
return Watts;
*/
logObj["pmPower"] = mCfg->groups[group].pmPower;
logObj["pmPowerL1"] = mCfg->groups[group].pmPowerL1;
logObj["pmPowerL2"] = mCfg->groups[group].pmPowerL2;
logObj["pmPowerL3"] = mCfg->groups[group].pmPowerL3;
}
http.end();
return ret;
}
int getPowermeterWattsMqtt(JsonObject logObj, uint8_t group) {
// TODO: nicht komplett
bool ret = false;
// logObj["pmPower"] = mCfg->groups[group].pmPower;
// logObj["pmPowerL1"] = mCfg->groups[group].pmPowerL1;
// logObj["pmPowerL2"] = mCfg->groups[group].pmPowerL2;
// logObj["pmPowerL3"] = mCfg->groups[group].pmPowerL3;
// }
// http.end();
return ret;
}
int getPowermeterWattsHichi(JsonObject logObj, uint8_t group) {
// TODO: nicht komplett
bool ret = false;
// logObj["pmPower"] = mCfg->groups[group].pmPower;
// logObj["pmPowerL1"] = mCfg->groups[group].pmPowerL1;
// logObj["pmPowerL2"] = mCfg->groups[group].pmPowerL2;
// logObj["pmPowerL3"] = mCfg->groups[group].pmPowerL3;
// }
// http.end();
return ret;
}
int getPowermeterWattsTibber(JsonObject logObj, uint8_t group) {
// TODO: nicht komplett
bool ret = false;
// logObj["pmPower"] = mCfg->groups[group].pmPower;
// logObj["pmPowerL1"] = mCfg->groups[group].pmPowerL1;
// logObj["pmPowerL2"] = mCfg->groups[group].pmPowerL2;
// logObj["pmPowerL3"] = mCfg->groups[group].pmPowerL3;
// }
// http.end();
return ret;
}
// Inverter
bool getInverterData(uint8_t group) {
JsonObject logObj = mLog.createNestedObject("getInverterPowerWatts");
logObj["grp"] = group;
bool ret = false;
JsonArray logArrInv = logObj.createNestedArray("iv");
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
JsonObject logObjInv = logArrInv.createNestedObject();
logObjInv["iv"] = inv;
// Wenn Inverter deaktiviert -> Eintrag ignorieren
if (!mCfg->groups[group].inverters[inv].enabled) {
continue;
}
// Daten holen
Inverter<> *iv;
record_t<> *rec;
// TODO: getInverterById
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
iv = mSys->getInverterByPos(i);
// Wenn kein Inverter -> ignorieren
if (iv == NULL) {
continue;
}
// Wenn falscher Inverter -> ignorieren
if (iv->id != (uint8_t)mCfg->groups[group].inverters[inv].id) {
continue;
}
// wenn Inverter deaktiviert -> Daten ignorieren
// if (!iv->enabled()) {
//DBGPRINT(String(" aber deaktiviert "));
// continue;
// }
// wenn Inverter nicht verfügbar -> Daten ignorieren
if (!iv->isAvailable()) {
DBGPRINT(String(" aber nicht erreichbar "));
continue;
}
// wenn Inverter nicht produziert -> Daten ignorieren
// if (!iv->isProducing()) {
//DBGPRINT(String(" aber produziert nix "));
// continue;
// }
// Daten abrufen
rec = iv->getRecordStruct(RealTimeRunData_Debug);
// TODO: gibts hier nen Timestamp? Wenn die Daten nicht aktueller sind als beim letzten Durchlauf dann brauch ich nix machen
mCfg->groups[group].inverters[inv].power = iv->getChannelFieldValue(CH0, FLD_PAC, rec);
logObjInv["P_ac"] = mCfg->groups[group].inverters[inv].power;
//mCfg->groups[group].inverters[inv].limit = iv->actPowerLimit;
//DBGPRINT(String("Li="));
//DBGPRINT(String(mCfg->groups[group].inverters[inv].limit));
//DBGPRINT(String("% "));
mCfg->groups[group].inverters[inv].limitAck = iv->powerLimitAck;
//DBGPRINT(String("Ack= "));
//DBGPRINT(String(mCfg->groups[group].inverters[inv].limitAck));
//DBGPRINT(String(" "));
mCfg->groups[group].inverters[inv].dcVoltage = iv->getChannelFieldValue(CH1, FLD_UDC, rec);
logObjInv["U_dc"] = mCfg->groups[group].inverters[inv].dcVoltage;
// TODO: Eingang muss konfigurierbar sein
ret = true;
}
}
return ret;
}
// Battery
bool batteryProtection(uint8_t group) {
JsonObject logObj = mLog.createNestedObject("batteryProtection");
logObj["grp"] = group;
bool ret = false;
DBGPRINT(String("batteryProtection: ("));
DBGPRINT(String(group));
DBGPRINT(String("): "));
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
DBGPRINT(String("iv: "));
DBGPRINT(String(inv));
DBGPRINT(String(" "));
if (mCfg->groups[group].battEnabled) {
// Konfigurationstest
if (mCfg->groups[group].battVoltageOn <= mCfg->groups[group].battVoltageOff) {
mCfg->groups[group].battSwitch = false;
DBGPRINT(String("Konfigurationsfehler: battVoltageOn ist < battVoltageOff"));
return false;
}
// Konfigurationstest
if (mCfg->groups[group].battVoltageOn >= (mCfg->groups[group].battVoltageOff + 1)) {
mCfg->groups[group].battSwitch = false;
DBGPRINT(String("Konfigurationsfehler: battVoltageOn muss >= (battVoltageOff + 1)"));
return false;
}
// Konfigurationstest
if (mCfg->groups[group].battVoltageOn <= 22) {
mCfg->groups[group].battSwitch = false;
DBGPRINT(String("Konfigurationsfehler: battVoltageOn ist <= 22"));
return false;
}
// Einschalten
if (mCfg->groups[group].inverters[inv].dcVoltage > mCfg->groups[group].battVoltageOn) {
mCfg->groups[group].battSwitch = true;
ret = true;
DBGPRINT(String("Einschalten"));
continue;
}
// Ausschalten
if (mCfg->groups[group].inverters[inv].dcVoltage < mCfg->groups[group].battVoltageOff) {
mCfg->groups[group].battSwitch = false;
DBGPRINT(String("Ausschalten"));
return true;
}
} else {
mCfg->groups[group].battSwitch = false;
ret = true;
}
}
DBGPRINTLN(String(""));
return ret;
}
// Controller
bool controller(uint8_t group) {
// TODO: Die Funktion controller() soll die Regelabweichung berechnen und alle nötigen Korrekuren speichern
JsonObject logObj = mLog.createNestedObject("controller");
logObj["grp"] = group;
// TODO: Regelparameter unter Advanced konfigurierbar? Aber erst wenn Regler komplett ingegriert.
const float Kp = 1;
const float Ki = 1;
const float Kd = 1;
unsigned long tsp = millis();
float xSum = 0;
float xL1 = 0;
float xL2 = 0;
float xL3 = 0;
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
// Wenn Inverter disabled -> überspringen
if (!mCfg->groups[group].inverters[inv].enabled) {
continue;
}
// TODO: Wenn der Inverter nicht produziert -> überspringen
// Istwert
if (mCfg->groups[group].inverters[inv].target == 0) {
// Sum
xSum += mCfg->groups[group].inverters[inv].limit;
}
if (mCfg->groups[group].inverters[inv].target == 1) {
// L1
xL1 += mCfg->groups[group].inverters[inv].limit;
}
if (mCfg->groups[group].inverters[inv].target == 2) {
// L2
xL2 += mCfg->groups[group].inverters[inv].limit;
}
if (mCfg->groups[group].inverters[inv].target == 3) {
// L3
xL3 += mCfg->groups[group].inverters[inv].limit;
}
if (mCfg->groups[group].inverters[inv].target == 4) {
// L1 + Sum
xSum += mCfg->groups[group].inverters[inv].limit;
xL1 += mCfg->groups[group].inverters[inv].limit;
}
if (mCfg->groups[group].inverters[inv].target == 5) {
// L2 + Sum
xSum += mCfg->groups[group].inverters[inv].limit;
xL2 += mCfg->groups[group].inverters[inv].limit;
}
if (mCfg->groups[group].inverters[inv].target == 6) {
// L3 + Sum
xSum += mCfg->groups[group].inverters[inv].limit;
xL3 += mCfg->groups[group].inverters[inv].limit;
}
}
logObj["xSum"] = xSum;
logObj["xL1"] = xL1;
logObj["xL2"] = xL2;
logObj["xL3"] = xL3;
// Regelabweichung Sum
float e_Sum = mCfg->groups[group].pmPower - mCfg->groups[group].setPoint;
float e_L1 = mCfg->groups[group].pmPowerL1 - (mCfg->groups[group].setPoint / 3);
float e_L2 = mCfg->groups[group].pmPowerL2 - (mCfg->groups[group].setPoint / 3);
float e_L3 = mCfg->groups[group].pmPowerL3 - (mCfg->groups[group].setPoint / 3);
logObj["e_Sum"] = e_Sum;
logObj["e_L1"] = e_L1;
logObj["e_L2"] = e_L2;
logObj["e_L3"] = e_L3;
// Regler
// P-Anteil
float yP_Sum = Kp * e_Sum;
float yP_L1 = Kp * e_L1;
float yP_L2 = Kp * e_L2;
float yP_L3 = Kp * e_L3;
// I-Anteil
// float esum = esum + e;
// float yI = Ki * Ta * esum;
float yI_Sum = 0;
float yI_L1 = 0;
float yI_L2 = 0;
float yI_L3 = 0;
// D-Anteil
// float yD = Kd * (e -ealt) / Ta;
// float ealt = e;
float yD_Sum = 0;
float yD_L1 = 0;
float yD_L2 = 0;
float yD_L3 = 0;
// PID-Anteil
float yPID_Sum = yP_Sum + yI_Sum + yD_Sum;
float yPID_L1 = yP_L1 + yI_L1 + yD_L1;
float yPID_L2 = yP_L2 + yI_L2 + yD_L2;
float yPID_L3 = yP_L3 + yI_L3 + yD_L3;
// Regelbegrenzung
// if (yPID < 5) yPID = 5;
// if (yPID > 95) yPID = 95;
logObj["yPID_Sum"] = yPID_Sum;
logObj["yPID_L1"] = yPID_L1;
logObj["yPID_L2"] = yPID_L2;
logObj["yPID_L3"] = yPID_L3;
JsonArray logArrInv = logObj.createNestedArray("iv");
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
JsonObject logObjInv = logArrInv.createNestedObject();
logObjInv["iv"] = inv;
if (!mCfg->groups[group].inverters[inv].enabled) {
continue;
}
// Inverterdaten
logObjInv["limit"] = mCfg->groups[group].inverters[inv].limit;
logObjInv["powerMin"] = mCfg->groups[group].inverters[inv].powerMin;
logObjInv["powerMax"] = mCfg->groups[group].inverters[inv].powerMax;
logObjInv["power"] = mCfg->groups[group].inverters[inv].power;
// limitNew berechnen
if (mCfg->groups[group].inverters[inv].target == 0) {
// Sum
mCfg->groups[group].inverters[inv].limitNew = (uint16_t)(mCfg->groups[group].inverters[inv].limit + (int16_t)yPID_Sum);
}
if (mCfg->groups[group].inverters[inv].target == 1) {
// L1
mCfg->groups[group].inverters[inv].limitNew = (uint16_t)(mCfg->groups[group].inverters[inv].limit + (int16_t)yPID_L1);
}
if (mCfg->groups[group].inverters[inv].target == 2) {
// L2
mCfg->groups[group].inverters[inv].limitNew = (uint16_t)(mCfg->groups[group].inverters[inv].limit + (int16_t)yPID_L2);
}
if (mCfg->groups[group].inverters[inv].target == 3) {
// L3
mCfg->groups[group].inverters[inv].limitNew = (uint16_t)(mCfg->groups[group].inverters[inv].limit + (int16_t)yPID_L3);
}
if (mCfg->groups[group].inverters[inv].target == 4) {
// L1 + Sum
}
if (mCfg->groups[group].inverters[inv].target == 5) {
// L2 + Sum
}
if (mCfg->groups[group].inverters[inv].target == 6) {
// L3 + Sum
}
}
return true;
}
bool setControl(uint8_t group) {
// TODO: Die Funktion setControl soll alle gespeicherten Aufgaben in der korrekten Reihenfolge abarbeiten.
JsonObject logObj = mLog.createNestedObject("setControl");
logObj["grp"] = group;
bool ret = true;
JsonArray logArrInv = logObj.createNestedArray("iv");
unsigned long tsp = millis();
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
JsonObject logObjInv = logArrInv.createNestedObject();
logObjInv["iv"] = inv;
if (!mCfg->groups[group].inverters[inv].enabled) {
continue;
}
if (mCfg->groups[group].inverters[inv].limitNew != mCfg->groups[group].inverters[inv].limit) {
// Wenn keine Freigabe für neues Limit vorhanden ist -> überspringen
if (mCfg->groups[group].inverters[inv].limitTsp != 0) {
continue;
}
// Begrenzen
if (mCfg->groups[group].inverters[inv].limitNew <= mCfg->groups[group].inverters[inv].powerMin) {
mCfg->groups[group].inverters[inv].limitNew = mCfg->groups[group].inverters[inv].powerMin;
}
if (mCfg->groups[group].inverters[inv].limitNew >= mCfg->groups[group].inverters[inv].powerMax) {
mCfg->groups[group].inverters[inv].limitNew = mCfg->groups[group].inverters[inv].powerMax;
}
logObjInv["limitOld"] = mCfg->groups[group].inverters[inv].limit;
logObjInv["limitNew"] = mCfg->groups[group].inverters[inv].limitNew;
setLimit(group, inv);
ret = false;
}
}
if (ret) {
logObj["todo"] = "- nothing todo - ";
}
return ret;
}
bool waitAccept(uint8_t group) {
JsonObject logObj = mLog.createNestedObject("waitAccept");
logObj["grp"] = group;
bool ret = true;
JsonArray logArrInv = logObj.createNestedArray("iv");
unsigned long tsp = millis();
for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) {
JsonObject logObjInv = logArrInv.createNestedObject();
logObjInv["iv"] = inv;
if (!mCfg->groups[group].inverters[inv].enabled) {
continue;
}
if (mCfg->groups[group].inverters[inv].limitAck) {
mCfg->groups[group].inverters[inv].limitAck = 0;
mCfg->groups[group].inverters[inv].limitTsp = 0;
ret = false;
continue;
}
if ((mCfg->groups[group].inverters[inv].limitTsp + 5000UL) < tsp) {
mCfg->groups[group].inverters[inv].limitTsp = 0;
ret = false;
continue;
}
}
return true;
}
// TODO: Hier folgen Unterfunktionen für SetControl die Erweitert werden müssen
// setLimit, checkLimit
// setPower, checkPower
// setReboot, checkReboot
bool setLimit(uint8_t group, uint8_t inv) {
DBGPRINT(String("setLimit: ("));
DBGPRINT(String(group));
DBGPRINT(String("): "));
DBGPRINT(String("iv: ("));
DBGPRINT(String(inv));
DBGPRINT(String(") -> "));
// Limit verweigern wenn Abweichung < 5 W
// if ((mCfg->groups[group].inverters[inv].limitNew > mCfg->groups[group].inverters[inv].limit - 5) && (mCfg->groups[group].inverters[inv].limitNew < mCfg->groups[group].inverters[inv].limit + 5)) {
// TODO: 5W Toleranz konfigurierbar?
//DBGPRINT(String(mCfg->groups[group].inverters[inv].limit));
//DBGPRINTLN(String(" W"));
// return true;
// }
// Limit speichern
//DBGPRINT(String(mCfg->groups[group].inverters[inv].limit));
//DBGPRINTLN(String(" W -> "));
//DBGPRINT(String(mCfg->groups[group].inverters[inv].limitNew));
//DBGPRINTLN(String(" W"));
mCfg->groups[group].inverters[inv].limit = mCfg->groups[group].inverters[inv].limitNew;
mCfg->groups[group].inverters[inv].limitTsp = millis();
// Limit übergeben
DynamicJsonDocument doc(512);
JsonObject obj = doc.to<JsonObject>();
obj["val"] = mCfg->groups[group].inverters[inv].limit;
obj["id"] = mCfg->groups[group].inverters[inv].id;
obj["path"] = "ctrl";
obj["cmd"] = "limit_nonpersistent_absolute";
String data;
serializeJsonPretty(obj, data);
DBGPRINTLN(data);
mApi->ctrlRequest(obj);
return true;
}
bool checkLimitAllowed(uint8_t group, uint8_t inv) {
//DBGPRINT(String("checkLimit: ("));
//DBGPRINT(String(group));
//DBGPRINT(String("): "));
//DBGPRINT(String("iv: ("));
//DBGPRINT(String(inv));
//DBGPRINT(String(") -> "));
//DBGPRINT(String(mCfg->groups[group].inverters[inv].limitAck));
if (mCfg->groups[group].inverters[inv].limitAck == 0) {
return true;
} else {
mCfg->groups[group].inverters[inv].limitAck = 0;
return false;
}
}
/*
// TODO: Vorlage für Berechnung
Inverter<> *iv = mSys.getInverterByPos(mConfig->plugin.zeroExport.Iv);
DynamicJsonDocument doc(512);
JsonObject object = doc.to<JsonObject>();
double nValue = round(mZeroExport.getPowertoSetnewValue());
double twoPerVal = nValue <= (iv->getMaxPower() / 100 * 2 );
if(mConfig->plugin.zeroExport.two_percent && (nValue <= twoPerVal))
nValue = twoPerVal;
if(mConfig->plugin.zeroExport.max_power <= nValue)
nValue = mConfig->plugin.zeroExport.max_power;
if(iv->actPowerLimit == nValue) {
mConfig->plugin.zeroExport.lastTime = millis(); // set last timestamp
return; // if PowerLimit same as befor, then skip
}
object["val"] = nValue;
object["id"] = mConfig->plugin.zeroExport.Iv;
object["path"] = "ctrl";
object["cmd"] = "limit_nonpersistent_absolute";
String data;
serializeJsonPretty(object, data);
DPRINTLN(DBG_INFO, data);
mApi.ctrlRequest(object);
mConfig->plugin.zeroExport.lastTime = millis(); // set last timestamp
*/
// private member variables
bool mIsInitialized = false;
zeroExport_t *mCfg;
settings_t *mConfig;
HMSYSTEM *mSys;
RestApiType *mApi;
StaticJsonDocument<5000> mDocLog;
JsonObject mLog = mDocLog.to<JsonObject>();
};
// TODO: Vorlagen für Powermeter-Analyse
/*
Shelly 1pm
Der Shelly 1pm verfügt über keine eigene Spannungsmessung sondern geht von 220V * Korrekturfaktor aus. Dadurch wird die Leistungsmessung verfälscht und der Shelly ist ungeeignet.
http://192.168.xxx.xxx/status
Shelly 3em (oder em3) :
ok
{"wifi_sta":{"connected":true,"ssid":"Odyssee2001","ip":"192.168.100.85","rssi":-23},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":true},"time":"17:13","unixtime":1709223219,"serial":27384,"has_update":false,"mac":"3494547B94EE","cfg_changed_cnt":1,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"is_valid":true,"source":"input"}],"emeters":[{"power":51.08,"pf":0.27,"current":0.78,"voltage":234.90,"is_valid":true,"total":1686297.2,"total_returned":428958.4},{"power":155.02,"pf":0.98,"current":0.66,"voltage":235.57,"is_valid":true,"total":878905.6,"total_returned":4.1},{"power":6.75,"pf":0.26,"current":0.11,"voltage":234.70,"is_valid":true,"total":206151.1,"total_returned":0.0}],"total_power":212.85,"emeter_n":{"current":0.00,"ixsum":1.29,"mismatch":false,"is_valid":false},"fs_mounted":true,"v_data":1,"ct_calst":0,"update":{"status":"idle","has_update":false,"new_version":"20230913-114244/v1.14.0-gcb84623","old_version":"20230913-114244/v1.14.0-gcb84623","beta_version":"20231107-165007/v1.14.1-rc1-g0617c15"},"ram_total":49920,"ram_free":30192,"fs_size":233681,"fs_free":154616,"uptime":13728721}
Shelly plus 2pm :
ok
{"ble":{},"cloud":{"connected":false},"input:0":{"id":0,"state":false},"input:1":{"id":1,"state":false},"mqtt":{"connected":true},"switch:0":{"id":0, "source":"MQTT", "output":false, "apower":0.0, "voltage":237.0, "freq":50.0, "current":0.000, "pf":0.00, "aenergy":{"total":62758.285,"by_minute":[0.000,0.000,0.000],"minute_ts":1709223337},"temperature":{"tC":35.5, "tF":96.0}},"switch:1":{"id":1, "source":"MQTT", "output":false, "apower":0.0, "voltage":237.1, "freq":50.0, "current":0.000, "pf":0.00, "aenergy":{"total":61917.211,"by_minute":[0.000,0.000,0.000],"minute_ts":1709223337},"temperature":{"tC":35.5, "tF":96.0}},"sys":{"mac":"B0B21C10A478","restart_required":false,"time":"17:15","unixtime":1709223338,"uptime":8746115,"ram_size":245016,"ram_free":141004,"fs_size":458752,"fs_free":135168,"cfg_rev":7,"kvs_rev":0,"schedule_rev":0,"webhook_rev":0,"available_updates":{"stable":{"version":"1.2.2"}}},"wifi":{"sta_ip":"192.168.100.87","status":"got ip","ssid":"Odyssee2001","rssi":-62},"ws":{"connected":false}}
http://192.168.xxx.xxx/rpc/Shelly.GetStatus
Shelly plus 1pm :
nok keine negativen Leistungswerte
{"ble":{},"cloud":{"connected":false},"input:0":{"id":0,"state":false},"mqtt":{"connected":true},"switch:0":{"id":0, "source":"MQTT", "output":false, "apower":0.0, "voltage":235.9, "current":0.000, "aenergy":{"total":20393.619,"by_minute":[0.000,0.000,0.000],"minute_ts":1709223441},"temperature":{"tC":34.6, "tF":94.3}},"sys":{"mac":"FCB467A66E3C","restart_required":false,"time":"17:17","unixtime":1709223443,"uptime":8644483,"ram_size":246256,"ram_free":143544,"fs_size":458752,"fs_free":147456,"cfg_rev":9,"kvs_rev":0,"schedule_rev":0,"webhook_rev":0,"available_updates":{"stable":{"version":"1.2.2"}}},"wifi":{"sta_ip":"192.168.100.88","status":"got ip","ssid":"Odyssee2001","rssi":-42},"ws":{"connected":false}}
Tasmota
http://192.168.100.81/cm?cmnd=Status0
{"Status":{"Module":1,"DeviceName":"Tasmota","FriendlyName":["Tasmota"],"Topic":"Tasmota","ButtonTopic":"0","Power":0,"PowerOnState":3,"LedState":1,"LedMask":"FFFF","SaveData":1,"SaveState":1,"SwitchTopic":"0","SwitchMode":[0,0,0,0,0,0,0,0],"ButtonRetain":0,"SwitchRetain":0,"SensorRetain":0,"PowerRetain":0,"InfoRetain":0,"StateRetain":0},"StatusPRM":{"Baudrate":9600,"SerialConfig":"8N1","GroupTopic":"tasmotas","OtaUrl":"http://ota.tasmota.com/tasmota/release/tasmota.bin.gz","RestartReason":"Software/System restart","Uptime":"202T01:24:51","StartupUTC":"2023-08-13T15:21:13","Sleep":50,"CfgHolder":4617,"BootCount":27,"BCResetTime":"2023-02-04T16:45:38","SaveCount":150,"SaveAddress":"F5000"},"StatusFWR":{"Version":"11.1.0(tasmota)","BuildDateTime":"2022-05-05T03:23:22","Boot":31,"Core":"2_7_4_9","SDK":"2.2.2-dev(38a443e)","CpuFrequency":80,"Hardware":"ESP8266EX","CR":"378/699"},"StatusLOG":{"SerialLog":0,"WebLog":2,"MqttLog":0,"SysLog":0,"LogHost":"","LogPort":514,"SSId":["Odyssee2001",""],"TelePeriod":300,"Resolution":"558180C0","SetOption":["00008009","2805C80001000600003C5A0A190000000000","00000080","00006000","00004000"]},"StatusMEM":{"ProgramSize":658,"Free":344,"Heap":17,"ProgramFlashSize":1024,"FlashSize":1024,"FlashChipId":"14325E","FlashFrequency":40,"FlashMode":3,"Features":["00000809","87DAC787","043E8001","000000CF","010013C0","C000F989","00004004","00001000","04000020"],"Drivers":"1,2,3,4,5,6,7,8,9,10,12,16,18,19,20,21,22,24,26,27,29,30,35,37,45,56,62","Sensors":"1,2,3,4,5,6,53"},"StatusNET":{"Hostname":"Tasmota","IPAddress":"192.168.100.81","Gateway":"192.168.100.1","Subnetmask":"255.255.255.0","DNSServer1":"192.168.100.1","DNSServer2":"0.0.0.0","Mac":"4C:11:AE:11:F8:50","Webserver":2,"HTTP_API":1,"WifiConfig":4,"WifiPower":17.0},"StatusMQT":{"MqttHost":"192.168.100.80","MqttPort":1883,"MqttClientMask":"Tasmota","MqttClient":"Tasmota","MqttUser":"mqttuser","MqttCount":156,"MAX_PACKET_SIZE":1200,"KEEPALIVE":30,"SOCKET_TIMEOUT":4},"StatusTIM":{"UTC":"2024-03-02T16:46:04","Local":"2024-03-02T17:46:04","StartDST":"2024-03-31T02:00:00","EndDST":"2024-10-27T03:00:00","Timezone":"+01:00","Sunrise":"07:29","Sunset":"18:35"},"StatusSNS":{"Time":"2024-03-02T17:46:04","PV":{"Bezug":0.364,"Einspeisung":3559.439,"Leistung":-14}},"StatusSTS":{"Time":"2024-03-02T17:46:04","Uptime":"202T01:24:51","UptimeSec":17457891,"Heap":16,"SleepMode":"Dynamic","Sleep":50,"LoadAvg":19,"MqttCount":156,"POWER":"OFF","Wifi":{"AP":1,"SSId":"Odyssee2001","BSSId":"34:31:C4:22:92:74","Channel":6,"Mode":"11n","RSSI":100,"Signal":-50,"LinkCount":15,"Downtime":"0T00:08:22"}}}
*/
#endif /*__ZEROEXPORT__*/
#endif /* #if defined(ESP32) */