BREAKING CHANGE: Web API Endpoint /api/livedata/status
To reduce the heap usage it is necessary to send the inverters one by one instead of a huge response. A simple call to `/api/livedata/status` returns just some very general information. If detailed inverter information are required the inverter serial number has to appended `?inv=<serial number>`. The websocket also returns only one inverter at a time. It as to be assembled at client side.
This commit is contained in:
parent
557c5d645e
commit
c27ecc3620
@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <Hoymiles.h>
|
||||
@ -12,16 +13,19 @@ public:
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void generateJsonResponse(JsonVariant& root);
|
||||
void addField(JsonObject& root, std::shared_ptr<InverterAbstract> 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<InverterAbstract> inv);
|
||||
static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
|
||||
static void generateCommonJsonResponse(JsonVariant& root);
|
||||
|
||||
static void addField(JsonObject& root, std::shared_ptr<InverterAbstract> 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;
|
||||
|
||||
|
||||
@ -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,43 +57,6 @@ void WebApiWsLiveClass::sendDataTaskCb()
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t maxTimeStamp = 0;
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
maxTimeStamp = std::max<uint32_t>(maxTimeStamp, inv->Statistics()->getLastUpdate());
|
||||
}
|
||||
|
||||
// Update on every inverter change or at least after 10 seconds
|
||||
if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) {
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
DynamicJsonDocument root(4096 * INV_MAX_COUNT);
|
||||
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
|
||||
_ws.textAll(buffer);
|
||||
_newestInverterTimestamp = maxTimeStamp;
|
||||
}
|
||||
|
||||
} 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)
|
||||
{
|
||||
JsonArray invArray = root.createNestedArray("inverters");
|
||||
|
||||
// Loop all inverters
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
@ -102,64 +64,43 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject invObject = invArray.createNestedObject();
|
||||
INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
|
||||
if (inv_cfg == nullptr) {
|
||||
const uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal();
|
||||
if (!((lastUpdateInternal > 0 && lastUpdateInternal > _lastPublishStats[i]) || (millis() - _lastPublishStats[i] > (10 * 1000)))) {
|
||||
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;
|
||||
}
|
||||
_lastPublishStats[i] = millis();
|
||||
|
||||
// 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<uint8_t>(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);
|
||||
}
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
DynamicJsonDocument root(4096);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
JsonVariant var = root;
|
||||
|
||||
if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) {
|
||||
invObject["events"] = inv->EventLog()->getEntryCount();
|
||||
} else {
|
||||
invObject["events"] = -1;
|
||||
auto invArray = var.createNestedArray("inverters");
|
||||
auto invObject = invArray.createNestedObject();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root)
|
||||
{
|
||||
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<InverterAbstract> 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<InverterAbstract> 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<uint8_t>(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<std::mutex> 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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -103,20 +103,30 @@
|
||||
<div class="card-body">
|
||||
<div class="row flex-row-reverse flex-wrap-reverse g-3">
|
||||
<template v-for="chanType in [{obj: inverter.INV, name: 'INV'}, {obj: inverter.AC, name: 'AC'}, {obj: inverter.DC, name: 'DC'}].reverse()">
|
||||
<template v-for="channel in Object.keys(chanType.obj).sort().reverse().map(x=>+x)" :key="channel">
|
||||
<template v-if="(chanType.name != 'DC') ||
|
||||
(chanType.name == 'DC' && getSumIrridiation(inverter) == 0) ||
|
||||
(chanType.name == 'DC' && getSumIrridiation(inverter) > 0 && chanType.obj[channel].Irradiation?.max || 0 > 0)
|
||||
">
|
||||
<div class="col">
|
||||
<InverterChannelInfo :channelData="chanType.obj[channel]"
|
||||
:channelType="chanType.name"
|
||||
:channelNumber="channel" />
|
||||
</div>
|
||||
<template v-if="chanType.obj != null">
|
||||
<template v-for="channel in Object.keys(chanType.obj).sort().reverse().map(x=>+x)" :key="channel">
|
||||
<template v-if="(chanType.name != 'DC') ||
|
||||
(chanType.name == 'DC' && getSumIrridiation(inverter) == 0) ||
|
||||
(chanType.name == 'DC' && getSumIrridiation(inverter) > 0 && chanType.obj[channel].Irradiation?.max || 0 > 0)
|
||||
">
|
||||
<div class="col">
|
||||
<InverterChannelInfo :channelData="chanType.obj[channel]"
|
||||
:channelType="chanType.name"
|
||||
:channelNumber="channel" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<BootstrapAlert class="m-3" :show="!inverter.hasOwnProperty('INV')">
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="spinner-border m-1" role="status">
|
||||
<span class="visually-hidden">{{ $t('home.LoadingInverter') }}</span>
|
||||
</div>
|
||||
<span>{{ $t('home.LoadingInverter') }}</span>
|
||||
</div>
|
||||
</BootstrapAlert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -441,7 +451,16 @@ export default defineComponent({
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
if (event.data != "{}") {
|
||||
this.liveData = JSON.parse(event.data);
|
||||
const newData = JSON.parse(event.data);
|
||||
Object.assign(this.liveData.total, newData.total);
|
||||
Object.assign(this.liveData.hints, newData.hints);
|
||||
|
||||
const foundIdx = this.liveData.inverters.findIndex((element) => element.serial == newData.inverters[0].serial);
|
||||
if (foundIdx == -1) {
|
||||
Object.assign(this.liveData.inverters, newData.inverters);
|
||||
} else {
|
||||
Object.assign(this.liveData.inverters[foundIdx], newData.inverters[0]);
|
||||
}
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
} else {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user