diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 9adc848e1..05f8ab8f9 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Configuration.h" #include #include #include @@ -12,16 +13,19 @@ class WebApiWsLiveClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - void generateJsonResponse(JsonVariant& root); - void addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); - void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); + static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv); + static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv); + static void generateCommonJsonResponse(JsonVariant& root); + + static void addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); + static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); + void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); AsyncWebSocket _ws; - uint32_t _lastWsPublish = 0; - uint32_t _newestInverterTimestamp = 0; + uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; std::mutex _mutex; diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 8ac419013..319506a3e 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -3,7 +3,6 @@ * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_ws_live.h" -#include "Configuration.h" #include "Datastore.h" #include "MessageOutput.h" #include "Utils.h" @@ -58,108 +57,50 @@ void WebApiWsLiveClass::sendDataTaskCb() return; } - uint32_t maxTimeStamp = 0; + // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); - maxTimeStamp = std::max(maxTimeStamp, inv->Statistics()->getLastUpdate()); - } + if (inv == nullptr) { + continue; + } - // Update on every inverter change or at least after 10 seconds - if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { + const uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); + if (!((lastUpdateInternal > 0 && lastUpdateInternal > _lastPublishStats[i]) || (millis() - _lastPublishStats[i] > (10 * 1000)))) { + continue; + } + + _lastPublishStats[i] = millis(); try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(4096 * INV_MAX_COUNT); - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - JsonVariant var = root; - generateJsonResponse(var); + DynamicJsonDocument root(4096); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + continue; + } + JsonVariant var = root; - String buffer; - serializeJson(root, buffer); + auto invArray = var.createNestedArray("inverters"); + auto invObject = invArray.createNestedObject(); - _ws.textAll(buffer); - _newestInverterTimestamp = maxTimeStamp; - } + generateCommonJsonResponse(var); + generateInverterCommonJsonResponse(invObject, inv); + generateInverterChannelJsonResponse(invObject, inv); + + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer); } catch (const std::bad_alloc& bad_alloc) { MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); } catch (const std::exception& exc) { MessageOutput.printf("Unknown exception in /api/livedata/status. Reason: \"%s\".\r\n", exc.what()); } - - _lastWsPublish = millis(); } } -void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) +void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) { - JsonArray invArray = root.createNestedArray("inverters"); - - // Loop all inverters - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); - if (inv == nullptr) { - continue; - } - - JsonObject invObject = invArray.createNestedObject(); - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg == nullptr) { - continue; - } - - invObject["serial"] = inv->serialString(); - invObject["name"] = inv->name(); - invObject["order"] = inv_cfg->Order; - invObject["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; - invObject["poll_enabled"] = inv->getEnablePolling(); - invObject["reachable"] = inv->isReachable(); - invObject["producing"] = inv->isProducing(); - invObject["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); - if (inv->DevInfo()->getMaxPower() > 0) { - invObject["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; - } else { - invObject["limit_absolute"] = -1; - } - - // Loop all channels - for (auto& t : inv->Statistics()->getChannelTypes()) { - JsonObject chanTypeObj = invObject.createNestedObject(inv->Statistics()->getChannelTypeName(t)); - for (auto& c : inv->Statistics()->getChannelsByType(t)) { - if (t == TYPE_DC) { - chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; - } - addField(chanTypeObj, inv, t, c, FLD_PAC); - addField(chanTypeObj, inv, t, c, FLD_UAC); - addField(chanTypeObj, inv, t, c, FLD_IAC); - if (t == TYPE_AC) { - addField(chanTypeObj, inv, t, c, FLD_PDC, "Power DC"); - } else { - addField(chanTypeObj, inv, t, c, FLD_PDC); - } - addField(chanTypeObj, inv, t, c, FLD_UDC); - addField(chanTypeObj, inv, t, c, FLD_IDC); - addField(chanTypeObj, inv, t, c, FLD_YD); - addField(chanTypeObj, inv, t, c, FLD_YT); - addField(chanTypeObj, inv, t, c, FLD_F); - addField(chanTypeObj, inv, t, c, FLD_T); - addField(chanTypeObj, inv, t, c, FLD_PF); - addField(chanTypeObj, inv, t, c, FLD_Q); - addField(chanTypeObj, inv, t, c, FLD_EFF); - if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { - addField(chanTypeObj, inv, t, c, FLD_IRR); - chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c); - } - } - } - - if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { - invObject["events"] = inv->EventLog()->getEntryCount(); - } else { - invObject["events"] = -1; - } - } - JsonObject totalObj = root.createNestedObject("total"); addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits()); addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits()); @@ -169,10 +110,73 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) struct tm timeinfo; hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); - if (!strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD)) { - hintObj["default_password"] = true; + hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0; +} + +void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } + + root["serial"] = inv->serialString(); + root["name"] = inv->name(); + root["order"] = inv_cfg->Order; + root["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; + root["poll_enabled"] = inv->getEnablePolling(); + root["reachable"] = inv->isReachable(); + root["producing"] = inv->isProducing(); + root["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); + if (inv->DevInfo()->getMaxPower() > 0) { + root["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; } else { - hintObj["default_password"] = false; + root["limit_absolute"] = -1; + } +} + +void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } + + // Loop all channels + for (auto& t : inv->Statistics()->getChannelTypes()) { + JsonObject chanTypeObj = root.createNestedObject(inv->Statistics()->getChannelTypeName(t)); + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + if (t == TYPE_DC) { + chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; + } + addField(chanTypeObj, inv, t, c, FLD_PAC); + addField(chanTypeObj, inv, t, c, FLD_UAC); + addField(chanTypeObj, inv, t, c, FLD_IAC); + if (t == TYPE_AC) { + addField(chanTypeObj, inv, t, c, FLD_PDC, "Power DC"); + } else { + addField(chanTypeObj, inv, t, c, FLD_PDC); + } + addField(chanTypeObj, inv, t, c, FLD_UDC); + addField(chanTypeObj, inv, t, c, FLD_IDC); + addField(chanTypeObj, inv, t, c, FLD_YD); + addField(chanTypeObj, inv, t, c, FLD_YT); + addField(chanTypeObj, inv, t, c, FLD_F); + addField(chanTypeObj, inv, t, c, FLD_T); + addField(chanTypeObj, inv, t, c, FLD_PF); + addField(chanTypeObj, inv, t, c, FLD_Q); + addField(chanTypeObj, inv, t, c, FLD_EFF); + if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { + addField(chanTypeObj, inv, t, c, FLD_IRR); + chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c); + } + } + } + + if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { + root["events"] = inv->EventLog()->getEntryCount(); + } else { + root["events"] = -1; } } @@ -217,10 +221,38 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096 * INV_MAX_COUNT); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); auto& root = response->getRoot(); - generateJsonResponse(root); + JsonArray invArray = root.createNestedArray("inverters"); + + uint64_t serial = 0; + if (request->hasParam("inv")) { + String s = request->getParam("inv")->value(); + serial = strtoll(s.c_str(), NULL, 16); + } + + if (serial > 0) { + auto inv = Hoymiles.getInverterBySerial(serial); + if (inv != nullptr) { + JsonObject invObject = invArray.createNestedObject(); + generateInverterCommonJsonResponse(invObject, inv); + generateInverterChannelJsonResponse(invObject, inv); + } + } else { + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } + + JsonObject invObject = invArray.createNestedObject(); + generateInverterCommonJsonResponse(invObject, inv); + } + } + + generateCommonJsonResponse(root); response->setLength(); request->send(response); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index dbcd96a7a..ab48f2a7f 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -136,7 +136,8 @@ "Ok": "Ok", "Unknown": "Unbekannt", "ShowGridProfile": "Zeige Grid Profil", - "GridProfile": "Grid Profil" + "GridProfile": "Grid Profil", + "LoadingInverter": "Warte auf Daten... (kann bis zu 10 Sekunden dauern)" }, "eventlog": { "Start": "Begin", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 71ba094fb..8ccbb1bca 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -136,7 +136,8 @@ "Ok": "Ok", "Unknown": "Unknown", "ShowGridProfile": "Show Grid Profile", - "GridProfile": "Grid Profile" + "GridProfile": "Grid Profile", + "LoadingInverter": "Waiting for data... (can take up to 10 seconds)" }, "eventlog": { "Start": "Start", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index a763e206d..3f7069498 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -136,7 +136,8 @@ "Ok": "OK", "Unknown": "Inconnu", "ShowGridProfile": "Show Grid Profile", - "GridProfile": "Grid Profile" + "GridProfile": "Grid Profile", + "LoadingInverter": "Waiting for data... (can take up to 10 seconds)" }, "eventlog": { "Start": "Départ", diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index d2fca8388..953d5cf49 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -103,20 +103,30 @@