From 054a67757525919dd44b828c1be7790a0415daf1 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 23 Mar 2024 21:03:56 +0100 Subject: [PATCH] DPL settings in web app: split metadata from config users are manipulating the DPL using HTTP POST requests. often they are requesting the current settings using HTTP GET on the respective route, then change a particular settings, and send all the data back using HTTP POST. if they failed to remove the metadata node from the JSON, OpenDTU-OnBattery would not be able to process the JSON due to its size. the web app does not submit the metadata. to avoid problems, the metadata is now split from the configuration data. --- include/WebApi_powerlimiter.h | 1 + src/WebApi_powerlimiter.cpp | 35 +++++++++----- webapp/src/types/PowerLimiterConfig.ts | 1 - webapp/src/views/PowerLimiterAdminView.vue | 55 ++++++++++++---------- 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/include/WebApi_powerlimiter.h b/include/WebApi_powerlimiter.h index 7e076e0bb..c846e6d17 100644 --- a/include/WebApi_powerlimiter.h +++ b/include/WebApi_powerlimiter.h @@ -11,6 +11,7 @@ class WebApiPowerLimiterClass { private: void onStatus(AsyncWebServerRequest* request); + void onMetaData(AsyncWebServerRequest* request); void onAdminGet(AsyncWebServerRequest* request); void onAdminPost(AsyncWebServerRequest* request); diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index ecbbcfbfc..81987a231 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -22,18 +22,14 @@ void WebApiPowerLimiterClass::init(AsyncWebServer& server, Scheduler& scheduler) _server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1)); _server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1)); _server->on("/api/powerlimiter/config", HTTP_POST, std::bind(&WebApiPowerLimiterClass::onAdminPost, this, _1)); + _server->on("/api/powerlimiter/metadata", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onMetaData, this, _1)); } void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) { auto const& config = Configuration.get(); - size_t invAmount = 0; - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - if (config.Inverter[i].Serial != 0) { ++invAmount; } - } - - AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024 + 384 * invAmount); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 512); auto& root = response->getRoot(); root["enabled"] = config.PowerLimiter.Enabled; @@ -60,12 +56,29 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root["full_solar_passthrough_start_voltage"] = static_cast(config.PowerLimiter.FullSolarPassThroughStartVoltage * 100 + 0.5) / 100.0; root["full_solar_passthrough_stop_voltage"] = static_cast(config.PowerLimiter.FullSolarPassThroughStopVoltage * 100 + 0.5) / 100.0; - JsonObject metadata = root.createNestedObject("metadata"); - metadata["power_meter_enabled"] = config.PowerMeter.Enabled; - metadata["battery_enabled"] = config.Battery.Enabled; - metadata["charge_controller_enabled"] = config.Vedirect.Enabled; + response->setLength(); + request->send(response); +} + +void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { return; } + + auto const& config = Configuration.get(); + + size_t invAmount = 0; + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial != 0) { ++invAmount; } + } + + AsyncJsonResponse* response = new AsyncJsonResponse(false, 256 + 256 * invAmount); + auto& root = response->getRoot(); + + root["power_meter_enabled"] = config.PowerMeter.Enabled; + root["battery_enabled"] = config.Battery.Enabled; + root["charge_controller_enabled"] = config.Vedirect.Enabled; - JsonObject inverters = metadata.createNestedObject("inverters"); + JsonObject inverters = root.createNestedObject("inverters"); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial == 0) { continue; } diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index efd0451fd..c6edde8d6 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -42,5 +42,4 @@ export interface PowerLimiterConfig { full_solar_passthrough_soc: number; full_solar_passthrough_start_voltage: number; full_solar_passthrough_stop_voltage: number; - metadata: PowerLimiterMetaData; } diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index 10be443c0..0a58b4a9d 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -57,7 +57,7 @@
@@ -74,7 +74,7 @@
@@ -194,7 +194,7 @@ - + @@ -208,7 +208,7 @@ import CardElement from '@/components/CardElement.vue'; import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue'; -import type { PowerLimiterConfig } from "@/types/PowerLimiterConfig"; +import type { PowerLimiterConfig, PowerLimiterMetaData } from "@/types/PowerLimiterConfig"; export default defineComponent({ components: { @@ -223,6 +223,7 @@ export default defineComponent({ return { dataLoading: true, powerLimiterConfigList: {} as PowerLimiterConfig, + powerLimiterMetaData: {} as PowerLimiterMetaData, alertMessage: "", alertType: "info", showAlert: false, @@ -230,17 +231,18 @@ export default defineComponent({ }; }, created() { - this.getPowerLimiterConfig(); + this.getAllData(); }, watch: { 'powerLimiterConfigList.inverter_serial'(newVal) { var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; if (newVal === "") { return; } // do not try to convert the placeholder value - if (cfg.metadata.inverters[newVal] !== undefined) { return; } + if (meta.inverters[newVal] !== undefined) { return; } - for (const [serial, inverter] of Object.entries(cfg.metadata.inverters)) { + for (const [serial, inverter] of Object.entries(meta.inverters)) { // cfg.inverter_serial might be too large to parse as a 32 bit // int, so we make sure to only try to parse two characters. if // cfg.inverter_serial is indeed an old position based index, @@ -261,30 +263,31 @@ export default defineComponent({ methods: { getConfigHints() { var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; var hints = []; - if (cfg.metadata.power_meter_enabled !== true) { + if (meta.power_meter_enabled !== true) { hints.push({severity: "requirement", subject: "PowerMeterDisabled"}); this.configAlert = true; } - if (typeof cfg.metadata.inverters === "undefined" || Object.keys(cfg.metadata.inverters).length == 0) { + if (typeof meta.inverters === "undefined" || Object.keys(meta.inverters).length == 0) { hints.push({severity: "requirement", subject: "NoInverter"}); this.configAlert = true; } else { - var inv = cfg.metadata.inverters[cfg.inverter_serial]; + var inv = meta.inverters[cfg.inverter_serial]; if (inv !== undefined && !(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) { hints.push({severity: "requirement", subject: "InverterCommunication"}); } } if (!cfg.is_inverter_solar_powered) { - if (!cfg.metadata.charge_controller_enabled) { + if (!meta.charge_controller_enabled) { hints.push({severity: "optional", subject: "NoChargeController"}); } - if (!cfg.metadata.battery_enabled) { + if (!meta.battery_enabled) { hints.push({severity: "optional", subject: "NoBatteryInterface"}); } } @@ -296,13 +299,15 @@ export default defineComponent({ }, canUseSolarPassthrough() { var cfg = this.powerLimiterConfigList; - var canUse = this.isEnabled() && cfg.metadata.charge_controller_enabled && !cfg.is_inverter_solar_powered; + var meta = this.powerLimiterMetaData; + var canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered; if (!canUse) { cfg.solar_passthrough_enabled = false; } return canUse; }, canUseSoCThresholds() { var cfg = this.powerLimiterConfigList; - return this.isEnabled() && cfg.metadata.battery_enabled && !cfg.is_inverter_solar_powered; + var meta = this.powerLimiterMetaData; + return this.isEnabled() && meta.battery_enabled && !cfg.is_inverter_solar_powered; }, canUseVoltageThresholds() { var cfg = this.powerLimiterConfigList; @@ -316,6 +321,7 @@ export default defineComponent({ }, needsChannelSelection() { var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; var reset = function() { cfg.inverter_channel_id = 0; @@ -326,7 +332,7 @@ export default defineComponent({ if (cfg.is_inverter_solar_powered) { return reset(); } - var inverter = cfg.metadata.inverters[cfg.inverter_serial]; + var inverter = meta.inverters[cfg.inverter_serial]; if (inverter === undefined) { return reset(); } if (cfg.inverter_channel_id >= inverter.channels) { @@ -335,24 +341,25 @@ export default defineComponent({ return inverter.channels > 1; }, - getPowerLimiterConfig() { + getAllData() { this.dataLoading = true; - fetch("/api/powerlimiter/config", { headers: authHeader() }) + fetch("/api/powerlimiter/metadata", { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) .then((data) => { - this.powerLimiterConfigList = data; - this.dataLoading = false; + this.powerLimiterMetaData = data; + fetch("/api/powerlimiter/config", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter, this.$router)) + .then((data) => { + this.powerLimiterConfigList = data; + this.dataLoading = false; + }); }); }, savePowerLimiterConfig(e: Event) { e.preventDefault(); const formData = new FormData(); - formData.append("data", JSON.stringify(this.powerLimiterConfigList, (key, value) => { - // do not submit metadata - if (key === "metadata") { return undefined; } - return value; - })); + formData.append("data", JSON.stringify(this.powerLimiterConfigList)); fetch("/api/powerlimiter/config", { method: "POST",