diff --git a/src/app.cpp b/src/app.cpp index 1edef85c..4fb5d66e 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -126,7 +126,7 @@ void app::setup() { #if defined(PLUGIN_ZEROEXPORT) // TODO: aufräumen // if (mConfig->plugin.zeroExport.enabled) { - mZeroExport.setup(&mConfig->plugin.zeroExport, &mSys, mConfig); + mZeroExport.setup(&mConfig->plugin.zeroExport, &mSys, mConfig, &mApi); // } #endif // Plugin ZeroExport - Ende diff --git a/src/config/settings.h b/src/config/settings.h index 1162e63d..d9e98a99 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -186,19 +186,19 @@ typedef struct { #endif // Plugin ZeroExport -#define ZEROEXPORT_MAX_GROUPS 6 -#define ZEROEXPORT_GROUP_MAX_LEN_NAME 25 -#define ZEROEXPORT_GROUP_MAX_LEN_PM_URL 100 -#define ZEROEXPORT_GROUP_MAX_LEN_PM_JSONPATH 100 -#define ZEROEXPORT_GROUP_MAX_LEN_PM_USER 25 -#define ZEROEXPORT_GROUP_MAX_LEN_PM_PASS 25 -#define ZEROEXPORT_GROUP_MAX_INVERTERS 3 -#define ZEROEXPORT_POWERMETER_MAX_ERRORS 5 -#define ZEROEXPORT_DEF_INV_WAITINGTIME_MS 10000 +#define ZEROEXPORT_MAX_GROUPS 6 +#define ZEROEXPORT_GROUP_MAX_LEN_NAME 25 +#define ZEROEXPORT_GROUP_MAX_LEN_PM_URL 100 +#define ZEROEXPORT_GROUP_MAX_LEN_PM_JSONPATH 100 +#define ZEROEXPORT_GROUP_MAX_LEN_PM_USER 25 +#define ZEROEXPORT_GROUP_MAX_LEN_PM_PASS 25 +#define ZEROEXPORT_GROUP_MAX_INVERTERS 3 +#define ZEROEXPORT_POWERMETER_MAX_ERRORS 5 +#define ZEROEXPORT_DEF_INV_WAITINGTIME_MS 10000 #if defined(PLUGIN_ZEROEXPORT) enum class zeroExportState : uint8_t { - RESET, GETPOWERMETER, GETINVERTERPOWER, FINISH + RESET, GETPOWERMETER, GETINVERTERDATA, BATTERYPROTECTION, CONTROL, SETCONTROL, WAITACCEPT, FINISH }; typedef enum { @@ -239,12 +239,14 @@ typedef struct { bool enabled; int8_t id; int8_t target; - bool twoPercent; + uint16_t powerMin; uint16_t powerMax; float power; uint16_t limit; + uint16_t limitNew; + unsigned long limitTsp; bool limitAck; float dcVoltage; // uint16_t waitingTime; @@ -267,6 +269,7 @@ typedef struct { float battVoltageOn; float battVoltageOff; // Advanced + uint16_t setPoint; uint8_t refresh; uint8_t powerTolerance; uint16_t powerMax; @@ -278,6 +281,7 @@ typedef struct { float pmPowerL1; float pmPowerL2; float pmPowerL3; + bool battSwitch; // uint16_t power; // Aktueller Verbrauch // uint16_t powerLimitAkt; // Aktuelles Limit // uint16_t powerHyst; // Hysterese @@ -626,7 +630,7 @@ class settings { mCfg.plugin.zeroExport.groups[group].inverters[inv].enabled = false; mCfg.plugin.zeroExport.groups[group].inverters[inv].id = -1; mCfg.plugin.zeroExport.groups[group].inverters[inv].target = -1; - mCfg.plugin.zeroExport.groups[group].inverters[inv].twoPercent = false; + mCfg.plugin.zeroExport.groups[group].inverters[inv].powerMin = 10; mCfg.plugin.zeroExport.groups[group].inverters[inv].powerMax = 600; } // Battery @@ -634,6 +638,7 @@ class settings { mCfg.plugin.zeroExport.groups[group].battVoltageOn = 0; mCfg.plugin.zeroExport.groups[group].battVoltageOff = 0; // Advanced + mCfg.plugin.zeroExport.groups[group].setPoint = 0; mCfg.plugin.zeroExport.groups[group].refresh = 10; mCfg.plugin.zeroExport.groups[group].powerTolerance = 10; mCfg.plugin.zeroExport.groups[group].powerMax = 600; @@ -645,6 +650,7 @@ class settings { mCfg.plugin.zeroExport.groups[group].pmPowerL2 = 0; mCfg.plugin.zeroExport.groups[group].pmPowerL3 = 0; + mCfg.plugin.zeroExport.groups[group].battSwitch = false; } // snprintf(mCfg.plugin.zeroExport.monitor_url, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT); // snprintf(mCfg.plugin.zeroExport.tibber_pw, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT); @@ -916,7 +922,7 @@ class settings { obj[F("enabled")] = mCfg.plugin.zeroExport.groups[group].inverters[inv].enabled; obj[F("id")] = mCfg.plugin.zeroExport.groups[group].inverters[inv].id; obj[F("target")] = mCfg.plugin.zeroExport.groups[group].inverters[inv].target; - obj[F("twoPercent")] = mCfg.plugin.zeroExport.groups[group].inverters[inv].twoPercent; + obj[F("powerMin")] = mCfg.plugin.zeroExport.groups[group].inverters[inv].powerMin; obj[F("powerMax")] = mCfg.plugin.zeroExport.groups[group].inverters[inv].powerMax; } else { if (obj.containsKey(F("enabled"))) @@ -925,8 +931,8 @@ class settings { getVal(obj, F("id"), &mCfg.plugin.zeroExport.groups[group].inverters[inv].id); if (obj.containsKey(F("target"))) getVal(obj, F("target"), &mCfg.plugin.zeroExport.groups[group].inverters[inv].target); - if (obj.containsKey(F("twoPercent"))) - getVal(obj, F("twoPercent"), &mCfg.plugin.zeroExport.groups[group].inverters[inv].twoPercent); + if (obj.containsKey(F("powerMin"))) + getVal(obj, F("powerMin"), &mCfg.plugin.zeroExport.groups[group].inverters[inv].powerMin); if (obj.containsKey(F("powerMax"))) getVal(obj, F("powerMax"), &mCfg.plugin.zeroExport.groups[group].inverters[inv].powerMax); } @@ -953,6 +959,7 @@ class settings { obj[F("battVoltageOn")] = mCfg.plugin.zeroExport.groups[group].battVoltageOn; obj[F("battVoltageOff")] = mCfg.plugin.zeroExport.groups[group].battVoltageOff; // Advanced + obj[F("setPoint")] = mCfg.plugin.zeroExport.groups[group].setPoint; obj[F("refresh")] = mCfg.plugin.zeroExport.groups[group].refresh; obj[F("powerTolerance")] = mCfg.plugin.zeroExport.groups[group].powerTolerance; obj[F("powerMax")] = mCfg.plugin.zeroExport.groups[group].powerMax; @@ -987,6 +994,8 @@ class settings { if (obj.containsKey(F("battVoltageOff"))) getVal(obj, F("battVoltageOff"), &mCfg.plugin.zeroExport.groups[group].battVoltageOff); // Advanced + if (obj.containsKey(F("setPoint"))) + getVal(obj, F("setPoint"), &mCfg.plugin.zeroExport.groups[group].setPoint); if (obj.containsKey(F("refresh"))) getVal(obj, F("refresh"), &mCfg.plugin.zeroExport.groups[group].refresh); if (obj.containsKey(F("powerTolerance"))) diff --git a/src/defines.h b/src/defines.h index 48b2daba..d147702f 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 89 +#define VERSION_PATCH 890001 //------------------------------------- typedef struct { diff --git a/src/plugins/zeroExport/zeroExport.h b/src/plugins/zeroExport/zeroExport.h index 5818923a..4a05d1a5 100644 --- a/src/plugins/zeroExport/zeroExport.h +++ b/src/plugins/zeroExport/zeroExport.h @@ -11,159 +11,113 @@ template +// 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 { -// bool enabled; // true -// uint8_t query_device; // 0 - Tibber, 1 - Shelly, 2 - other (rs232?) -// char monitor_url[ZEXPORT_ADDR_LEN]; // -// char json_path[ZEXPORT_ADDR_LEN]; // -// char tibber_pw[10]; // -// uint8_t Iv; // Id gemäß anlegen -// float power_avg; // -// uint8_t count_avg; // -// double total_power; // -// unsigned long lastTime; // tic toc -// double max_power; // 600 W -// bool two_percent; // P >= 2% von max_power - -// mCfg // -> siehe oben -// mSys // -> -// mConfig // -> - - public: - ZeroExport() { - mIsInitialized = false; - } - - void setup(zeroExport_t *cfg, HMSYSTEM *sys, settings_t *config) { - mCfg = cfg; - mSys = sys; - mConfig = config; - - if (!mCfg->enabled) { - return; + public: + + ZeroExport() { + mIsInitialized = false; } - mIsInitialized = true; - } + void setup(zeroExport_t *cfg, HMSYSTEM *sys, settings_t *config, RestApiType *api) { + mCfg = cfg; + mSys = sys; + mConfig = config; + mApi = api; - void loop(void) { - if ((!mIsInitialized) || (!mCfg->enabled)) { - return; +// TODO: Sicherheitsreturn weil noch Sicherheitsfunktionen fehlen. +// mIsInitialized = true; } - for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) { - if (!mCfg->groups[group].enabled) { - continue; + 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: -//DBGPRINT(F("State::RESET: ")); -//DBGPRINTLN(String(group)); - mCfg->groups[group].lastRun = millis(); - // Weiter zum nächsten State - mCfg->groups[group].state = zeroExportState::GETPOWERMETER; - break; - case zeroExportState::GETPOWERMETER: - if ((millis() - mCfg->groups[group].lastRun) > (mCfg->groups[group].refresh * 1000UL)) { -//DBGPRINT(F("State::GETPOWERMETER:")); -//DBGPRINTLN(String(group)); - if (getPowermeterWatts(group)) { - // Weiter zum nächsten State - mCfg->groups[group].state = zeroExportState::GETINVERTERPOWER; - } else { - // Wartezeit wenn Keine Daten vom Powermeter - mCfg->groups[group].lastRun = (millis() - (mCfg->groups[group].refresh * 100UL)); + 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::GETINVERTERPOWER: - if ((millis() - mCfg->groups[group].lastRun) > (mCfg->groups[group].refresh * 1000UL)) { -//DBGPRINT(F("State::GETINVERTERPOWER:")); -//DBGPRINTLN(String(group)); - if (getInverterPowerWatts(group)) { - // Weiter zum nächsten State + 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 wenn Keine Daten vom Powermeter - mCfg->groups[group].lastRun = (millis() - (mCfg->groups[group].refresh * 100UL)); + //} else { + // Wartezeit } - } - break; -/* - case 4: - // - mCfg->groups[group].state = zeroExportState::RESET; - break; - case 5: - // - mCfg->groups[group].state = zeroExportState::RESET; - break; -*/ - default: - // -//DBGPRINT(F("State::?: ")); -//DBGPRINTLN(String(group)); - mCfg->groups[group].state = zeroExportState::RESET; - break; + break; + default: + DBGPRINT(String("ze: ")); + DBGPRINTLN(mDocLog.as()); + mDocLog.clear(); + mCfg->groups[group].state = zeroExportState::RESET; + break; + } } + } - - - - - - - - - - - - - -/* - if (!mCfg->groups[group].powermeter.error) { -DBGPRINTLN(String("ok verarbeiten.")); + void tickerSecond() { +// TODO: Warten ob benötigt, ggf ein bit setzen, das in der loop() abgearbeitet wird. + if ((!mIsInitialized) || (!mCfg->enabled)) { + return; } + } + - if (mCfg->groups[group].powermeter.error >= ZEROEXPORT_POWERMETER_MAX_ERRORS) { -DBGPRINTLN(String("nok Notmodus.")); - } -*/ -/* - // getPowermeterGroup - mCfg->zeGroup[group].PowermeterSum = getPowermeterSum(); - // getPowermeterPhase - for (uint8_t phase = 1; phase <= 3; phase++) { - mCfg->zeGroup[group].PowermeterPhase[phase] = getPowermeterPhase(); - } - // getInverterPower - for (uint8_t inv = 0; inv < MAX; inv++) { - mCfg->zeGroup[group].InverterpowerGroup = getInverterpowerGroup(); - for (uint8_t phase = 1; phase <= 3; phase++) { - mCfg->zeGroup[group].InverterpowerPhase[phase] = getInverterpowerPhase(); - } - } - // calcPowerSum - mCfg->zeGroup[group].LimitSumNew = mCfg->zeGroup[group].LimitSumOld + mCfg->zeGroup[group].PowermeterSum; - // calcPowerPhase - for (uint8_t phase = 1; phase <= 3; phase++) { - mCfg->zeGroup[group].LimitPhaseNew[phase] = mCfg->zeGroup[group].LimitPhaseOld[phase] - mCfg->zeGroup[group].PowermeterPhase[phase]; - } - // calcPowerInverter - for (uint8_t inv = 0; inv < MAX; inv++) { - } -*/ - } - // setPower - for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) { - if (!mCfg->groups[group].enabled) { - continue; - } /* // TODO: Inverter sortieren nach Leistung // -> Aufsteigend bei Leistungserhöhung @@ -210,52 +164,16 @@ DBGPRINTLN(String("nok Notmodus.")); } */ - } - - } - - void tickerSecond() { - if ((!mIsInitialized) || (!mCfg->enabled)) { - return; - } - - - - - //DPRINTLN(DBG_INFO, (F("tickerSecond()"))); -// if (millis() - mCfg->lastTime < mCfg->count_avg * 1000UL) { -/// zero(); // just refresh when it is needed. To get cpu load low. -// } - } - - // Sums up the power values ​​of all phases and returns them. - // If the value is negative, all power values ​​from the inverter are taken into account - double getPowertoSetnewValue() - { + private: /* - float ivPower = 0; - Inverter<> *iv; - record_t<> *rec; - for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { - iv = mSys->getInverterByPos(i); - rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (iv == NULL) - continue; - ivPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); - } -*/ -// return ((unsigned)(mCfg->total_power - mCfg->power_avg) >= mCfg->power_avg) ? ivPower + mCfg->total_power : ivPower - mCfg->total_power; - return 0; - } +// TODO: Vorlage für nachfolgende Funktion getPowermeterWatts. Funktionen erst zusammenführen, wenn keine weiteren Powermeter mehr kommen. //C2T2-B91B - private: 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); @@ -308,7 +226,6 @@ DBGPRINTLN(String("nok Notmodus.")); return false; } httpClient.end(); -*/ return true; } @@ -316,47 +233,49 @@ DBGPRINTLN(String("nok Notmodus.")); { } +*/ - - + // Powermeter bool getPowermeterWatts(uint8_t group) { + JsonObject logObj = mLog.createNestedObject("getPowermeterWatts"); + logObj["grp"] = group; + bool ret = false; -DBGPRINT(String("getPowermeterWatts: ")); -DBGPRINTLN(String(group)); + switch (mCfg->groups[group].pm_type) { case 1: - ret = getPowermeterWattsShelly(group); + ret = getPowermeterWattsShelly(logObj, group); + break; + case 2: + ret = getPowermeterWattsTasmota(logObj, group); break; -// case 2: -// ret = getPowermeterWattsTasmota(group); -// break; // case 3: -// ret = getPowermeterWattsMqtt(group); +// ret = getPowermeterWattsMqtt(logObj, group); // break; // case 4: -// ret = getPowermeterWattsHichi(group); +// ret = getPowermeterWattsHichi(logObj, group); // break; // case 5: -// ret = getPowermeterWattsTibber(group); +// ret = getPowermeterWattsTibber(logObj, group); // break; -/// default: -/// ret = false; -/// break; } + return ret; } - int getPowermeterWattsShelly(uint8_t group) { + int getPowermeterWattsShelly(JsonObject logObj, uint8_t group) { bool ret = false; + HTTPClient http; -// httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - httpClient.setUserAgent("Ahoy-Agent"); +// http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + http.setUserAgent("Ahoy-Agent"); // TODO: Ahoy-0.8.850024-zero -// httpClient.setConnectTimeout(1000); -// httpClient.setTimeout(1000); -// httpClient.addHeader("Content-Type", "application/json"); -// httpClient.addHeader("Accept", "application/json"); +// 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) { @@ -366,8 +285,7 @@ DBGPRINTLN(String(group)); DeserializationError error = deserializeJson(doc, http.getString()); if (error) { -DBGPRINT(String("deserializeJson() failed: ")); -DBGPRINTLN(String(error.c_str())); + logObj["error"] = "deserializeJson() failed: " + String(error.c_str()); return ret; } @@ -406,8 +324,6 @@ DBGPRINTLN(String(error.c_str())); } else { mCfg->groups[group].pmPowerL1 = 0; } -DBGPRINT(String("pmPowerL1: ")); -DBGPRINTLN(String(mCfg->groups[group].pmPowerL1)); // Shelly 3EM if (doc.containsKey(F("emeters"))) { @@ -431,8 +347,6 @@ DBGPRINTLN(String(mCfg->groups[group].pmPowerL1)); } else { mCfg->groups[group].pmPowerL2 = 0; } -DBGPRINT(String("pmPowerL2: ")); -DBGPRINTLN(String(mCfg->groups[group].pmPowerL2)); // Shelly 3EM if (doc.containsKey(F("emeters"))) { @@ -456,35 +370,53 @@ DBGPRINTLN(String(mCfg->groups[group].pmPowerL2)); } else { mCfg->groups[group].pmPowerL3 = 0; } -DBGPRINT(String("pmPowerL3: ")); -DBGPRINTLN(String(mCfg->groups[group].pmPowerL3)); -DBGPRINT(String("pmPower: ")); -DBGPRINTLN(String(mCfg->groups[group].pmPower)); + 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(void) { -/* + int getPowermeterWattsTasmota(JsonObject logObj, uint8_t group) { +// TODO: nicht komplett + bool ret = false; + HTTPClient http; - char url[100] = "http://"; - strcat(url, mCfg->monitor_url); - strcat(url, "/cm?cmd=status%2010"); - http.begin(url); +// 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) + { - if (http.GET() == 200) { // Parsing - DynamicJsonDocument doc(384); + DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, http.getString()); - if (error) { - Serial.print("deserializeJson() failed: "); - Serial.println(error.c_str()); - return 0; + 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; @@ -495,58 +427,101 @@ DBGPRINTLN(String(mCfg->groups[group].pmPower)); 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 0; - } - int getPowermeterWattsMqtt(void) { - // TODO: - return 0; + return ret; } - int getPowermeterWattsHichi(void) { - // TODO: - return 0; + 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(void) { - // TODO: - return 0; + 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 getInverterPowerWatts(uint8_t group) { bool ret = false; -DBGPRINT(String("getInverterPowerWatts: ")); -DBGPRINTLN(String(group)); + + JsonArray logArrInv = logObj.createNestedArray("iv"); + for (uint8_t inv = 0; inv < ZEROEXPORT_GROUP_MAX_INVERTERS; inv++) { -DBGPRINT(String("iv: ")); -DBGPRINT(String(inv)); -DBGPRINT(String(" ")); + JsonObject logObjInv = logArrInv.createNestedObject(); + logObjInv["iv"] = inv; + // Wenn Inverter deaktiviert -> Eintrag ignorieren if (!mCfg->groups[group].inverters[inv].enabled) { -DBGPRINT(String("(ze disabled).")); 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; } -DBGPRINT(String("(")); -DBGPRINT(String(iv->id)); -DBGPRINT(String("( gefunden)")); + // wenn Inverter deaktiviert -> Daten ignorieren // if (!iv->enabled()) { //DBGPRINT(String(" aber deaktiviert ")); @@ -567,28 +542,77 @@ DBGPRINT(String(" aber nicht erreichbar ")); // 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); -DBGPRINT(String("(P=")); -DBGPRINT(String(mCfg->groups[group].inverters[inv].power)); -DBGPRINT(String("W ")); + 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].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].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); -DBGPRINT(String("U=")); -DBGPRINT(String(mCfg->groups[group].inverters[inv].dcVoltage)); -DBGPRINT(String("V) ")); + 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; } } @@ -596,20 +620,372 @@ 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(); + 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(); + + 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(); }; + + + + + + + +// 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. @@ -631,16 +1007,9 @@ 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"}}} diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 8b0983ca..c1409f04 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -741,7 +741,7 @@ class RestApi { objGroupInv[F("enabled")] = (bool)mConfig->plugin.zeroExport.groups[group].inverters[inv].enabled; objGroupInv[F("id")] = (int8_t)mConfig->plugin.zeroExport.groups[group].inverters[inv].id; objGroupInv[F("target")] = (int8_t)mConfig->plugin.zeroExport.groups[group].inverters[inv].target; - objGroupInv[F("twoPercent")] = (bool)mConfig->plugin.zeroExport.groups[group].inverters[inv].twoPercent; + objGroupInv[F("powerMin")] = (uint16_t)mConfig->plugin.zeroExport.groups[group].inverters[inv].powerMin; objGroupInv[F("powerMax")] = (uint16_t)mConfig->plugin.zeroExport.groups[group].inverters[inv].powerMax; } // Battery @@ -749,6 +749,7 @@ class RestApi { objGroup[F("battVoltageOn")] = ah::round3((float)mConfig->plugin.zeroExport.groups[group].battVoltageOn); objGroup[F("battVoltageOff")] = ah::round3((float)mConfig->plugin.zeroExport.groups[group].battVoltageOff); // Advanced + objGroup[F("setPoint")] = (uint16_t)mConfig->plugin.zeroExport.groups[group].setPoint; objGroup[F("refresh")] = (uint8_t)mConfig->plugin.zeroExport.groups[group].refresh; objGroup[F("powerTolerance")] = (uint8_t)mConfig->plugin.zeroExport.groups[group].powerTolerance; objGroup[F("powerMax")] = (uint16_t)mConfig->plugin.zeroExport.groups[group].powerMax; @@ -1002,7 +1003,7 @@ class RestApi { mConfig->plugin.zeroExport.groups[group].inverters[inv].enabled = jsonIn[F("inverters")][inv][F("enabled")]; mConfig->plugin.zeroExport.groups[group].inverters[inv].id = jsonIn[F("inverters")][inv][F("id")]; mConfig->plugin.zeroExport.groups[group].inverters[inv].target = jsonIn[F("inverters")][inv][F("target")]; - mConfig->plugin.zeroExport.groups[group].inverters[inv].twoPercent = jsonIn[F("inverters")][inv][F("twoPercent")]; + mConfig->plugin.zeroExport.groups[group].inverters[inv].powerMin = jsonIn[F("inverters")][inv][F("powerMin")]; mConfig->plugin.zeroExport.groups[group].inverters[inv].powerMax = jsonIn[F("inverters")][inv][F("powerMax")]; } // Battery @@ -1010,6 +1011,7 @@ class RestApi { mConfig->plugin.zeroExport.groups[group].battVoltageOn = jsonIn[F("battVoltageOn")]; mConfig->plugin.zeroExport.groups[group].battVoltageOff = jsonIn[F("battVoltageOff")]; // Advanced + mConfig->plugin.zeroExport.groups[group].setPoint = jsonIn[F("setPoint")]; mConfig->plugin.zeroExport.groups[group].refresh = jsonIn[F("refresh")]; mConfig->plugin.zeroExport.groups[group].powerTolerance = jsonIn[F("powerTolerance")]; mConfig->plugin.zeroExport.groups[group].powerMax = jsonIn[F("powerMax")]; diff --git a/src/web/html/setup.html b/src/web/html/setup.html index f6af6966..b9bf5355 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -1191,7 +1191,7 @@ ml("th", {style: "width: 10%;"}, "{#ZE_GROUP_TAB_INVERTER_ENABLED}"), ml("th", {}, "{#ZE_GROUP_TAB_INVERTER_NAME}"), ml("th", {}, "{#ZE_GROUP_TAB_INVERTER_SUM}"), - ml("th", {style: "width: 10%;"}, "{#ZE_GROUP_TAB_INVERTER_TWOPERCENT}"), + ml("th", {style: "width: 15%;"}, "{#ZE_GROUP_TAB_INVERTER_POWERMIN}"), ml("th", {style: "width: 15%;"}, "{#ZE_GROUP_TAB_INVERTER_POWERMAX}"), ])); @@ -1215,7 +1215,7 @@ ), ml("td", {}, ml("div", {}, [ - ml("input", {name: "invTwoPercent"+inv, class: "text", id: "invTwoPercent"+inv, type: "checkbox"}, null) + ml("input", {name: "invPowerMin"+inv, class: "text", id: "invPowerMin"+inv, type: "number", min: "0", max: "65535"}, null) ]), ), ml("td", {}, @@ -1281,6 +1281,7 @@ ]), // Advanced ml("div", {id: "div{#ZE_GROUP_TAB_ADVANCED}", class: "tab-content hide"}, [ + divRow("{#ZE_GROUP_TAB_ADVANCED_SETPOINT}", ml("input", {name: "setPoint", class: "text", type: "number", min: "0", max: "65535", value: obj.setPoint}, null)), divRow("{#ZE_GROUP_TAB_ADVANCED_REFRESH}", ml("input", {name: "refresh", class: "text", type: "number", min: "0", max: "255", value: obj.refresh}, null)), divRow("{#ZE_GROUP_TAB_ADVANCED_POWERTOLERANCE}", ml("input", {name: "powerTolerance", class: "text", type: "number", min: "0", max: "255", value: obj.powerTolerance}, null)), divRow("{#ZE_GROUP_TAB_ADVANCED_POWERMAX}", ml("input", {name: "powerMax", class: "text", type: "number", min: "0", max: "65535", value: obj.powerMax}, null)), @@ -1350,10 +1351,10 @@ } } } - // - twoPercent + // - powerMin for (var inv = 0; inv < maxInv; inv++) { - var e = document.getElementById("invTwoPercent"+inv); - e.checked = (obj.inverters[inv].twoPercent); + var e = document.getElementById("invPowerMin"+inv); + e.value = (obj.inverters[inv].powerMin); } // - powerMax for (var inv = 0; inv < maxInv; inv++) { @@ -1387,7 +1388,7 @@ q.id = e.options[e.selectedIndex].value; var e = document.getElementById("invTarget"+inv); q.target = e.options[e.selectedIndex].value; - q.twoPercent = document.getElementById("invTwoPercent"+inv).checked; + q.powerMin = document.getElementById("invPowerMin"+inv).value; q.powerMax = document.getElementById("invPowerMax"+inv).value; o.inverters.push(q); } @@ -1396,6 +1397,7 @@ o.battVoltageOn = document.getElementsByName("battVoltageOn")[0].value; o.battVoltageOff = document.getElementsByName("battVoltageOff")[0].value; // Advanced + o.setPoint = document.getElementsByName("setPoint")[0].value; o.refresh = document.getElementsByName("refresh")[0].value; o.powerTolerance = document.getElementsByName("powerTolerance")[0].value; o.powerMax = document.getElementsByName("powerMax")[0].value; @@ -1446,7 +1448,7 @@ q.id = -1; var e = document.getElementById("invTarget"+inv); q.target = -1; - q.twoPercent = false; + q.powerMin = 0; q.powerMax = 0; o.inverters.push(q); } @@ -1455,6 +1457,7 @@ o.battVoltageOn = 0; o.battVoltageOff = 0; // Advanced + o.setPoint = 0; o.refresh = 10; o.powerTolerance = 10; o.powerMax = 600; diff --git a/src/web/lang.json b/src/web/lang.json index 54547531..f74b3650 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -884,9 +884,9 @@ "de": "Gesamt" }, { - "token": "ZE_GROUP_TAB_INVERTER_TWOPERCENT", - "en": "2%", - "de": "2%" + "token": "ZE_GROUP_TAB_INVERTER_POWERMIN", + "en": "Power (Min)", + "de": "Leistung (Min)" }, { "token": "ZE_GROUP_TAB_INVERTER_POWERMAX", @@ -918,6 +918,11 @@ "en": "Advanced", "de": "Erweitert" }, + { + "token": "ZE_GROUP_TAB_ADVANCED_SETPOINT", + "en": "SetPoint (Watt):", + "de": "Zielpunkt (Watt):" + }, { "token": "ZE_GROUP_TAB_ADVANCED_REFRESH", "en": "Refresh rate (sec):",