From 50635ee2ce64494f8a745cc4771d3d457bcbaf64 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Mon, 4 Mar 2024 20:27:47 +0100 Subject: [PATCH] Feature: live view: update with respective frequency the update frequency of Victron MPPT charger data, the battery Soc, the huawei charger power, and the power meter differ from one another, and differ in particular from the inverter update frequency. the OnBattery-specific data is now handled in a new method, outside the upstream code, which merely call the new function(s). the new function will update the websocket independently from inverter updates. also, it adds the respective data if it actually changed since it was last updated through the websocket. for the webapp to be able to recover in case of errors, all values are also written to the websocket with a fixed interval of 10 seconds. --- include/WebApi_ws_live.h | 9 ++++ src/WebApi_ws_live.cpp | 85 ++++++++++++++++++++++++++--------- webapp/src/views/HomeView.vue | 12 +++-- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 05f8ab8f9..4a29fff5b 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -17,6 +17,9 @@ class WebApiWsLiveClass { static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv); static void generateCommonJsonResponse(JsonVariant& root); + void generateOnBatteryJsonResponse(JsonVariant& root, bool all); + void sendOnBatteryStats(); + 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); @@ -25,6 +28,12 @@ class WebApiWsLiveClass { AsyncWebSocket _ws; + uint32_t _lastPublishOnBatteryFull = 0; + uint32_t _lastPublishVictron = 0; + uint32_t _lastPublishHuawei = 0; + uint32_t _lastPublishBattery = 0; + uint32_t _lastPublishPowerMeter = 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 e51361d7c..d2ed35d9d 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -54,6 +54,66 @@ void WebApiWsLiveClass::wsCleanupTaskCb() } } +void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool all) +{ + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; + + if (all || (millis() - _lastPublishVictron) > VictronMppt.getDataAgeMillis()) { + JsonObject vedirectObj = root.createNestedObject("vedirect"); + vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled; + JsonObject totalVeObj = vedirectObj.createNestedObject("total"); + + addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); + addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); + addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); + + if (!all) { _lastPublishVictron = millis(); } + } + + if (all || (HuaweiCan.getLastUpdate() - _lastPublishHuawei) < halfOfAllMillis ) { + JsonObject huaweiObj = root.createNestedObject("huawei"); + huaweiObj["enabled"] = Configuration.get().Huawei.Enabled; + const RectifierParameters_t * rp = HuaweiCan.get(); + addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); + + if (!all) { _lastPublishHuawei = millis(); } + } + + auto spStats = Battery.getStats(); + if (all || spStats->updateAvailable(_lastPublishBattery)) { + JsonObject batteryObj = root.createNestedObject("battery"); + batteryObj["enabled"] = Configuration.get().Battery.Enabled; + addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0); + + if (!all) { _lastPublishBattery = millis(); } + } + + if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { + JsonObject powerMeterObj = root.createNestedObject("power_meter"); + powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled; + addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); + + if (!all) { _lastPublishPowerMeter = millis(); } + } +} + +void WebApiWsLiveClass::sendOnBatteryStats() +{ + DynamicJsonDocument root(512); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; } + + JsonVariant var = root; + + bool all = (millis() - _lastPublishOnBatteryFull) > 10 * 1000; + if (all) { _lastPublishOnBatteryFull = millis(); } + generateOnBatteryJsonResponse(var, all); + + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer); +} + void WebApiWsLiveClass::sendDataTaskCb() { // do nothing if no WS client is connected @@ -61,6 +121,8 @@ void WebApiWsLiveClass::sendDataTaskCb() return; } + sendOnBatteryStats(); + // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); @@ -115,27 +177,6 @@ void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0; - - JsonObject vedirectObj = root.createNestedObject("vedirect"); - vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled; - JsonObject totalVeObj = vedirectObj.createNestedObject("total"); - - addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); - addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); - addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); - - JsonObject huaweiObj = root.createNestedObject("huawei"); - huaweiObj["enabled"] = Configuration.get().Huawei.Enabled; - const RectifierParameters_t * rp = HuaweiCan.get(); - addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); - - JsonObject batteryObj = root.createNestedObject("battery"); - batteryObj["enabled"] = Configuration.get().Battery.Enabled; - addTotalField(batteryObj, "soc", Battery.getStats()->getSoC(), "%", 0); - - JsonObject powerMeterObj = root.createNestedObject("power_meter"); - powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled; - addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); } void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv) @@ -279,6 +320,8 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) generateCommonJsonResponse(root); + generateOnBatteryJsonResponse(root, true); + response->setLength(); request->send(response); diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index b8f4b682c..9371d0fae 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -461,12 +461,16 @@ export default defineComponent({ console.log(event); if (event.data != "{}") { const newData = JSON.parse(event.data); + + if (typeof newData.vedirect !== 'undefined') { Object.assign(this.liveData.vedirect, newData.vedirect); } + if (typeof newData.huawei !== 'undefined') { Object.assign(this.liveData.huawei, newData.huawei); } + if (typeof newData.battery !== 'undefined') { Object.assign(this.liveData.battery, newData.battery); } + if (typeof newData.power_meter !== 'undefined') { Object.assign(this.liveData.power_meter, newData.power_meter); } + + if (typeof newData.total === 'undefined') { return; } + Object.assign(this.liveData.total, newData.total); Object.assign(this.liveData.hints, newData.hints); - Object.assign(this.liveData.vedirect, newData.vedirect); - Object.assign(this.liveData.huawei, newData.huawei); - Object.assign(this.liveData.battery, newData.battery); - Object.assign(this.liveData.power_meter, newData.power_meter); const foundIdx = this.liveData.inverters.findIndex((element) => element.serial == newData.inverters[0].serial); if (foundIdx == -1) {