From 8ec1695d1beff874097cbae9840cb06693c78870 Mon Sep 17 00:00:00 2001 From: Marvin Carstensen Date: Tue, 23 Apr 2024 19:17:01 +0200 Subject: [PATCH 01/50] Feature: support Tibber bridge as power meter interface --- include/Configuration.h | 9 ++ include/PowerMeter.h | 13 +- include/TibberPowerMeter.h | 23 +++ include/WebApi_powermeter.h | 2 + src/Configuration.cpp | 12 ++ src/PowerMeter.cpp | 9 ++ src/TibberPowerMeter.cpp | 188 +++++++++++++++++++++++ src/WebApi_powermeter.cpp | 85 ++++++++++ webapp/src/locales/de.json | 4 +- webapp/src/locales/en.json | 4 +- webapp/src/types/PowerMeterConfig.ts | 8 + webapp/src/views/PowerMeterAdminView.vue | 68 +++++++- 12 files changed, 416 insertions(+), 9 deletions(-) create mode 100644 include/TibberPowerMeter.h create mode 100644 src/TibberPowerMeter.cpp diff --git a/include/Configuration.h b/include/Configuration.h index cd810d84..b0e28621 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -78,6 +78,14 @@ struct POWERMETER_HTTP_PHASE_CONFIG_T { }; using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T; +struct POWERMETER_TIBBER_CONFIG_T { + char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1]; + char Username[POWERMETER_MAX_USERNAME_STRLEN + 1]; + char Password[POWERMETER_MAX_USERNAME_STRLEN + 1]; + uint16_t Timeout; +}; +using PowerMeterTibberConfig = struct POWERMETER_TIBBER_CONFIG_T; + struct CONFIG_T { struct { uint32_t Version; @@ -200,6 +208,7 @@ struct CONFIG_T { uint32_t HttpInterval; bool HttpIndividualRequests; PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES]; + PowerMeterTibberConfig Tibber; } PowerMeter; struct { diff --git a/include/PowerMeter.h b/include/PowerMeter.h index 5b3d8f31..2d2c8749 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -26,13 +26,19 @@ public: SDM3PH = 2, HTTP = 3, SML = 4, - SMAHM2 = 5 + SMAHM2 = 5, + TIBBER = 6 }; void init(Scheduler& scheduler); float getPowerTotal(bool forceUpdate = true); uint32_t getLastPowerMeterUpdate(); bool isDataValid(); + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power}, + {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport}, + {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport} + }; private: void loop(); void mqtt(); @@ -68,11 +74,6 @@ private: void readPowerMeter(); bool smlReadLoop(); - const std::list smlHandlerList{ - {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power}, - {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport}, - {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport} - }; }; extern PowerMeterClass PowerMeter; diff --git a/include/TibberPowerMeter.h b/include/TibberPowerMeter.h new file mode 100644 index 00000000..84639ab7 --- /dev/null +++ b/include/TibberPowerMeter.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include "Configuration.h" + +class TibberPowerMeterClass { +public: + bool updateValues(); + char tibberPowerMeterError[256]; + bool query(PowerMeterTibberConfig const& config); + +private: + HTTPClient httpClient; + String httpResponse; + bool httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config); + bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); + void prepareRequest(uint32_t timeout); +}; + +extern TibberPowerMeterClass TibberPowerMeter; diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h index 7e873b1c..12e5afae 100644 --- a/include/WebApi_powermeter.h +++ b/include/WebApi_powermeter.h @@ -15,7 +15,9 @@ private: void onAdminGet(AsyncWebServerRequest* request); void onAdminPost(AsyncWebServerRequest* request); void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const; + void decodeJsonTibberConfig(JsonObject const& json, PowerMeterTibberConfig& config) const; void onTestHttpRequest(AsyncWebServerRequest* request); + void onTestTibberRequest(AsyncWebServerRequest* request); AsyncWebServer* _server; }; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 9adda28d..0749a1a2 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -159,6 +159,12 @@ bool ConfigurationClass::write() powermeter["sdmaddress"] = config.PowerMeter.SdmAddress; powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; + JsonObject tibber = powermeter["tibber"].to(); + tibber["url"] = config.PowerMeter.Tibber.Url; + tibber["username"] = config.PowerMeter.Tibber.Username; + tibber["password"] = config.PowerMeter.Tibber.Password; + tibber["timeout"] = config.PowerMeter.Tibber.Timeout; + JsonArray powermeter_http_phases = powermeter["http_phases"].to(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { JsonObject powermeter_phase = powermeter_http_phases.add(); @@ -420,6 +426,12 @@ bool ConfigurationClass::read() config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false; + JsonObject tibber = powermeter["tibber"]; + strlcpy(config.PowerMeter.Tibber.Url, tibber["url"] | "", sizeof(config.PowerMeter.Tibber.Url)); + strlcpy(config.PowerMeter.Tibber.Username, tibber["username"] | "", sizeof(config.PowerMeter.Tibber.Username)); + strlcpy(config.PowerMeter.Tibber.Password, tibber["password"] | "", sizeof(config.PowerMeter.Tibber.Password)); + config.PowerMeter.Tibber.Timeout = tibber["timeout"] | POWERMETER_HTTP_TIMEOUT; + JsonArray powermeter_http_phases = powermeter["http_phases"]; for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { JsonObject powermeter_phase = powermeter_http_phases[i].as(); diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 56582d00..e1e2d432 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -6,6 +6,7 @@ #include "Configuration.h" #include "PinMapping.h" #include "HttpPowerMeter.h" +#include "TibberPowerMeter.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "MessageOutput.h" @@ -96,6 +97,9 @@ void PowerMeterClass::init(Scheduler& scheduler) case Source::SMAHM2: SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging); break; + + case Source::TIBBER: + break; } } @@ -274,6 +278,11 @@ void PowerMeterClass::readPowerMeter() _powerMeter3Power = SMA_HM.getPowerL3(); _lastPowerMeterUpdate = millis(); } + else if (configuredSource == Source::TIBBER) { + if (TibberPowerMeter.updateValues()) { + _lastPowerMeterUpdate = millis(); + } + } } bool PowerMeterClass::smlReadLoop() diff --git a/src/TibberPowerMeter.cpp b/src/TibberPowerMeter.cpp new file mode 100644 index 00000000..d7889c22 --- /dev/null +++ b/src/TibberPowerMeter.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "Configuration.h" +#include "TibberPowerMeter.h" +#include "MessageOutput.h" +#include +#include +#include +#include + +bool TibberPowerMeterClass::updateValues() +{ + auto const& config = Configuration.get(); + + auto const& tibberConfig = config.PowerMeter.Tibber; + + if (!query(tibberConfig)) { + MessageOutput.printf("[TibberPowerMeter] Getting the power of tibber failed.\r\n"); + MessageOutput.printf("%s\r\n", tibberPowerMeterError); + return false; + } + + return true; +} + +bool TibberPowerMeterClass::query(PowerMeterTibberConfig const& config) +{ + //hostByName in WiFiGeneric fails to resolve local names. issue described in + //https://github.com/espressif/arduino-esp32/issues/3822 + //and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 + //in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses. + //have to do it manually here. Feels Hacky... + String protocol; + String host; + String uri; + String base64Authorization; + uint16_t port; + extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization); + + IPAddress ipaddr((uint32_t)0); + //first check if "host" is already an IP adress + if (!ipaddr.fromString(host)) + { + //"host"" is not an IP address so try to resolve the IP adress + //first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() will spam the console if done the otherway around. + const bool mdnsEnabled = Configuration.get().Mdns.Enabled; + if (!mdnsEnabled) { + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str()); + //ensure we try resolving via DNS even if mDNS is disabled + if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); + } + } + else + { + ipaddr = MDNS.queryHost(host); + if (ipaddr == INADDR_NONE){ + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str()); + //when we cannot find local server via mDNS, try resolving via DNS + if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); + } + } + } + } + + // secureWifiClient MUST be created before HTTPClient + // see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381 + std::unique_ptr wifiClient; + + bool https = protocol == "https"; + if (https) { + auto secureWifiClient = std::make_unique(); + secureWifiClient->setInsecure(); + wifiClient = std::move(secureWifiClient); + } else { + wifiClient = std::make_unique(); + } + + return httpRequest(*wifiClient, ipaddr.toString(), port, uri, https, config); +} + +bool TibberPowerMeterClass::httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) +{ + if(!httpClient.begin(wifiClient, host, port, uri, https)){ + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); + return false; + } + + prepareRequest(config.Timeout); + + String authString = config.Username; + authString += ":"; + authString += config.Password; + String auth = "Basic "; + auth.concat(base64::encode(authString)); + httpClient.addHeader("Authorization", auth); + + int httpCode = httpClient.GET(); + + if (httpCode <= 0) { + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str()); + return false; + } + + if (httpCode != HTTP_CODE_OK) { + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode); + return false; + } + + while (httpClient.getStream().available()) { + double readVal = 0; + unsigned char smlCurrentChar = httpClient.getStream().read(); + sml_states_t smlCurrentState = smlState(smlCurrentChar); + if (smlCurrentState == SML_LISTEND) { + for (auto& handler: PowerMeter.smlHandlerList) { + if (smlOBISCheck(handler.OBIS)) { + handler.Fn(readVal); + *handler.Arg = readVal; + } + } + } + } + httpClient.end(); + + return true; +} + +//extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 +bool TibberPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) +{ + // check for : (http: or https: + int index = url.indexOf(':'); + if(index < 0) { + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("failed to parse protocol")); + return false; + } + + _protocol = url.substring(0, index); + + //initialize port to default values for http or https. + //port will be overwritten below in case port is explicitly defined + _port = (_protocol == "https" ? 443 : 80); + + url.remove(0, (index + 3)); // remove http:// or https:// + + index = url.indexOf('/'); + if (index == -1) { + index = url.length(); + url += '/'; + } + String host = url.substring(0, index); + url.remove(0, index); // remove host part + + // get Authorization + index = host.indexOf('@'); + if(index >= 0) { + // auth info + String auth = host.substring(0, index); + host.remove(0, index + 1); // remove auth part including @ + _base64Authorization = base64::encode(auth); + } + + // get port + index = host.indexOf(':'); + String the_host; + if(index >= 0) { + the_host = host.substring(0, index); // hostname + host.remove(0, (index + 1)); // remove hostname + : + _port = host.toInt(); // get port + } else { + the_host = host; + } + + _host = the_host; + _uri = url; + return true; +} + +void TibberPowerMeterClass::prepareRequest(uint32_t timeout) { + httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + httpClient.setUserAgent("OpenDTU-OnBattery"); + httpClient.setConnectTimeout(timeout); + httpClient.setTimeout(timeout); + httpClient.addHeader("Content-Type", "application/json"); + httpClient.addHeader("Accept", "application/json"); +} + +TibberPowerMeterClass TibberPowerMeter; diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 8ca492b0..74472bc0 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -13,6 +13,7 @@ #include "PowerLimiter.h" #include "PowerMeter.h" #include "HttpPowerMeter.h" +#include "TibberPowerMeter.h" #include "WebApi.h" #include "helper.h" @@ -26,6 +27,7 @@ void WebApiPowerMeterClass::init(AsyncWebServer& server, Scheduler& scheduler) _server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1)); _server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, this, _1)); _server->on("/api/powermeter/testhttprequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpRequest, this, _1)); + _server->on("/api/powermeter/testtibberrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestTibberRequest, this, _1)); } void WebApiPowerMeterClass::decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const @@ -43,6 +45,14 @@ void WebApiPowerMeterClass::decodeJsonPhaseConfig(JsonObject const& json, PowerM config.SignInverted = json["sign_inverted"].as(); } +void WebApiPowerMeterClass::decodeJsonTibberConfig(JsonObject const& json, PowerMeterTibberConfig& config) const +{ + strlcpy(config.Url, json["url"].as().c_str(), sizeof(config.Url)); + strlcpy(config.Username, json["username"].as().c_str(), sizeof(config.Username)); + strlcpy(config.Password, json["password"].as().c_str(), sizeof(config.Password)); + config.Timeout = json["timeout"].as(); +} + void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(); @@ -60,6 +70,12 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) root["sdmaddress"] = config.PowerMeter.SdmAddress; root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; + auto tibber = root["tibber"].to(); + tibber["url"] = String(config.PowerMeter.Tibber.Url); + tibber["username"] = String(config.PowerMeter.Tibber.Username); + tibber["password"] = String(config.PowerMeter.Tibber.Password); + tibber["timeout"] = config.PowerMeter.Tibber.Timeout; + auto httpPhases = root["http_phases"].to(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { @@ -158,6 +174,34 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } } + if (static_cast(root["source"].as()) == PowerMeterClass::Source::TIBBER) { + JsonObject tibber = root["tibber"]; + + if (!tibber.containsKey("url") + || (!tibber["url"].as().startsWith("http://") + && !tibber["url"].as().startsWith("https://"))) { + retMsg["message"] = "URL must either start with http:// or https://!"; + response->setLength(); + request->send(response); + return; + } + + if ((tibber["username"].as().length() == 0 || tibber["password"].as().length() == 0)) { + retMsg["message"] = "Username or password must not be empty!"; + response->setLength(); + request->send(response); + return; + } + + if (!tibber.containsKey("timeout") + || tibber["timeout"].as() <= 0) { + retMsg["message"] = "Timeout must be greater than 0 ms!"; + response->setLength(); + request->send(response); + return; + } + } + CONFIG_T& config = Configuration.get(); config.PowerMeter.Enabled = root["enabled"].as(); config.PowerMeter.VerboseLogging = root["verbose_logging"].as(); @@ -170,6 +214,8 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerMeter.SdmAddress = root["sdmaddress"].as(); config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as(); + decodeJsonTibberConfig(root["tibber"].as(), config.PowerMeter.Tibber); + JsonArray http_phases = root["http_phases"]; for (uint8_t i = 0; i < http_phases.size(); i++) { decodeJsonPhaseConfig(http_phases[i].as(), config.PowerMeter.Http_Phase[i]); @@ -228,3 +274,42 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) asyncJsonResponse->setLength(); request->send(asyncJsonResponse); } + +void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* asyncJsonResponse = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, asyncJsonResponse, root)) { + return; + } + + auto& retMsg = asyncJsonResponse->getRoot(); + + if (!root.containsKey("url") || !root.containsKey("username") || !root.containsKey("password") + || !root.containsKey("timeout")) { + retMsg["message"] = "Missing fields!"; + asyncJsonResponse->setLength(); + request->send(asyncJsonResponse); + return; + } + + + char response[256]; + + PowerMeterTibberConfig tibberConfig; + decodeJsonTibberConfig(root.as(), tibberConfig); + if (TibberPowerMeter.query(tibberConfig)) { + retMsg["type"] = "success"; + snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", PowerMeter.getPowerTotal()); + } else { + snprintf_P(response, sizeof(response), "%s", TibberPowerMeter.tibberPowerMeterError); + } + + retMsg["message"] = response; + asyncJsonResponse->setLength(); + request->send(asyncJsonResponse); +} diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 120cd55c..db78e137 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -563,6 +563,7 @@ "typeHTTP": "HTTP(S) + JSON", "typeSML": "SML (OBIS 16.7.0)", "typeSMAHM2": "SMA Homemanager 2.0", + "typeTIBBER": "Tibber Pulse (via Tibber Bridge)", "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", @@ -587,7 +588,8 @@ "httpSignInverted": "Vorzeichen umkehren", "httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", "httpTimeout": "Timeout", - "testHttpRequest": "Testen" + "testHttpRequest": "Testen", + "TIBBER": "Tibber Pulse (via Tibber Bridge) - Konfiguration" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 61fa972b..2e3dde41 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -565,6 +565,7 @@ "typeHTTP": "HTTP(s) + JSON", "typeSML": "SML (OBIS 16.7.0)", "typeSMAHM2": "SMA Homemanager 2.0", + "typeTIBBER": "Tibber Pulse (via Tibber Bridge)", "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", @@ -593,7 +594,8 @@ "httpSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", "httpTimeout": "Timeout", "testHttpRequest": "Run test", - "milliSeconds": "ms" + "milliSeconds": "ms", + "TIBBER": "Tibber Pulse (via Tibber Bridge) - Configuration" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Settings", diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index a8ceb4f7..97238929 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -13,6 +13,13 @@ export interface PowerMeterHttpPhaseConfig { sign_inverted: boolean; } +export interface PowerMeterTibberConfig { + url: string; + username: string; + password: string; + timeout: number; +} + export interface PowerMeterConfig { enabled: boolean; verbose_logging: boolean; @@ -25,4 +32,5 @@ export interface PowerMeterConfig { sdmaddress: number; http_individual_requests: boolean; http_phases: Array; + tibber: PowerMeterTibberConfig; } diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index ffe0868c..aff34017 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -220,6 +220,44 @@ + +
+ + + + + + + + + + +
+ +
+ + + {{ testTibberRequestAlert.message }} + +
+
@@ -257,6 +295,7 @@ export default defineComponent({ { key: 3, value: this.$t('powermeteradmin.typeHTTP') }, { key: 4, value: this.$t('powermeteradmin.typeSML') }, { key: 5, value: this.$t('powermeteradmin.typeSMAHM2') }, + { key: 6, value: this.$t('powermeteradmin.typeTIBBER') }, ], powerMeterAuthList: [ { key: 0, value: "None" }, @@ -266,7 +305,8 @@ export default defineComponent({ alertMessage: "", alertType: "info", showAlert: false, - testHttpRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[] + testHttpRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[], + testTibberRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; } }; }, created() { @@ -347,6 +387,32 @@ export default defineComponent({ } ) }, + testTibberRequest() { + this.testTibberRequestAlert = { + message: "Sending Tibber request...", + type: "info", + show: true, + }; + + const formData = new FormData(); + formData.append("data", JSON.stringify(this.powerMeterConfigList.tibber)); + + fetch("/api/powermeter/testtibberrequest", { + method: "POST", + headers: authHeader(), + body: formData, + }) + .then((response) => handleResponse(response, this.$emitter, this.$router)) + .then( + (response) => { + this.testTibberRequestAlert = { + message: response.message, + type: response.type, + show: true, + }; + } + ) + }, }, }); From 2397e5cdf5bba4108dc968f8476701a9bc814ad9 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 8 May 2024 11:08:04 +0200 Subject: [PATCH 02/50] powermeter refactor: split providers into their own classes it is important to separate the capabilities of each power meter provider into their own class/source file, as the providers work fundamentally different and their implementations must not be intermangled, which made maintenance and improvements a nightmare in the past. --- include/PowerMeter.h | 72 +--- ...{HttpPowerMeter.h => PowerMeterHttpJson.h} | 21 +- ...TibberPowerMeter.h => PowerMeterHttpSml.h} | 29 +- include/PowerMeterMqtt.h | 28 ++ include/PowerMeterProvider.h | 46 +++ include/PowerMeterSerialSdm.h | 33 ++ include/PowerMeterSerialSml.h | 39 ++ ...SMA_HM.h => PowerMeterUdpSmaHomeManager.h} | 20 +- src/Display_Graphic.cpp | 2 +- src/Huawei_can.cpp | 4 +- src/PowerLimiter.cpp | 2 +- src/PowerMeter.cpp | 335 +++--------------- ...pPowerMeter.cpp => PowerMeterHttpJson.cpp} | 86 +++-- ...erPowerMeter.cpp => PowerMeterHttpSml.cpp} | 44 ++- src/PowerMeterMqtt.cpp | 74 ++++ src/PowerMeterProvider.cpp | 22 ++ src/PowerMeterSerialSdm.cpp | 113 ++++++ src/PowerMeterSerialSml.cpp | 68 ++++ ...HM.cpp => PowerMeterUdpSmaHomeManager.cpp} | 58 +-- src/WebApi_powermeter.cpp | 23 +- src/WebApi_ws_live.cpp | 4 +- 21 files changed, 667 insertions(+), 456 deletions(-) rename include/{HttpPowerMeter.h => PowerMeterHttpJson.h} (75%) rename include/{TibberPowerMeter.h => PowerMeterHttpSml.h} (51%) create mode 100644 include/PowerMeterMqtt.h create mode 100644 include/PowerMeterProvider.h create mode 100644 include/PowerMeterSerialSdm.h create mode 100644 include/PowerMeterSerialSml.h rename include/{SMA_HM.h => PowerMeterUdpSmaHomeManager.h} (53%) rename src/{HttpPowerMeter.cpp => PowerMeterHttpJson.cpp} (84%) rename src/{TibberPowerMeter.cpp => PowerMeterHttpSml.cpp} (84%) create mode 100644 src/PowerMeterMqtt.cpp create mode 100644 src/PowerMeterProvider.cpp create mode 100644 src/PowerMeterSerialSdm.cpp create mode 100644 src/PowerMeterSerialSml.cpp rename src/{SMA_HM.cpp => PowerMeterUdpSmaHomeManager.cpp} (76%) diff --git a/include/PowerMeter.h b/include/PowerMeter.h index 2d2c8749..44c99d06 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -1,79 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "Configuration.h" -#include -#include -#include -#include -#include -#include "SDM.h" -#include "sml.h" +#include "PowerMeterProvider.h" #include -#include - -typedef struct { - const unsigned char OBIS[6]; - void (*Fn)(double&); - float* Arg; -} OBISHandler; +#include +#include class PowerMeterClass { public: - enum class Source : unsigned { - MQTT = 0, - SDM1PH = 1, - SDM3PH = 2, - HTTP = 3, - SML = 4, - SMAHM2 = 5, - TIBBER = 6 - }; void init(Scheduler& scheduler); - float getPowerTotal(bool forceUpdate = true); - uint32_t getLastPowerMeterUpdate(); - bool isDataValid(); - const std::list smlHandlerList{ - {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power}, - {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport}, - {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport} - }; + void updateSettings(); + + float getPowerTotal() const; + uint32_t getLastUpdate() const; + bool isDataValid() const; + private: void loop(); - void mqtt(); - - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); Task _loopTask; - - bool _verboseLogging = true; - uint32_t _lastPowerMeterCheck; - // Used in Power limiter for safety check - uint32_t _lastPowerMeterUpdate; - - float _powerMeter1Power = 0.0; - float _powerMeter2Power = 0.0; - float _powerMeter3Power = 0.0; - float _powerMeter1Voltage = 0.0; - float _powerMeter2Voltage = 0.0; - float _powerMeter3Voltage = 0.0; - float _powerMeterImport = 0.0; - float _powerMeterExport = 0.0; - - std::map _mqttSubscriptions; - mutable std::mutex _mutex; - - static char constexpr _sdmSerialPortOwner[] = "SDM power meter"; - std::unique_ptr _upSdmSerial = nullptr; - std::unique_ptr _upSdm = nullptr; - std::unique_ptr _upSmlSerial = nullptr; - - void readPowerMeter(); - - bool smlReadLoop(); + std::unique_ptr _upProvider = nullptr; }; extern PowerMeterClass PowerMeter; diff --git a/include/HttpPowerMeter.h b/include/PowerMeterHttpJson.h similarity index 75% rename from include/HttpPowerMeter.h rename to include/PowerMeterHttpJson.h index 8f703bba..9e548210 100644 --- a/include/HttpPowerMeter.h +++ b/include/PowerMeterHttpJson.h @@ -5,22 +5,29 @@ #include #include #include "Configuration.h" +#include "PowerMeterProvider.h" using Auth_t = PowerMeterHttpConfig::Auth; using Unit_t = PowerMeterHttpConfig::Unit; -class HttpPowerMeterClass { +class PowerMeterHttpJson : public PowerMeterProvider { public: - void init(); - bool updateValues(); - float getPower(int8_t phase); - char httpPowerMeterError[256]; + bool init() final { return true; } + void deinit() final { } + void loop() final; + float getPowerTotal() const final; + void doMqttPublish() const final; + bool queryPhase(int phase, PowerMeterHttpConfig const& config); + char httpPowerMeterError[256]; private: - float power[POWERMETER_MAX_PHASES]; + uint32_t _lastPoll; + std::array _cache; + std::array _powerValues; HTTPClient httpClient; String httpResponse; + bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); String extractParam(String& authReq, const String& param, const char delimit); @@ -30,5 +37,3 @@ private: void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); String sha256(const String& data); }; - -extern HttpPowerMeterClass HttpPowerMeter; diff --git a/include/TibberPowerMeter.h b/include/PowerMeterHttpSml.h similarity index 51% rename from include/TibberPowerMeter.h rename to include/PowerMeterHttpSml.h index 84639ab7..31b44244 100644 --- a/include/TibberPowerMeter.h +++ b/include/PowerMeterHttpSml.h @@ -1,23 +1,46 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include +#include #include #include #include #include "Configuration.h" +#include "PowerMeterProvider.h" +#include "sml.h" -class TibberPowerMeterClass { +class PowerMeterHttpSml : public PowerMeterProvider { public: + bool init() final { return true; } + void deinit() final { } + void loop() final; + float getPowerTotal() const final; + void doMqttPublish() const final; bool updateValues(); char tibberPowerMeterError[256]; bool query(PowerMeterTibberConfig const& config); private: + mutable std::mutex _mutex; + + uint32_t _lastPoll = 0; + + float _activePower = 0.0; + + typedef struct { + const unsigned char OBIS[6]; + void (*Fn)(double&); + float* Arg; + } OBISHandler; + + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower} + }; + HTTPClient httpClient; String httpResponse; bool httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); void prepareRequest(uint32_t timeout); }; - -extern TibberPowerMeterClass TibberPowerMeter; diff --git a/include/PowerMeterMqtt.h b/include/PowerMeterMqtt.h new file mode 100644 index 00000000..5dd01d2c --- /dev/null +++ b/include/PowerMeterMqtt.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "PowerMeterProvider.h" +#include +#include +#include + +class PowerMeterMqtt : public PowerMeterProvider { +public: + bool init() final; + void deinit() final; + void loop() final { } + float getPowerTotal() const final; + void doMqttPublish() const final; + +private: + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, + const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + + float _powerValueOne = 0; + float _powerValueTwo = 0; + float _powerValueThree = 0; + + std::map _mqttSubscriptions; + + mutable std::mutex _mutex; +}; diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h new file mode 100644 index 00000000..9fb74c78 --- /dev/null +++ b/include/PowerMeterProvider.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" + +class PowerMeterProvider { +public: + virtual ~PowerMeterProvider() { } + + enum class Type : unsigned { + MQTT = 0, + SDM1PH = 1, + SDM3PH = 2, + HTTP = 3, + SML = 4, + SMAHM2 = 5, + TIBBER = 6 + }; + + // returns true if the provider is ready for use, false otherwise + virtual bool init() = 0; + + virtual void deinit() = 0; + virtual void loop() = 0; + virtual float getPowerTotal() const = 0; + + uint32_t getLastUpdate() const { return _lastUpdate; } + bool isDataValid() const; + void mqttLoop() const; + +protected: + PowerMeterProvider() { + auto const& config = Configuration.get(); + _verboseLogging = config.PowerMeter.VerboseLogging; + } + + void gotUpdate() { _lastUpdate = millis(); } + + bool _verboseLogging; + +private: + virtual void doMqttPublish() const = 0; + + uint32_t _lastUpdate = 0; + mutable uint32_t _lastMqttPublish = 0; +}; diff --git a/include/PowerMeterSerialSdm.h b/include/PowerMeterSerialSdm.h new file mode 100644 index 00000000..7e01c8f7 --- /dev/null +++ b/include/PowerMeterSerialSdm.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include "PowerMeterProvider.h" +#include "SDM.h" + +class PowerMeterSerialSdm : public PowerMeterProvider { +public: + bool init() final; + void deinit() final; + void loop() final; + float getPowerTotal() const final; + void doMqttPublish() const final; + +private: + uint32_t _lastPoll; + + float _phase1Power = 0.0; + float _phase2Power = 0.0; + float _phase3Power = 0.0; + float _phase1Voltage = 0.0; + float _phase2Voltage = 0.0; + float _phase3Voltage = 0.0; + float _energyImport = 0.0; + float _energyExport = 0.0; + + mutable std::mutex _mutex; + + static char constexpr _sdmSerialPortOwner[] = "SDM power meter"; + std::unique_ptr _upSdmSerial = nullptr; + std::unique_ptr _upSdm = nullptr; +}; diff --git a/include/PowerMeterSerialSml.h b/include/PowerMeterSerialSml.h new file mode 100644 index 00000000..31a90484 --- /dev/null +++ b/include/PowerMeterSerialSml.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "PowerMeterProvider.h" +#include "Configuration.h" +#include "sml.h" +#include +#include +#include + +class PowerMeterSerialSml : public PowerMeterProvider { +public: + bool init() final; + void deinit() final; + void loop() final; + float getPowerTotal() const final { return _activePower; } + void doMqttPublish() const final; + +private: + float _activePower = 0.0; + float _energyImport = 0.0; + float _energyExport = 0.0; + + mutable std::mutex _mutex; + + std::unique_ptr _upSmlSerial = nullptr; + + typedef struct { + const unsigned char OBIS[6]; + void (*Fn)(double&); + float* Arg; + } OBISHandler; + + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower}, + {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyImport}, + {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyExport} + }; +}; diff --git a/include/SMA_HM.h b/include/PowerMeterUdpSmaHomeManager.h similarity index 53% rename from include/SMA_HM.h rename to include/PowerMeterUdpSmaHomeManager.h index e5600902..34e47f4d 100644 --- a/include/SMA_HM.h +++ b/include/PowerMeterUdpSmaHomeManager.h @@ -5,17 +5,15 @@ #pragma once #include -#include +#include "PowerMeterProvider.h" -class SMA_HMClass { +class PowerMeterUdpSmaHomeManager : public PowerMeterProvider { public: - void init(Scheduler& scheduler, bool verboseLogging); - void loop(); - void event1(); - float getPowerTotal() const { return _powerMeterPower; } - float getPowerL1() const { return _powerMeterL1; } - float getPowerL2() const { return _powerMeterL2; } - float getPowerL3() const { return _powerMeterL3; } + bool init() final; + void deinit() final; + void loop() final; + float getPowerTotal() const final { return _powerMeterPower; } + void doMqttPublish() const final; private: void Soutput(int kanal, int index, int art, int tarif, @@ -23,14 +21,10 @@ private: uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen); - bool _verboseLogging = false; float _powerMeterPower = 0.0; float _powerMeterL1 = 0.0; float _powerMeterL2 = 0.0; float _powerMeterL3 = 0.0; uint32_t _previousMillis = 0; uint32_t _serial = 0; - Task _loopTask; }; - -extern SMA_HMClass SMA_HM; diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 1b08aff8..540554f2 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -294,7 +294,7 @@ void DisplayGraphicClass::loop() _display->drawBox(0, y, _display->getDisplayWidth(), lineHeight); _display->setDrawColor(1); - auto acPower = PowerMeter.getPowerTotal(false); + auto acPower = PowerMeter.getPowerTotal(); if (acPower > 999) { snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000)); } else { diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index 5f378602..aef57a19 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -371,12 +371,12 @@ void HuaweiCanClass::loop() } } - if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && + if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis && _autoPowerEnabledCounter > 0) { // We have received a new PowerMeter value. Also we're _autoPowerEnabled // So we're good to calculate a new limit - _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate(); + _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastUpdate(); // Calculate new power limit float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index d9e28048..5851f327 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -201,7 +201,7 @@ void PowerLimiterClass::loop() // arrives. this can be the case for readings provided by networked meter // readers, where a packet needs to travel through the network for some // time after the actual measurement was done by the reader. - if (PowerMeter.isDataValid() && PowerMeter.getLastPowerMeterUpdate() <= (*_oInverterStatsMillis + 2000)) { + if (PowerMeter.isDataValid() && PowerMeter.getLastUpdate() <= (*_oInverterStatsMillis + 2000)) { return announceStatus(Status::PowerMeterPending); } diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index e1e2d432..8212529b 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -1,18 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ #include "PowerMeter.h" #include "Configuration.h" -#include "PinMapping.h" -#include "HttpPowerMeter.h" -#include "TibberPowerMeter.h" -#include "MqttSettings.h" -#include "NetworkSettings.h" -#include "MessageOutput.h" -#include "SerialPortManager.h" -#include -#include +#include "PowerMeterHttpJson.h" +#include "PowerMeterHttpSml.h" +#include "PowerMeterMqtt.h" +#include "PowerMeterSerialSdm.h" +#include "PowerMeterSerialSml.h" +#include "PowerMeterUdpSmaHomeManager.h" PowerMeterClass PowerMeter; @@ -23,285 +17,74 @@ void PowerMeterClass::init(Scheduler& scheduler) _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); - _lastPowerMeterCheck = 0; - _lastPowerMeterUpdate = 0; - - for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); } - _mqttSubscriptions.clear(); - - CONFIG_T& config = Configuration.get(); - - if (!config.PowerMeter.Enabled) { - return; - } - - const PinMapping_t& pin = PinMapping.get(); - MessageOutput.printf("[PowerMeter] rx = %d, tx = %d, dere = %d\r\n", - pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); - - switch(static_cast(config.PowerMeter.Source)) { - case Source::MQTT: { - auto subscribe = [this](char const* topic, float* target) { - if (strlen(topic) == 0) { return; } - MqttSettings.subscribe(topic, 0, - std::bind(&PowerMeterClass::onMqttMessage, - this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6) - ); - _mqttSubscriptions.try_emplace(topic, target); - }; - - subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerMeter1Power); - subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerMeter2Power); - subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerMeter3Power); - break; - } - - case Source::SDM1PH: - case Source::SDM3PH: { - if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { - MessageOutput.println("[PowerMeter] invalid pin config for SDM power meter (RX and TX pins must be defined)"); - return; - } - - auto oHwSerialPort = SerialPortManager.allocatePort(_sdmSerialPortOwner); - if (!oHwSerialPort) { return; } - - _upSdmSerial = std::make_unique(*oHwSerialPort); - _upSdmSerial->end(); // make sure the UART will be re-initialized - _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, - SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); - _upSdm->begin(); - break; - } - - case Source::HTTP: - HttpPowerMeter.init(); - break; - - case Source::SML: - if (pin.powermeter_rx < 0) { - MessageOutput.println("[PowerMeter] invalid pin config for SML power meter (RX pin must be defined)"); - return; - } - - pinMode(pin.powermeter_rx, INPUT); - _upSmlSerial = std::make_unique(); - _upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95); - _upSmlSerial->enableRx(true); - _upSmlSerial->enableTx(false); - _upSmlSerial->flush(); - break; - - case Source::SMAHM2: - SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging); - break; - - case Source::TIBBER: - break; - } + updateSettings(); } -void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) -{ - for (auto const& subscription: _mqttSubscriptions) { - if (subscription.first != topic) { continue; } - - std::string value(reinterpret_cast(payload), len); - try { - *subscription.second = std::stof(value); - } - catch(std::invalid_argument const& e) { - MessageOutput.printf("PowerMeterClass: cannot parse payload of topic '%s' as float: %s\r\n", - topic, value.c_str()); - return; - } - - if (_verboseLogging) { - MessageOutput.printf("PowerMeterClass: Updated from '%s', TotalPower: %5.2f\r\n", - topic, getPowerTotal()); - } - - _lastPowerMeterUpdate = millis(); - } -} - -float PowerMeterClass::getPowerTotal(bool forceUpdate) -{ - if (forceUpdate) { - CONFIG_T& config = Configuration.get(); - if (config.PowerMeter.Enabled - && (millis() - _lastPowerMeterUpdate) > (1000)) { - readPowerMeter(); - } - } - - std::lock_guard l(_mutex); - return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; -} - -uint32_t PowerMeterClass::getLastPowerMeterUpdate() +void PowerMeterClass::updateSettings() { std::lock_guard l(_mutex); - return _lastPowerMeterUpdate; -} -bool PowerMeterClass::isDataValid() -{ + if (_upProvider) { + _upProvider->deinit(); + _upProvider = nullptr; + } + auto const& config = Configuration.get(); - std::lock_guard l(_mutex); + if (!config.PowerMeter.Enabled) { return; } - bool valid = config.PowerMeter.Enabled && - _lastPowerMeterUpdate > 0 && - ((millis() - _lastPowerMeterUpdate) < (30 * 1000)); + switch(static_cast(config.PowerMeter.Source)) { + case PowerMeterProvider::Type::MQTT: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::SDM1PH: + case PowerMeterProvider::Type::SDM3PH: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::HTTP: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::SML: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::SMAHM2: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::TIBBER: + _upProvider = std::make_unique(); + break; + } - // reset if timed out to avoid glitch once - // (millis() - _lastPowerMeterUpdate) overflows - if (!valid) { _lastPowerMeterUpdate = 0; } - - return valid; + if (!_upProvider->init()) { + _upProvider = nullptr; + } } -void PowerMeterClass::mqtt() +float PowerMeterClass::getPowerTotal() const { - if (!MqttSettings.getConnected()) { return; } - - String topic = "powermeter"; - auto totalPower = getPowerTotal(); - std::lock_guard l(_mutex); - MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); - MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); - MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); - MqttSettings.publish(topic + "/powertotal", String(totalPower)); - MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); - MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); - MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); - MqttSettings.publish(topic + "/import", String(_powerMeterImport)); - MqttSettings.publish(topic + "/export", String(_powerMeterExport)); + if (!_upProvider) { return 0.0; } + return _upProvider->getPowerTotal(); +} + +uint32_t PowerMeterClass::getLastUpdate() const +{ + std::lock_guard l(_mutex); + if (!_upProvider) { return 0; } + return _upProvider->getLastUpdate(); +} + +bool PowerMeterClass::isDataValid() const +{ + std::lock_guard l(_mutex); + if (!_upProvider) { return false; } + return _upProvider->isDataValid(); } void PowerMeterClass::loop() { - CONFIG_T const& config = Configuration.get(); - _verboseLogging = config.PowerMeter.VerboseLogging; - - if (!config.PowerMeter.Enabled) { return; } - - if (static_cast(config.PowerMeter.Source) == Source::SML && - nullptr != _upSmlSerial) { - if (!smlReadLoop()) { return; } - _lastPowerMeterUpdate = millis(); - } - - if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) { - return; - } - - readPowerMeter(); - - MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\r\n", getPowerTotal()); - - mqtt(); - - _lastPowerMeterCheck = millis(); -} - -void PowerMeterClass::readPowerMeter() -{ - CONFIG_T& config = Configuration.get(); - - uint8_t _address = config.PowerMeter.SdmAddress; - Source configuredSource = static_cast(config.PowerMeter.Source); - - if (configuredSource == Source::SDM1PH) { - if (!_upSdm) { return; } - - // this takes a "very long" time as each readVal() is a synchronous - // exchange of serial messages. cache the values and write later. - auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); - auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); - auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); - auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); - - std::lock_guard l(_mutex); - _powerMeter1Power = static_cast(phase1Power); - _powerMeter2Power = 0; - _powerMeter3Power = 0; - _powerMeter1Voltage = static_cast(phase1Voltage); - _powerMeter2Voltage = 0; - _powerMeter3Voltage = 0; - _powerMeterImport = static_cast(energyImport); - _powerMeterExport = static_cast(energyExport); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::SDM3PH) { - if (!_upSdm) { return; } - - // this takes a "very long" time as each readVal() is a synchronous - // exchange of serial messages. cache the values and write later. - auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); - auto phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, _address); - auto phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, _address); - auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); - auto phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, _address); - auto phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, _address); - auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); - auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); - - std::lock_guard l(_mutex); - _powerMeter1Power = static_cast(phase1Power); - _powerMeter2Power = static_cast(phase2Power); - _powerMeter3Power = static_cast(phase3Power); - _powerMeter1Voltage = static_cast(phase1Voltage); - _powerMeter2Voltage = static_cast(phase2Voltage); - _powerMeter3Voltage = static_cast(phase3Voltage); - _powerMeterImport = static_cast(energyImport); - _powerMeterExport = static_cast(energyExport); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::HTTP) { - if (HttpPowerMeter.updateValues()) { - std::lock_guard l(_mutex); - _powerMeter1Power = HttpPowerMeter.getPower(1); - _powerMeter2Power = HttpPowerMeter.getPower(2); - _powerMeter3Power = HttpPowerMeter.getPower(3); - _lastPowerMeterUpdate = millis(); - } - } - else if (configuredSource == Source::SMAHM2) { - std::lock_guard l(_mutex); - _powerMeter1Power = SMA_HM.getPowerL1(); - _powerMeter2Power = SMA_HM.getPowerL2(); - _powerMeter3Power = SMA_HM.getPowerL3(); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::TIBBER) { - if (TibberPowerMeter.updateValues()) { - _lastPowerMeterUpdate = millis(); - } - } -} - -bool PowerMeterClass::smlReadLoop() -{ - while (_upSmlSerial->available()) { - double readVal = 0; - unsigned char smlCurrentChar = _upSmlSerial->read(); - sml_states_t smlCurrentState = smlState(smlCurrentChar); - if (smlCurrentState == SML_LISTEND) { - for (auto& handler: smlHandlerList) { - if (smlOBISCheck(handler.OBIS)) { - handler.Fn(readVal); - *handler.Arg = readVal; - } - } - } else if (smlCurrentState == SML_FINAL) { - return true; - } - } - - return false; + std::lock_guard lock(_mutex); + if (!_upProvider) { return; } + _upProvider->loop(); + _upProvider->mqttLoop(); } diff --git a/src/HttpPowerMeter.cpp b/src/PowerMeterHttpJson.cpp similarity index 84% rename from src/HttpPowerMeter.cpp rename to src/PowerMeterHttpJson.cpp index e3033cbb..bf13ca25 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "Configuration.h" -#include "HttpPowerMeter.h" +#include "PowerMeterHttpJson.h" #include "MessageOutput.h" +#include "MqttSettings.h" #include #include #include "mbedtls/sha256.h" @@ -9,48 +10,63 @@ #include #include -void HttpPowerMeterClass::init() -{ -} - -float HttpPowerMeterClass::getPower(int8_t phase) -{ - if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; } - - return power[phase - 1]; -} - -bool HttpPowerMeterClass::updateValues() +void PowerMeterHttpJson::loop() { auto const& config = Configuration.get(); + if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + return; + } + + _lastPoll = millis(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { auto const& phaseConfig = config.PowerMeter.Http_Phase[i]; if (!phaseConfig.Enabled) { - power[i] = 0.0; + _cache[i] = 0.0; continue; } if (i == 0 || config.PowerMeter.HttpIndividualRequests) { if (!queryPhase(i, phaseConfig)) { - MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1); + MessageOutput.printf("[PowerMeterHttpJson] Getting HTTP response for phase %d failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); - return false; + return; } continue; } if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) { - MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); + MessageOutput.printf("[PowerMeterHttpJson] Reading power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); - return false; + return; } } - return true; + + gotUpdate(); + + _powerValues = _cache; } -bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config) +float PowerMeterHttpJson::getPowerTotal() const +{ + float sum = 0.0; + for (auto v: _powerValues) { sum += v; } + return sum; +} + +void PowerMeterHttpJson::doMqttPublish() const +{ + String topic = "powermeter"; + auto power = getPowerTotal(); + + MqttSettings.publish(topic + "/power1", String(_powerValues[0])); + MqttSettings.publish(topic + "/power2", String(_powerValues[1])); + MqttSettings.publish(topic + "/power3", String(_powerValues[2])); + MqttSettings.publish(topic + "/powertotal", String(power)); +} + +bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -107,7 +123,7 @@ bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& conf return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, config); } -bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) +bool PowerMeterHttpJson::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) { if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); @@ -163,13 +179,13 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit, config.SignInverted); } -String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) { +String PowerMeterHttpJson::extractParam(String& authReq, const String& param, const char delimit) { int _begin = authReq.indexOf(param); if (_begin == -1) { return ""; } return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length())); } -String HttpPowerMeterClass::getcNonce(const int len) { +String PowerMeterHttpJson::getcNonce(const int len) { static const char alphanum[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"; @@ -180,7 +196,7 @@ String HttpPowerMeterClass::getcNonce(const int len) { return s; } -String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) { +String PowerMeterHttpJson::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) { // extracting required parameters for RFC 2617 Digest String realm = extractParam(authReq, "realm=\"", '"'); String nonce = extractParam(authReq, "nonce=\"", '"'); @@ -218,13 +234,13 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam return authorization; } -bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted) +bool PowerMeterHttpJson::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted) { JsonDocument root; const DeserializationError error = deserializeJson(root, httpResponse); if (error) { snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Unable to parse server response as JSON")); + PSTR("[PowerMeterHttpJson] Unable to parse server response as JSON")); return false; } @@ -286,32 +302,32 @@ bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, U if (!value.is()) { snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] not a float: '%s'"), + PSTR("[PowerMeterHttpJson] not a float: '%s'"), value.as().c_str()); return false; } // this value is supposed to be in Watts and positive if energy is consumed. - power[phase] = value.as(); + _cache[phase] = value.as(); switch (unit) { case Unit_t::MilliWatts: - power[phase] /= 1000; + _cache[phase] /= 1000; break; case Unit_t::KiloWatts: - power[phase] *= 1000; + _cache[phase] *= 1000; break; default: break; } - if (signInverted) { power[phase] *= -1; } + if (signInverted) { _cache[phase] *= -1; } return true; } //extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 -bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) +bool PowerMeterHttpJson::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) { // check for : (http: or https: int index = url.indexOf(':'); @@ -361,7 +377,7 @@ bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, St return true; } -String HttpPowerMeterClass::sha256(const String& data) { +String PowerMeterHttpJson::sha256(const String& data) { uint8_t hash[32]; mbedtls_sha256_context ctx; @@ -379,7 +395,7 @@ String HttpPowerMeterClass::sha256(const String& data) { return res; } -void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) { +void PowerMeterHttpJson::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) { httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); httpClient.setUserAgent("OpenDTU-OnBattery"); httpClient.setConnectTimeout(timeout); @@ -391,5 +407,3 @@ void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeade httpClient.addHeader(httpHeader, httpValue); } } - -HttpPowerMeterClass HttpPowerMeter; diff --git a/src/TibberPowerMeter.cpp b/src/PowerMeterHttpSml.cpp similarity index 84% rename from src/TibberPowerMeter.cpp rename to src/PowerMeterHttpSml.cpp index d7889c22..e7462fdf 100644 --- a/src/TibberPowerMeter.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -1,28 +1,44 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "Configuration.h" -#include "TibberPowerMeter.h" +#include "PowerMeterHttpSml.h" #include "MessageOutput.h" +#include "MqttSettings.h" #include #include #include -#include -bool TibberPowerMeterClass::updateValues() +float PowerMeterHttpSml::getPowerTotal() const +{ + std::lock_guard l(_mutex); + return _activePower; +} + +void PowerMeterHttpSml::doMqttPublish() const +{ + String topic = "powermeter"; + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/powertotal", String(_activePower)); +} + +void PowerMeterHttpSml::loop() { auto const& config = Configuration.get(); + if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + return; + } + + _lastPoll = millis(); auto const& tibberConfig = config.PowerMeter.Tibber; if (!query(tibberConfig)) { - MessageOutput.printf("[TibberPowerMeter] Getting the power of tibber failed.\r\n"); + MessageOutput.printf("[PowerMeterHttpSml] Getting the power value failed.\r\n"); MessageOutput.printf("%s\r\n", tibberPowerMeterError); - return false; } - - return true; } -bool TibberPowerMeterClass::query(PowerMeterTibberConfig const& config) +bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -79,7 +95,7 @@ bool TibberPowerMeterClass::query(PowerMeterTibberConfig const& config) return httpRequest(*wifiClient, ipaddr.toString(), port, uri, https, config); } -bool TibberPowerMeterClass::httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) +bool PowerMeterHttpSml::httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) { if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); @@ -112,10 +128,12 @@ bool TibberPowerMeterClass::httpRequest(WiFiClient &wifiClient, const String& ho unsigned char smlCurrentChar = httpClient.getStream().read(); sml_states_t smlCurrentState = smlState(smlCurrentChar); if (smlCurrentState == SML_LISTEND) { - for (auto& handler: PowerMeter.smlHandlerList) { + for (auto& handler: smlHandlerList) { if (smlOBISCheck(handler.OBIS)) { + std::lock_guard l(_mutex); handler.Fn(readVal); *handler.Arg = readVal; + gotUpdate(); } } } @@ -126,7 +144,7 @@ bool TibberPowerMeterClass::httpRequest(WiFiClient &wifiClient, const String& ho } //extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 -bool TibberPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) +bool PowerMeterHttpSml::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) { // check for : (http: or https: int index = url.indexOf(':'); @@ -176,7 +194,7 @@ bool TibberPowerMeterClass::extractUrlComponents(String url, String& _protocol, return true; } -void TibberPowerMeterClass::prepareRequest(uint32_t timeout) { +void PowerMeterHttpSml::prepareRequest(uint32_t timeout) { httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); httpClient.setUserAgent("OpenDTU-OnBattery"); httpClient.setConnectTimeout(timeout); @@ -184,5 +202,3 @@ void TibberPowerMeterClass::prepareRequest(uint32_t timeout) { httpClient.addHeader("Content-Type", "application/json"); httpClient.addHeader("Accept", "application/json"); } - -TibberPowerMeterClass TibberPowerMeter; diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp new file mode 100644 index 00000000..9a470ccf --- /dev/null +++ b/src/PowerMeterMqtt.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterMqtt.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "MessageOutput.h" + +bool PowerMeterMqtt::init() +{ + auto subscribe = [this](char const* topic, float* target) { + if (strlen(topic) == 0) { return; } + MqttSettings.subscribe(topic, 0, + std::bind(&PowerMeterMqtt::onMqttMessage, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6) + ); + _mqttSubscriptions.try_emplace(topic, target); + }; + + auto const& config = Configuration.get(); + subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerValueOne); + subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerValueTwo); + subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerValueThree); + + return _mqttSubscriptions.size() > 0; +} + +void PowerMeterMqtt::deinit() +{ + for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); } + _mqttSubscriptions.clear(); +} + +void PowerMeterMqtt::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +{ + for (auto const& subscription: _mqttSubscriptions) { + if (subscription.first != topic) { continue; } + + std::string value(reinterpret_cast(payload), len); + try { + *subscription.second = std::stof(value); + } + catch(std::invalid_argument const& e) { + MessageOutput.printf("[PowerMeterMqtt] cannot parse payload of topic '%s' as float: %s\r\n", + topic, value.c_str()); + return; + } + + if (_verboseLogging) { + MessageOutput.printf("[PowerMeterMqtt] Updated from '%s', TotalPower: %5.2f\r\n", + topic, getPowerTotal()); + } + + gotUpdate(); + } +} + +float PowerMeterMqtt::getPowerTotal() const +{ + std::lock_guard l(_mutex); + return _powerValueOne + _powerValueTwo + _powerValueThree; +} + +void PowerMeterMqtt::doMqttPublish() const +{ + String topic = "powermeter"; + auto totalPower = getPowerTotal(); + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/power1", String(_powerValueOne)); + MqttSettings.publish(topic + "/power2", String(_powerValueTwo)); + MqttSettings.publish(topic + "/power3", String(_powerValueThree)); + MqttSettings.publish(topic + "/powertotal", String(totalPower)); +} diff --git a/src/PowerMeterProvider.cpp b/src/PowerMeterProvider.cpp new file mode 100644 index 00000000..8becdebd --- /dev/null +++ b/src/PowerMeterProvider.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterProvider.h" +#include "MqttSettings.h" + +bool PowerMeterProvider::isDataValid() const +{ + return _lastUpdate > 0 && ((millis() - _lastUpdate) < (30 * 1000)); +} + +void PowerMeterProvider::mqttLoop() const +{ + if (!MqttSettings.getConnected()) { return; } + + if (!isDataValid()) { return; } + + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; + if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; } + + doMqttPublish(); + + _lastMqttPublish = millis(); +} diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp new file mode 100644 index 00000000..d08cac19 --- /dev/null +++ b/src/PowerMeterSerialSdm.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSerialSdm.h" +#include "Configuration.h" +#include "PinMapping.h" +#include "MessageOutput.h" +#include "MqttSettings.h" +#include "SerialPortManager.h" + +void PowerMeterSerialSdm::deinit() +{ + if (_upSdmSerial) { + _upSdmSerial->end(); + _upSdmSerial = nullptr; + } +} + +bool PowerMeterSerialSdm::init() +{ + const PinMapping_t& pin = PinMapping.get(); + + MessageOutput.printf("[PowerMeterSerialSdm] rx = %d, tx = %d, dere = %d\r\n", + pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); + + if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { + MessageOutput.println("[PowerMeterSerialSdm] invalid pin config for SDM " + "power meter (RX and TX pins must be defined)"); + return false; + } + + auto oHwSerialPort = SerialPortManager.allocatePort(_sdmSerialPortOwner); + if (!oHwSerialPort) { return false; } + + _upSdmSerial = std::make_unique(*oHwSerialPort); + _upSdmSerial->end(); // make sure the UART will be re-initialized + _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, + SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); + _upSdm->begin(); + + return true; +} + +float PowerMeterSerialSdm::getPowerTotal() const +{ + std::lock_guard l(_mutex); + return _phase1Power + _phase2Power + _phase3Power; +} + +void PowerMeterSerialSdm::doMqttPublish() const +{ + String topic = "powermeter"; + auto power = getPowerTotal(); + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/power1", String(_phase1Power)); + MqttSettings.publish(topic + "/power2", String(_phase2Power)); + MqttSettings.publish(topic + "/power3", String(_phase3Power)); + MqttSettings.publish(topic + "/powertotal", String(power)); + MqttSettings.publish(topic + "/voltage1", String(_phase1Voltage)); + MqttSettings.publish(topic + "/voltage2", String(_phase2Voltage)); + MqttSettings.publish(topic + "/voltage3", String(_phase3Voltage)); + MqttSettings.publish(topic + "/import", String(_energyImport)); + MqttSettings.publish(topic + "/export", String(_energyExport)); +} + +void PowerMeterSerialSdm::loop() +{ + if (!_upSdm) { return; } + + auto const& config = Configuration.get(); + + if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + return; + } + + uint8_t addr = config.PowerMeter.SdmAddress; + + // reading takes a "very long" time as each readVal() is a synchronous + // exchange of serial messages. cache the values and write later to + // enforce consistent values. + float phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, addr); + float phase2Power = 0.0; + float phase3Power = 0.0; + float phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, addr); + float phase2Voltage = 0.0; + float phase3Voltage = 0.0; + float energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, addr); + float energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, addr); + + if (static_cast(config.PowerMeter.Source) == PowerMeterProvider::Type::SDM3PH) { + phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, addr); + phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, addr); + phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, addr); + phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, addr); + } + + { + std::lock_guard l(_mutex); + _phase1Power = static_cast(phase1Power); + _phase2Power = static_cast(phase2Power); + _phase3Power = static_cast(phase3Power); + _phase1Voltage = static_cast(phase1Voltage); + _phase2Voltage = static_cast(phase2Voltage); + _phase3Voltage = static_cast(phase3Voltage); + _energyImport = static_cast(energyImport); + _energyExport = static_cast(energyExport); + } + + gotUpdate(); + + MessageOutput.printf("[PowerMeterSerialSdm] TotalPower: %5.2f\r\n", getPowerTotal()); + + _lastPoll = millis(); +} diff --git a/src/PowerMeterSerialSml.cpp b/src/PowerMeterSerialSml.cpp new file mode 100644 index 00000000..04792514 --- /dev/null +++ b/src/PowerMeterSerialSml.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSerialSml.h" +#include "Configuration.h" +#include "PinMapping.h" +#include "MessageOutput.h" +#include "MqttSettings.h" + +bool PowerMeterSerialSml::init() +{ + const PinMapping_t& pin = PinMapping.get(); + + MessageOutput.printf("[PowerMeterSerialSml] rx = %d\r\n", pin.powermeter_rx); + + if (pin.powermeter_rx < 0) { + MessageOutput.println("[PowerMeterSerialSml] invalid pin config " + "for serial SML power meter (RX pin must be defined)"); + return false; + } + + pinMode(pin.powermeter_rx, INPUT); + _upSmlSerial = std::make_unique(); + _upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95); + _upSmlSerial->enableRx(true); + _upSmlSerial->enableTx(false); + _upSmlSerial->flush(); + + return true; +} + +void PowerMeterSerialSml::deinit() +{ + if (!_upSmlSerial) { return; } + _upSmlSerial->end(); +} + +void PowerMeterSerialSml::doMqttPublish() const +{ + String topic = "powermeter"; + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/powertotal", String(_activePower)); + MqttSettings.publish(topic + "/import", String(_energyImport)); + MqttSettings.publish(topic + "/export", String(_energyExport)); +} + +void PowerMeterSerialSml::loop() +{ + if (!_upSmlSerial) { return; } + + while (_upSmlSerial->available()) { + double readVal = 0; + unsigned char smlCurrentChar = _upSmlSerial->read(); + sml_states_t smlCurrentState = smlState(smlCurrentChar); + if (smlCurrentState == SML_LISTEND) { + for (auto& handler: smlHandlerList) { + if (smlOBISCheck(handler.OBIS)) { + handler.Fn(readVal); + std::lock_guard l(_mutex); + *handler.Arg = readVal; + } + } + } else if (smlCurrentState == SML_FINAL) { + gotUpdate(); + } + } + + MessageOutput.printf("[PowerMeterSerialSml]: TotalPower: %5.2f\r\n", getPowerTotal()); +} diff --git a/src/SMA_HM.cpp b/src/PowerMeterUdpSmaHomeManager.cpp similarity index 76% rename from src/SMA_HM.cpp rename to src/PowerMeterUdpSmaHomeManager.cpp index 7a3a9fe2..b79ae3d9 100644 --- a/src/SMA_HM.cpp +++ b/src/PowerMeterUdpSmaHomeManager.cpp @@ -2,51 +2,50 @@ /* * Copyright (C) 2024 Holger-Steffen Stapf */ -#include "SMA_HM.h" +#include "PowerMeterUdpSmaHomeManager.h" #include -#include "Configuration.h" -#include "NetworkSettings.h" +#include "MqttSettings.h" #include #include "MessageOutput.h" -unsigned int multicastPort = 9522; // local port to listen on -IPAddress multicastIP(239, 12, 255, 254); -WiFiUDP SMAUdp; +static constexpr unsigned int multicastPort = 9522; // local port to listen on +static const IPAddress multicastIP(239, 12, 255, 254); +static WiFiUDP SMAUdp; constexpr uint32_t interval = 1000; -SMA_HMClass SMA_HM; - -void SMA_HMClass::Soutput(int kanal, int index, int art, int tarif, +void PowerMeterUdpSmaHomeManager::Soutput(int kanal, int index, int art, int tarif, char const* name, float value, uint32_t timestamp) { if (!_verboseLogging) { return; } - MessageOutput.printf("SMA_HM: %s = %.1f (timestamp %d)\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] %s = %.1f (timestamp %d)\r\n", name, value, timestamp); } -void SMA_HMClass::init(Scheduler& scheduler, bool verboseLogging) +bool PowerMeterUdpSmaHomeManager::init() { - _verboseLogging = verboseLogging; - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&SMA_HMClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); SMAUdp.begin(multicastPort); SMAUdp.beginMulticast(multicastIP, multicastPort); + return true; } -void SMA_HMClass::loop() +void PowerMeterUdpSmaHomeManager::deinit() { - uint32_t currentMillis = millis(); - if (currentMillis - _previousMillis >= interval) { - _previousMillis = currentMillis; - event1(); - } + SMAUdp.stop(); } -uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) +void PowerMeterUdpSmaHomeManager::doMqttPublish() const +{ + String topic = "powermeter"; + + MqttSettings.publish(topic + "/powertotal", String(_powerMeterPower)); + MqttSettings.publish(topic + "/power1", String(_powerMeterL1)); + MqttSettings.publish(topic + "/power2", String(_powerMeterL2)); + MqttSettings.publish(topic + "/power3", String(_powerMeterL3)); +} + +uint8_t* PowerMeterUdpSmaHomeManager::decodeGroup(uint8_t* offset, uint16_t grouplen) { float Pbezug = 0; float BezugL1 = 0; @@ -149,7 +148,7 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) continue; } - MessageOutput.printf("SMA_HM: Skipped unknown measurement: %d %d %d %d\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] Skipped unknown measurement: %d %d %d %d\r\n", kanal, index, art, tarif); offset += art; } @@ -157,15 +156,20 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) return offset; } -void SMA_HMClass::event1() +void PowerMeterUdpSmaHomeManager::loop() { + uint32_t currentMillis = millis(); + if (currentMillis - _previousMillis < interval) { return; } + + _previousMillis = currentMillis; + int packetSize = SMAUdp.parsePacket(); if (!packetSize) { return; } uint8_t buffer[1024]; int rSize = SMAUdp.read(buffer, 1024); if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') { - MessageOutput.println("SMA_HM: Not an SMA packet?"); + MessageOutput.println("[PowerMeterUdpSmaHomeManager] Not an SMA packet?"); return; } @@ -196,7 +200,7 @@ void SMA_HMClass::event1() continue; } - MessageOutput.printf("SMA_HM: Unhandled group 0x%04x with length %d\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] Unhandled group 0x%04x with length %d\r\n", grouptag, grouplen); offset += grouplen; } while (grouplen > 0 && offset + 4 < buffer + rSize); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 74472bc0..c6cf373f 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -12,8 +12,8 @@ #include "MqttSettings.h" #include "PowerLimiter.h" #include "PowerMeter.h" -#include "HttpPowerMeter.h" -#include "TibberPowerMeter.h" +#include "PowerMeterHttpJson.h" +#include "PowerMeterHttpSml.h" #include "WebApi.h" #include "helper.h" @@ -128,7 +128,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if (static_cast(root["source"].as()) == PowerMeterClass::Source::HTTP) { + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP) { JsonArray http_phases = root["http_phases"]; for (uint8_t i = 0; i < http_phases.size(); i++) { JsonObject phase = http_phases[i].as(); @@ -174,7 +174,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } } - if (static_cast(root["source"].as()) == PowerMeterClass::Source::TIBBER) { + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::TIBBER) { JsonObject tibber = root["tibber"]; if (!tibber.containsKey("url") @@ -260,14 +260,14 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) char response[256]; - int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result PowerMeterHttpConfig phaseConfig; decodeJsonPhaseConfig(root.as(), phaseConfig); - if (HttpPowerMeter.queryPhase(phase, phaseConfig)) { + auto upMeter = std::make_unique(); + if (upMeter->queryPhase(0/*phase*/, phaseConfig)) { retMsg["type"] = "success"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1)); + snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal()); } else { - snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError); + snprintf_P(response, sizeof(response), "%s", upMeter->httpPowerMeterError); } retMsg["message"] = response; @@ -302,11 +302,12 @@ void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request) PowerMeterTibberConfig tibberConfig; decodeJsonTibberConfig(root.as(), tibberConfig); - if (TibberPowerMeter.query(tibberConfig)) { + auto upMeter = std::make_unique(); + if (upMeter->query(tibberConfig)) { retMsg["type"] = "success"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", PowerMeter.getPowerTotal()); + snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal()); } else { - snprintf_P(response, sizeof(response), "%s", TibberPowerMeter.tibberPowerMeterError); + snprintf_P(response, sizeof(response), "%s", upMeter->tibberPowerMeterError); } retMsg["message"] = response; diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index ab54d479..85781bce 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -98,12 +98,12 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al if (!all) { _lastPublishBattery = millis(); } } - if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { + if (all || (PowerMeter.getLastUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { auto powerMeterObj = root["power_meter"].to(); powerMeterObj["enabled"] = config.PowerMeter.Enabled; if (config.PowerMeter.Enabled) { - addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); + addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(), "W", 1); } if (!all) { _lastPublishPowerMeter = millis(); } From 33683d26c88e27456f96320bc2ff8924964558d8 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 8 May 2024 11:15:36 +0200 Subject: [PATCH 03/50] powermeter refactor: rename providers in enum the enum values did not change, but their name (only relevant in the code) are now more expressive. --- include/PowerMeterProvider.h | 6 +++--- src/PowerMeter.cpp | 6 +++--- src/WebApi_powermeter.cpp | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h index 9fb74c78..d3e02f80 100644 --- a/include/PowerMeterProvider.h +++ b/include/PowerMeterProvider.h @@ -11,10 +11,10 @@ public: MQTT = 0, SDM1PH = 1, SDM3PH = 2, - HTTP = 3, - SML = 4, + HTTP_JSON = 3, + SERIAL_SML = 4, SMAHM2 = 5, - TIBBER = 6 + HTTP_SML = 6 }; // returns true if the provider is ready for use, false otherwise diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 8212529b..37c3ab34 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -41,16 +41,16 @@ void PowerMeterClass::updateSettings() case PowerMeterProvider::Type::SDM3PH: _upProvider = std::make_unique(); break; - case PowerMeterProvider::Type::HTTP: + case PowerMeterProvider::Type::HTTP_JSON: _upProvider = std::make_unique(); break; - case PowerMeterProvider::Type::SML: + case PowerMeterProvider::Type::SERIAL_SML: _upProvider = std::make_unique(); break; case PowerMeterProvider::Type::SMAHM2: _upProvider = std::make_unique(); break; - case PowerMeterProvider::Type::TIBBER: + case PowerMeterProvider::Type::HTTP_SML: _upProvider = std::make_unique(); break; } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index c6cf373f..92b56d89 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -128,7 +128,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP) { + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_JSON) { JsonArray http_phases = root["http_phases"]; for (uint8_t i = 0; i < http_phases.size(); i++) { JsonObject phase = http_phases[i].as(); @@ -174,7 +174,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } } - if (static_cast(root["source"].as()) == PowerMeterProvider::Type::TIBBER) { + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_SML) { JsonObject tibber = root["tibber"]; if (!tibber.containsKey("url") From 5cd6334880f9ece2ca060e365862b1cdf2f9f065 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 8 May 2024 13:09:05 +0200 Subject: [PATCH 04/50] powermeter refactor: avoid reboot on settings change the current power meter provider will be de-initialized, and a new instance will be initialized with the new settings. --- src/WebApi_powermeter.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 92b56d89..4a4b13e6 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -226,12 +226,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - - // reboot requiered as per https://github.com/helgeerbe/OpenDTU-OnBattery/issues/565#issuecomment-1872552559 - yield(); - delay(1000); - yield(); - ESP.restart(); + PowerMeter.updateSettings(); } void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) From d4c07836d9ef793418f16b61e19860d2dcbc9b39 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 8 May 2024 13:12:43 +0200 Subject: [PATCH 05/50] MQTT powermeter: avoid iterating subscriptions instead of iterating a map with subscriptions, we now bind the target variable to the callback, which is executed once a message is arrived. this way, the target variable is already linked to the respective topic when the callback is executed. lock the mutex when writing the variable, as the MQTT callback is executed in a different context (MQTT task) than the main loop task, which otherwise accesses the variables. --- include/PowerMeterMqtt.h | 10 ++++---- src/PowerMeterMqtt.cpp | 50 ++++++++++++++++++++-------------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/include/PowerMeterMqtt.h b/include/PowerMeterMqtt.h index 5dd01d2c..3786fca6 100644 --- a/include/PowerMeterMqtt.h +++ b/include/PowerMeterMqtt.h @@ -3,7 +3,7 @@ #include "PowerMeterProvider.h" #include -#include +#include #include class PowerMeterMqtt : public PowerMeterProvider { @@ -15,14 +15,16 @@ public: void doMqttPublish() const final; private: - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + using MsgProperties = espMqttClientTypes::MessageProperties; + void onMessage(MsgProperties const& properties, char const* topic, + uint8_t const* payload, size_t len, size_t index, + size_t total, float* targetVariable); float _powerValueOne = 0; float _powerValueTwo = 0; float _powerValueThree = 0; - std::map _mqttSubscriptions; + std::vector _mqttSubscriptions; mutable std::mutex _mutex; }; diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp index 9a470ccf..1a74f41d 100644 --- a/src/PowerMeterMqtt.cpp +++ b/src/PowerMeterMqtt.cpp @@ -6,15 +6,16 @@ bool PowerMeterMqtt::init() { - auto subscribe = [this](char const* topic, float* target) { + auto subscribe = [this](char const* topic, float* targetVariable) { if (strlen(topic) == 0) { return; } MqttSettings.subscribe(topic, 0, - std::bind(&PowerMeterMqtt::onMqttMessage, + std::bind(&PowerMeterMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6) + std::placeholders::_5, std::placeholders::_6, + targetVariable) ); - _mqttSubscriptions.try_emplace(topic, target); + _mqttSubscriptions.push_back(topic); }; auto const& config = Configuration.get(); @@ -27,32 +28,31 @@ bool PowerMeterMqtt::init() void PowerMeterMqtt::deinit() { - for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); } + for (auto const& t: _mqttSubscriptions) { MqttSettings.unsubscribe(t); } _mqttSubscriptions.clear(); } -void PowerMeterMqtt::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void PowerMeterMqtt::onMessage(PowerMeterMqtt::MsgProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, + size_t total, float* targetVariable) { - for (auto const& subscription: _mqttSubscriptions) { - if (subscription.first != topic) { continue; } - - std::string value(reinterpret_cast(payload), len); - try { - *subscription.second = std::stof(value); - } - catch(std::invalid_argument const& e) { - MessageOutput.printf("[PowerMeterMqtt] cannot parse payload of topic '%s' as float: %s\r\n", - topic, value.c_str()); - return; - } - - if (_verboseLogging) { - MessageOutput.printf("[PowerMeterMqtt] Updated from '%s', TotalPower: %5.2f\r\n", - topic, getPowerTotal()); - } - - gotUpdate(); + std::string value(reinterpret_cast(payload), len); + try { + std::lock_guard l(_mutex); + *targetVariable = std::stof(value); } + catch (std::invalid_argument const& e) { + MessageOutput.printf("[PowerMeterMqtt] cannot parse payload of topic " + "'%s' as float: %s\r\n", topic, value.c_str()); + return; + } + + if (_verboseLogging) { + MessageOutput.printf("[PowerMeterMqtt] Updated from '%s', TotalPower: %5.2f\r\n", + topic, getPowerTotal()); + } + + gotUpdate(); } float PowerMeterMqtt::getPowerTotal() const From 9eb4f1714cff8b3b152c4267d7af05f127ae6ec8 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 8 May 2024 13:19:16 +0200 Subject: [PATCH 06/50] powermeter refactor: make timestamp of last update atomic the timestamp is potentially updated from a different thread, e.g., MQTT task, than the main loop, which typically reads that timestamp. --- include/PowerMeterProvider.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h index d3e02f80..8b9225ab 100644 --- a/include/PowerMeterProvider.h +++ b/include/PowerMeterProvider.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include "Configuration.h" class PowerMeterProvider { @@ -41,6 +42,9 @@ protected: private: virtual void doMqttPublish() const = 0; - uint32_t _lastUpdate = 0; + // gotUpdate() updates this variable potentially from a different thread + // than users that request to read this variable through getLastUpdate(). + std::atomic _lastUpdate = 0; + mutable uint32_t _lastMqttPublish = 0; }; From d99cfd5b31f96f0c97f44e9a41b569404c5e69b0 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 8 May 2024 13:50:58 +0200 Subject: [PATCH 07/50] powermeter refactor: publish values to MQTT in base class "powertotal" is always published and it is published by the base class directly. other values are still published by the derived classes, but use a base class method, which takes care that a common base topic is used in particular. --- include/PowerMeterProvider.h | 2 ++ src/PowerMeterHttpJson.cpp | 11 +++-------- src/PowerMeterHttpSml.cpp | 5 ----- src/PowerMeterMqtt.cpp | 10 +++------- src/PowerMeterProvider.cpp | 7 +++++++ src/PowerMeterSerialSdm.cpp | 21 ++++++++------------- src/PowerMeterSerialSml.cpp | 8 ++------ src/PowerMeterUdpSmaHomeManager.cpp | 10 +++------- 8 files changed, 28 insertions(+), 46 deletions(-) diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h index 8b9225ab..17704135 100644 --- a/include/PowerMeterProvider.h +++ b/include/PowerMeterProvider.h @@ -37,6 +37,8 @@ protected: void gotUpdate() { _lastUpdate = millis(); } + void mqttPublish(String const& topic, float const& value) const; + bool _verboseLogging; private: diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index bf13ca25..c8d95bd3 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -2,7 +2,6 @@ #include "Configuration.h" #include "PowerMeterHttpJson.h" #include "MessageOutput.h" -#include "MqttSettings.h" #include #include #include "mbedtls/sha256.h" @@ -57,13 +56,9 @@ float PowerMeterHttpJson::getPowerTotal() const void PowerMeterHttpJson::doMqttPublish() const { - String topic = "powermeter"; - auto power = getPowerTotal(); - - MqttSettings.publish(topic + "/power1", String(_powerValues[0])); - MqttSettings.publish(topic + "/power2", String(_powerValues[1])); - MqttSettings.publish(topic + "/power3", String(_powerValues[2])); - MqttSettings.publish(topic + "/powertotal", String(power)); + mqttPublish("power1", _powerValues[0]); + mqttPublish("power2", _powerValues[1]); + mqttPublish("power3", _powerValues[2]); } bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& config) diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index e7462fdf..f3d8e0c7 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -2,7 +2,6 @@ #include "Configuration.h" #include "PowerMeterHttpSml.h" #include "MessageOutput.h" -#include "MqttSettings.h" #include #include #include @@ -15,10 +14,6 @@ float PowerMeterHttpSml::getPowerTotal() const void PowerMeterHttpSml::doMqttPublish() const { - String topic = "powermeter"; - - std::lock_guard l(_mutex); - MqttSettings.publish(topic + "/powertotal", String(_activePower)); } void PowerMeterHttpSml::loop() diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp index 1a74f41d..3204bf1f 100644 --- a/src/PowerMeterMqtt.cpp +++ b/src/PowerMeterMqtt.cpp @@ -63,12 +63,8 @@ float PowerMeterMqtt::getPowerTotal() const void PowerMeterMqtt::doMqttPublish() const { - String topic = "powermeter"; - auto totalPower = getPowerTotal(); - std::lock_guard l(_mutex); - MqttSettings.publish(topic + "/power1", String(_powerValueOne)); - MqttSettings.publish(topic + "/power2", String(_powerValueTwo)); - MqttSettings.publish(topic + "/power3", String(_powerValueThree)); - MqttSettings.publish(topic + "/powertotal", String(totalPower)); + mqttPublish("power1", _powerValueOne); + mqttPublish("power2", _powerValueTwo); + mqttPublish("power3", _powerValueThree); } diff --git a/src/PowerMeterProvider.cpp b/src/PowerMeterProvider.cpp index 8becdebd..d1c7d628 100644 --- a/src/PowerMeterProvider.cpp +++ b/src/PowerMeterProvider.cpp @@ -7,6 +7,11 @@ bool PowerMeterProvider::isDataValid() const return _lastUpdate > 0 && ((millis() - _lastUpdate) < (30 * 1000)); } +void PowerMeterProvider::mqttPublish(String const& topic, float const& value) const +{ + MqttSettings.publish("powermeter/" + topic, String(value)); +} + void PowerMeterProvider::mqttLoop() const { if (!MqttSettings.getConnected()) { return; } @@ -16,6 +21,8 @@ void PowerMeterProvider::mqttLoop() const auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; } + mqttPublish("powertotal", getPowerTotal()); + doMqttPublish(); _lastMqttPublish = millis(); diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp index d08cac19..f040c173 100644 --- a/src/PowerMeterSerialSdm.cpp +++ b/src/PowerMeterSerialSdm.cpp @@ -3,7 +3,6 @@ #include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" -#include "MqttSettings.h" #include "SerialPortManager.h" void PowerMeterSerialSdm::deinit() @@ -47,19 +46,15 @@ float PowerMeterSerialSdm::getPowerTotal() const void PowerMeterSerialSdm::doMqttPublish() const { - String topic = "powermeter"; - auto power = getPowerTotal(); - std::lock_guard l(_mutex); - MqttSettings.publish(topic + "/power1", String(_phase1Power)); - MqttSettings.publish(topic + "/power2", String(_phase2Power)); - MqttSettings.publish(topic + "/power3", String(_phase3Power)); - MqttSettings.publish(topic + "/powertotal", String(power)); - MqttSettings.publish(topic + "/voltage1", String(_phase1Voltage)); - MqttSettings.publish(topic + "/voltage2", String(_phase2Voltage)); - MqttSettings.publish(topic + "/voltage3", String(_phase3Voltage)); - MqttSettings.publish(topic + "/import", String(_energyImport)); - MqttSettings.publish(topic + "/export", String(_energyExport)); + mqttPublish("power1", _phase1Power); + mqttPublish("power2", _phase2Power); + mqttPublish("power3", _phase3Power); + mqttPublish("voltage1", _phase1Voltage); + mqttPublish("voltage2", _phase2Voltage); + mqttPublish("voltage3", _phase3Voltage); + mqttPublish("import", _energyImport); + mqttPublish("export", _energyExport); } void PowerMeterSerialSdm::loop() diff --git a/src/PowerMeterSerialSml.cpp b/src/PowerMeterSerialSml.cpp index 04792514..9236dcd7 100644 --- a/src/PowerMeterSerialSml.cpp +++ b/src/PowerMeterSerialSml.cpp @@ -3,7 +3,6 @@ #include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" -#include "MqttSettings.h" bool PowerMeterSerialSml::init() { @@ -35,12 +34,9 @@ void PowerMeterSerialSml::deinit() void PowerMeterSerialSml::doMqttPublish() const { - String topic = "powermeter"; - std::lock_guard l(_mutex); - MqttSettings.publish(topic + "/powertotal", String(_activePower)); - MqttSettings.publish(topic + "/import", String(_energyImport)); - MqttSettings.publish(topic + "/export", String(_energyExport)); + mqttPublish("import", _energyImport); + mqttPublish("export", _energyExport); } void PowerMeterSerialSml::loop() diff --git a/src/PowerMeterUdpSmaHomeManager.cpp b/src/PowerMeterUdpSmaHomeManager.cpp index b79ae3d9..1347bfb4 100644 --- a/src/PowerMeterUdpSmaHomeManager.cpp +++ b/src/PowerMeterUdpSmaHomeManager.cpp @@ -4,7 +4,6 @@ */ #include "PowerMeterUdpSmaHomeManager.h" #include -#include "MqttSettings.h" #include #include "MessageOutput.h" @@ -37,12 +36,9 @@ void PowerMeterUdpSmaHomeManager::deinit() void PowerMeterUdpSmaHomeManager::doMqttPublish() const { - String topic = "powermeter"; - - MqttSettings.publish(topic + "/powertotal", String(_powerMeterPower)); - MqttSettings.publish(topic + "/power1", String(_powerMeterL1)); - MqttSettings.publish(topic + "/power2", String(_powerMeterL2)); - MqttSettings.publish(topic + "/power3", String(_powerMeterL3)); + mqttPublish("power1", _powerMeterL1); + mqttPublish("power2", _powerMeterL2); + mqttPublish("power3", _powerMeterL3); } uint8_t* PowerMeterUdpSmaHomeManager::decodeGroup(uint8_t* offset, uint16_t grouplen) From 54c04aed6139626f5de896020cc927e2b46acd96 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 9 May 2024 14:28:14 +0200 Subject: [PATCH 08/50] SDM power meter: remove baud rate setting this setting was not used. the baud rate for the SDM is set to 9600 in the source code. until the baud rate being customizable is actually required by somebody, we remove the setting altogether. --- include/Configuration.h | 1 - include/defaults.h | 1 - src/Configuration.cpp | 2 -- src/WebApi_powermeter.cpp | 2 -- webapp/src/locales/de.json | 1 - webapp/src/locales/en.json | 1 - webapp/src/types/PowerMeterConfig.ts | 1 - webapp/src/views/PowerMeterAdminView.vue | 10 ---------- 8 files changed, 19 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index b0e28621..3c0c41a1 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -203,7 +203,6 @@ struct CONFIG_T { char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; - uint32_t SdmBaudrate; uint32_t SdmAddress; uint32_t HttpInterval; bool HttpIndividualRequests; diff --git a/include/defaults.h b/include/defaults.h index 32d1e54c..865e595c 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -117,7 +117,6 @@ #define POWERMETER_ENABLED false #define POWERMETER_INTERVAL 10 #define POWERMETER_SOURCE 2 -#define POWERMETER_SDMBAUDRATE 9600 #define POWERMETER_SDMADDRESS 1 #define POWERLIMITER_ENABLED false diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 0749a1a2..e899de9b 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -155,7 +155,6 @@ bool ConfigurationClass::write() powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1; powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2; powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3; - powermeter["sdmbaudrate"] = config.PowerMeter.SdmBaudrate; powermeter["sdmaddress"] = config.PowerMeter.SdmAddress; powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; @@ -422,7 +421,6 @@ bool ConfigurationClass::read() strlcpy(config.PowerMeter.MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter1)); strlcpy(config.PowerMeter.MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter2)); strlcpy(config.PowerMeter.MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter3)); - config.PowerMeter.SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE; config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false; diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 4a4b13e6..0e519dbc 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -66,7 +66,6 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) root["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1; root["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2; root["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3; - root["sdmbaudrate"] = config.PowerMeter.SdmBaudrate; root["sdmaddress"] = config.PowerMeter.SdmAddress; root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; @@ -210,7 +209,6 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) strlcpy(config.PowerMeter.MqttTopicPowerMeter1, root["mqtt_topic_powermeter_1"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter1)); strlcpy(config.PowerMeter.MqttTopicPowerMeter2, root["mqtt_topic_powermeter_2"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter2)); strlcpy(config.PowerMeter.MqttTopicPowerMeter3, root["mqtt_topic_powermeter_3"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter3)); - config.PowerMeter.SdmBaudrate = root["sdmbaudrate"].as(); config.PowerMeter.SdmAddress = root["sdmaddress"].as(); config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index db78e137..c7021b99 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -568,7 +568,6 @@ "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", "SDM": "SDM-Stromzähler Konfiguration", - "sdmbaudrate": "Baudrate", "sdmaddress": "Modbus Adresse", "HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration", "httpIndividualRequests": "Individuelle HTTP requests pro Phase", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 2e3dde41..4b7a3959 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -570,7 +570,6 @@ "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", "SDM": "SDM-Power Meter Parameter", - "sdmbaudrate": "Baudrate", "sdmaddress": "Modbus Address", "HTTP": "HTTP(S) + Json - General configuration", "httpIndividualRequests": "Individual HTTP requests per phase", diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index 97238929..1675d8d3 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -28,7 +28,6 @@ export interface PowerMeterConfig { mqtt_topic_powermeter_1: string; mqtt_topic_powermeter_2: string; mqtt_topic_powermeter_3: string; - sdmbaudrate: number; sdmaddress: number; http_individual_requests: boolean; http_phases: Array; diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index aff34017..fd600069 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -74,16 +74,6 @@ :text="$t('powermeteradmin.SDM')" textVariant="text-bg-primary" add-space> -
- -
-
- -
-
-
-
From 6e44a6d750fb767a10d94828be22574c2c5bd4f7 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 9 May 2024 21:15:37 +0200 Subject: [PATCH 09/50] powermeter refactor: allow destruction of httpClient make sure the wifiClient used by the httpClient lives longer than the httpClient, as it accesses the pointer to the wifiClient in its destructor. --- include/PowerMeterHttpJson.h | 8 +++-- include/PowerMeterHttpSml.h | 8 +++-- src/PowerMeterHttpJson.cpp | 66 ++++++++++++++++++++---------------- src/PowerMeterHttpSml.cpp | 49 ++++++++++++++------------ 4 files changed, 76 insertions(+), 55 deletions(-) diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index 9e548210..4b7c8675 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include #include #include @@ -12,6 +13,8 @@ using Unit_t = PowerMeterHttpConfig::Unit; class PowerMeterHttpJson : public PowerMeterProvider { public: + ~PowerMeterHttpJson(); + bool init() final { return true; } void deinit() final { } void loop() final; @@ -25,10 +28,11 @@ private: uint32_t _lastPoll; std::array _cache; std::array _powerValues; - HTTPClient httpClient; + std::unique_ptr wifiClient; + std::unique_ptr httpClient; String httpResponse; - bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); + bool httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); String extractParam(String& authReq, const String& param, const char delimit); String getcNonce(const int len); diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h index 31b44244..2d100bd3 100644 --- a/include/PowerMeterHttpSml.h +++ b/include/PowerMeterHttpSml.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -12,6 +13,8 @@ class PowerMeterHttpSml : public PowerMeterProvider { public: + ~PowerMeterHttpSml(); + bool init() final { return true; } void deinit() final { } void loop() final; @@ -38,9 +41,10 @@ private: {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower} }; - HTTPClient httpClient; + std::unique_ptr wifiClient; + std::unique_ptr httpClient; String httpResponse; - bool httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config); + bool httpRequest(const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); void prepareRequest(uint32_t timeout); }; diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index c8d95bd3..ea9d5bee 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -6,9 +6,17 @@ #include #include "mbedtls/sha256.h" #include -#include #include +PowerMeterHttpJson::~PowerMeterHttpJson() +{ + // the wifiClient instance must live longer than the httpClient instance, + // as the httpClient holds a pointer to the wifiClient and uses it in its + // destructor. + httpClient.reset(); + wifiClient.reset(); +} + void PowerMeterHttpJson::loop() { auto const& config = Configuration.get(); @@ -66,7 +74,7 @@ bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& confi //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 //and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 - //in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses. + //in conclusion: we cannot rely on httpClient->begin(*wifiClient, url) to resolve IP adresses. //have to do it manually here. Feels Hacky... String protocol; String host; @@ -102,10 +110,6 @@ bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& confi } } - // secureWifiClient MUST be created before HTTPClient - // see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381 - std::unique_ptr wifiClient; - bool https = protocol == "https"; if (https) { auto secureWifiClient = std::make_unique(); @@ -115,49 +119,51 @@ bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& confi wifiClient = std::make_unique(); } - return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, config); + return httpRequest(phase, ipaddr.toString(), port, uri, https, config); } -bool PowerMeterHttpJson::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) +bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) { - if(!httpClient.begin(wifiClient, host, port, uri, https)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); + if (!httpClient) { httpClient = std::make_unique(); } + + if(!httpClient->begin(*wifiClient, host, port, uri, https)){ + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient->begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); return false; } prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); if (config.AuthType == Auth_t::Digest) { const char *headers[1] = {"WWW-Authenticate"}; - httpClient.collectHeaders(headers, 1); + httpClient->collectHeaders(headers, 1); } else if (config.AuthType == Auth_t::Basic) { String authString = config.Username; authString += ":"; authString += config.Password; String auth = "Basic "; auth.concat(base64::encode(authString)); - httpClient.addHeader("Authorization", auth); + httpClient->addHeader("Authorization", auth); } - int httpCode = httpClient.GET(); + int httpCode = httpClient->GET(); if (httpCode == HTTP_CODE_UNAUTHORIZED && config.AuthType == Auth_t::Digest) { // Handle authentication challenge - if (httpClient.hasHeader("WWW-Authenticate")) { - String authReq = httpClient.header("WWW-Authenticate"); + if (httpClient->hasHeader("WWW-Authenticate")) { + String authReq = httpClient->header("WWW-Authenticate"); String authorization = getDigestAuth(authReq, String(config.Username), String(config.Password), "GET", String(uri), 1); - httpClient.end(); - if(!httpClient.begin(wifiClient, host, port, uri, https)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str()); + httpClient->end(); + if(!httpClient->begin(*wifiClient, host, port, uri, https)){ + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient->begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str()); return false; } prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); - httpClient.addHeader("Authorization", authorization); - httpCode = httpClient.GET(); + httpClient->addHeader("Authorization", authorization); + httpCode = httpClient->GET(); } } if (httpCode <= 0) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str()); + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient->errorToString(httpCode).c_str()); return false; } @@ -166,8 +172,8 @@ bool PowerMeterHttpJson::httpRequest(int phase, WiFiClient &wifiClient, const St return false; } - httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly - httpClient.end(); + httpResponse = httpClient->getString(); // very unfortunate that we cannot parse WifiClient stream directly + httpClient->end(); // TODO(schlimmchen): postpone calling tryGetFloatValueForPhase, as it // will be called twice for each phase when doing separate requests. @@ -391,14 +397,14 @@ String PowerMeterHttpJson::sha256(const String& data) { } void PowerMeterHttpJson::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) { - httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - httpClient.setUserAgent("OpenDTU-OnBattery"); - httpClient.setConnectTimeout(timeout); - httpClient.setTimeout(timeout); - httpClient.addHeader("Content-Type", "application/json"); - httpClient.addHeader("Accept", "application/json"); + httpClient->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + httpClient->setUserAgent("OpenDTU-OnBattery"); + httpClient->setConnectTimeout(timeout); + httpClient->setTimeout(timeout); + httpClient->addHeader("Content-Type", "application/json"); + httpClient->addHeader("Accept", "application/json"); if (strlen(httpHeader) > 0) { - httpClient.addHeader(httpHeader, httpValue); + httpClient->addHeader(httpHeader, httpValue); } } diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index f3d8e0c7..a753ed7b 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -16,6 +16,15 @@ void PowerMeterHttpSml::doMqttPublish() const { } +PowerMeterHttpSml::~PowerMeterHttpSml() +{ + // the wifiClient instance must live longer than the httpClient instance, + // as the httpClient holds a pointer to the wifiClient and uses it in its + // destructor. + httpClient.reset(); + wifiClient.reset(); +} + void PowerMeterHttpSml::loop() { auto const& config = Configuration.get(); @@ -38,7 +47,7 @@ bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config) //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 //and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 - //in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses. + //in conclusion: we cannot rely on httpClient->begin(*wifiClient, url) to resolve IP adresses. //have to do it manually here. Feels Hacky... String protocol; String host; @@ -74,10 +83,6 @@ bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config) } } - // secureWifiClient MUST be created before HTTPClient - // see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381 - std::unique_ptr wifiClient; - bool https = protocol == "https"; if (https) { auto secureWifiClient = std::make_unique(); @@ -87,13 +92,15 @@ bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config) wifiClient = std::make_unique(); } - return httpRequest(*wifiClient, ipaddr.toString(), port, uri, https, config); + return httpRequest(ipaddr.toString(), port, uri, https, config); } -bool PowerMeterHttpSml::httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) +bool PowerMeterHttpSml::httpRequest(const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) { - if(!httpClient.begin(wifiClient, host, port, uri, https)){ - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); + if (!httpClient) { httpClient = std::make_unique(); } + + if(!httpClient->begin(*wifiClient, host, port, uri, https)){ + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("httpClient->begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); return false; } @@ -104,12 +111,12 @@ bool PowerMeterHttpSml::httpRequest(WiFiClient &wifiClient, const String& host, authString += config.Password; String auth = "Basic "; auth.concat(base64::encode(authString)); - httpClient.addHeader("Authorization", auth); + httpClient->addHeader("Authorization", auth); - int httpCode = httpClient.GET(); + int httpCode = httpClient->GET(); if (httpCode <= 0) { - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str()); + snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("HTTP Error %s"), httpClient->errorToString(httpCode).c_str()); return false; } @@ -118,9 +125,9 @@ bool PowerMeterHttpSml::httpRequest(WiFiClient &wifiClient, const String& host, return false; } - while (httpClient.getStream().available()) { + while (httpClient->getStream().available()) { double readVal = 0; - unsigned char smlCurrentChar = httpClient.getStream().read(); + unsigned char smlCurrentChar = httpClient->getStream().read(); sml_states_t smlCurrentState = smlState(smlCurrentChar); if (smlCurrentState == SML_LISTEND) { for (auto& handler: smlHandlerList) { @@ -133,7 +140,7 @@ bool PowerMeterHttpSml::httpRequest(WiFiClient &wifiClient, const String& host, } } } - httpClient.end(); + httpClient->end(); return true; } @@ -190,10 +197,10 @@ bool PowerMeterHttpSml::extractUrlComponents(String url, String& _protocol, Stri } void PowerMeterHttpSml::prepareRequest(uint32_t timeout) { - httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - httpClient.setUserAgent("OpenDTU-OnBattery"); - httpClient.setConnectTimeout(timeout); - httpClient.setTimeout(timeout); - httpClient.addHeader("Content-Type", "application/json"); - httpClient.addHeader("Accept", "application/json"); + httpClient->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + httpClient->setUserAgent("OpenDTU-OnBattery"); + httpClient->setConnectTimeout(timeout); + httpClient->setTimeout(timeout); + httpClient->addHeader("Content-Type", "application/json"); + httpClient->addHeader("Accept", "application/json"); } From 6108d24795dfbf9f8015c87dcde7dea696beca28 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 9 May 2024 21:28:26 +0200 Subject: [PATCH 10/50] powermeter refactor: introduce PowerMeterSml this new class handles SML data. it uses the SML lib to decode values and manages those. this de-duplicates code as the class is applicable to all power meters that collect SML data. --- include/PowerMeterHttpSml.h | 23 ++------------------ include/PowerMeterSerialSml.h | 28 ++---------------------- include/PowerMeterSml.h | 40 +++++++++++++++++++++++++++++++++++ src/PowerMeterHttpSml.cpp | 27 +++-------------------- src/PowerMeterSerialSml.cpp | 23 +------------------- src/PowerMeterSml.cpp | 40 +++++++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 93 deletions(-) create mode 100644 include/PowerMeterSml.h create mode 100644 src/PowerMeterSml.cpp diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h index 2d100bd3..b27cd449 100644 --- a/include/PowerMeterHttpSml.h +++ b/include/PowerMeterHttpSml.h @@ -1,46 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include -#include #include #include #include #include #include "Configuration.h" -#include "PowerMeterProvider.h" -#include "sml.h" +#include "PowerMeterSml.h" -class PowerMeterHttpSml : public PowerMeterProvider { +class PowerMeterHttpSml : public PowerMeterSml { public: ~PowerMeterHttpSml(); bool init() final { return true; } void deinit() final { } void loop() final; - float getPowerTotal() const final; - void doMqttPublish() const final; bool updateValues(); char tibberPowerMeterError[256]; bool query(PowerMeterTibberConfig const& config); private: - mutable std::mutex _mutex; - uint32_t _lastPoll = 0; - float _activePower = 0.0; - - typedef struct { - const unsigned char OBIS[6]; - void (*Fn)(double&); - float* Arg; - } OBISHandler; - - const std::list smlHandlerList{ - {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower} - }; - std::unique_ptr wifiClient; std::unique_ptr httpClient; String httpResponse; diff --git a/include/PowerMeterSerialSml.h b/include/PowerMeterSerialSml.h index 31a90484..1dbf87d2 100644 --- a/include/PowerMeterSerialSml.h +++ b/include/PowerMeterSerialSml.h @@ -1,39 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "PowerMeterProvider.h" -#include "Configuration.h" -#include "sml.h" +#include "PowerMeterSml.h" #include -#include -#include -class PowerMeterSerialSml : public PowerMeterProvider { +class PowerMeterSerialSml : public PowerMeterSml { public: bool init() final; void deinit() final; void loop() final; - float getPowerTotal() const final { return _activePower; } - void doMqttPublish() const final; private: - float _activePower = 0.0; - float _energyImport = 0.0; - float _energyExport = 0.0; - - mutable std::mutex _mutex; - std::unique_ptr _upSmlSerial = nullptr; - - typedef struct { - const unsigned char OBIS[6]; - void (*Fn)(double&); - float* Arg; - } OBISHandler; - - const std::list smlHandlerList{ - {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower}, - {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyImport}, - {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyExport} - }; }; diff --git a/include/PowerMeterSml.h b/include/PowerMeterSml.h new file mode 100644 index 00000000..e006f69f --- /dev/null +++ b/include/PowerMeterSml.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include "Configuration.h" +#include "PowerMeterProvider.h" +#include "sml.h" + +class PowerMeterSml : public PowerMeterProvider { +public: + float getPowerTotal() const final; + void doMqttPublish() const final; + +protected: + void processSmlByte(uint8_t byte); + +private: + mutable std::mutex _mutex; + + float _activePower = 0.0; + float _energyImport = 0.0; + float _energyExport = 0.0; + + typedef struct { + uint8_t const OBIS[6]; + void (*decoder)(double&); + float* target; + char const* name; + } OBISHandler; + + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower, "active power"}, + {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyImport, "energy import"}, + {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyExport, "energy export"} + }; +}; diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index a753ed7b..5e0df2c2 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -6,16 +6,6 @@ #include #include -float PowerMeterHttpSml::getPowerTotal() const -{ - std::lock_guard l(_mutex); - return _activePower; -} - -void PowerMeterHttpSml::doMqttPublish() const -{ -} - PowerMeterHttpSml::~PowerMeterHttpSml() { // the wifiClient instance must live longer than the httpClient instance, @@ -125,20 +115,9 @@ bool PowerMeterHttpSml::httpRequest(const String& host, uint16_t port, const Str return false; } - while (httpClient->getStream().available()) { - double readVal = 0; - unsigned char smlCurrentChar = httpClient->getStream().read(); - sml_states_t smlCurrentState = smlState(smlCurrentChar); - if (smlCurrentState == SML_LISTEND) { - for (auto& handler: smlHandlerList) { - if (smlOBISCheck(handler.OBIS)) { - std::lock_guard l(_mutex); - handler.Fn(readVal); - *handler.Arg = readVal; - gotUpdate(); - } - } - } + auto& stream = httpClient->getStream(); + while (stream.available()) { + processSmlByte(stream.read()); } httpClient->end(); diff --git a/src/PowerMeterSerialSml.cpp b/src/PowerMeterSerialSml.cpp index 9236dcd7..96580dbe 100644 --- a/src/PowerMeterSerialSml.cpp +++ b/src/PowerMeterSerialSml.cpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "PowerMeterSerialSml.h" -#include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" @@ -32,32 +31,12 @@ void PowerMeterSerialSml::deinit() _upSmlSerial->end(); } -void PowerMeterSerialSml::doMqttPublish() const -{ - std::lock_guard l(_mutex); - mqttPublish("import", _energyImport); - mqttPublish("export", _energyExport); -} - void PowerMeterSerialSml::loop() { if (!_upSmlSerial) { return; } while (_upSmlSerial->available()) { - double readVal = 0; - unsigned char smlCurrentChar = _upSmlSerial->read(); - sml_states_t smlCurrentState = smlState(smlCurrentChar); - if (smlCurrentState == SML_LISTEND) { - for (auto& handler: smlHandlerList) { - if (smlOBISCheck(handler.OBIS)) { - handler.Fn(readVal); - std::lock_guard l(_mutex); - *handler.Arg = readVal; - } - } - } else if (smlCurrentState == SML_FINAL) { - gotUpdate(); - } + processSmlByte(_upSmlSerial->read()); } MessageOutput.printf("[PowerMeterSerialSml]: TotalPower: %5.2f\r\n", getPowerTotal()); diff --git a/src/PowerMeterSml.cpp b/src/PowerMeterSml.cpp new file mode 100644 index 00000000..f3d619b8 --- /dev/null +++ b/src/PowerMeterSml.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSml.h" +#include "MessageOutput.h" + +float PowerMeterSml::getPowerTotal() const +{ + std::lock_guard l(_mutex); + return _activePower; +} + +void PowerMeterSml::doMqttPublish() const +{ + std::lock_guard l(_mutex); + mqttPublish("import", _energyImport); + mqttPublish("export", _energyExport); +} + +void PowerMeterSml::processSmlByte(uint8_t byte) +{ + switch (smlState(byte)) { + case SML_LISTEND: + for (auto& handler: smlHandlerList) { + if (!smlOBISCheck(handler.OBIS)) { continue; } + + double helper; + handler.decoder(helper); + + std::lock_guard l(_mutex); + *handler.target = helper; + gotUpdate(); + + if (!_verboseLogging) { continue; } + MessageOutput.printf("[PowerMeterSml] decoded %s to %.2f\r\n", + handler.name, helper); + } + break; + default: + break; + } +} From 75c07c17f22621c0aa465baf8f8de100a8bd94bd Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 9 May 2024 21:46:59 +0200 Subject: [PATCH 11/50] powermeter refactor: SML lib: replace double by float avoid additional conversions and avoid double for the fact that calculations on type double are implemented in software, whereas float is handled in hardware on ESP32. --- include/PowerMeterSml.h | 2 +- lib/SMLParser/sml.cpp | 10 +++++----- lib/SMLParser/sml.h | 9 ++++----- src/PowerMeterSml.cpp | 15 +++++++-------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/include/PowerMeterSml.h b/include/PowerMeterSml.h index e006f69f..fa6a3215 100644 --- a/include/PowerMeterSml.h +++ b/include/PowerMeterSml.h @@ -27,7 +27,7 @@ private: typedef struct { uint8_t const OBIS[6]; - void (*decoder)(double&); + void (*decoder)(float&); float* target; char const* name; } OBISHandler; diff --git a/lib/SMLParser/sml.cpp b/lib/SMLParser/sml.cpp index 7a378f63..f1892594 100644 --- a/lib/SMLParser/sml.cpp +++ b/lib/SMLParser/sml.cpp @@ -317,7 +317,7 @@ void smlOBISManufacturer(unsigned char *str, int maxSize) } } -void smlPow(double &val, signed char &scaler) +void smlPow(float &val, signed char &scaler) { if (scaler < 0) { while (scaler++) { @@ -372,7 +372,7 @@ void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit) } } -void smlOBISWh(double &wh) +void smlOBISWh(float &wh) { long long int val; smlOBISByUnit(val, sc, SML_WATT_HOUR); @@ -380,7 +380,7 @@ void smlOBISWh(double &wh) smlPow(wh, sc); } -void smlOBISW(double &w) +void smlOBISW(float &w) { long long int val; smlOBISByUnit(val, sc, SML_WATT); @@ -388,7 +388,7 @@ void smlOBISW(double &w) smlPow(w, sc); } -void smlOBISVolt(double &v) +void smlOBISVolt(float &v) { long long int val; smlOBISByUnit(val, sc, SML_VOLT); @@ -396,7 +396,7 @@ void smlOBISVolt(double &v) smlPow(v, sc); } -void smlOBISAmpere(double &a) +void smlOBISAmpere(float &a) { long long int val; smlOBISByUnit(val, sc, SML_AMPERE); diff --git a/lib/SMLParser/sml.h b/lib/SMLParser/sml.h index ac6405df..b837adda 100644 --- a/lib/SMLParser/sml.h +++ b/lib/SMLParser/sml.h @@ -97,10 +97,9 @@ bool smlOBISCheck(const unsigned char *obis); void smlOBISManufacturer(unsigned char *str, int maxSize); void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit); -// Be aware that double on Arduino UNO is just 32 bit -void smlOBISWh(double &wh); -void smlOBISW(double &w); -void smlOBISVolt(double &v); -void smlOBISAmpere(double &a); +void smlOBISWh(float &wh); +void smlOBISW(float &w); +void smlOBISVolt(float &v); +void smlOBISAmpere(float &a); #endif diff --git a/src/PowerMeterSml.cpp b/src/PowerMeterSml.cpp index f3d619b8..0c2f6893 100644 --- a/src/PowerMeterSml.cpp +++ b/src/PowerMeterSml.cpp @@ -22,16 +22,15 @@ void PowerMeterSml::processSmlByte(uint8_t byte) for (auto& handler: smlHandlerList) { if (!smlOBISCheck(handler.OBIS)) { continue; } - double helper; - handler.decoder(helper); - - std::lock_guard l(_mutex); - *handler.target = helper; gotUpdate(); - if (!_verboseLogging) { continue; } - MessageOutput.printf("[PowerMeterSml] decoded %s to %.2f\r\n", - handler.name, helper); + std::lock_guard l(_mutex); + handler.decoder(*handler.target); + + if (_verboseLogging) { + MessageOutput.printf("[PowerMeterSml] decoded %s to %.2f\r\n", + handler.name, *handler.target); + } } break; default: From e78f5849c1e472d9477077d212f0ecc83397944b Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 9 May 2024 22:10:15 +0200 Subject: [PATCH 12/50] Feature: decode more OBIS values in SML power meters supersedes #951. --- include/PowerMeterSml.h | 22 ++++++++++++++++++++-- src/PowerMeterSml.cpp | 11 ++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/include/PowerMeterSml.h b/include/PowerMeterSml.h index fa6a3215..7a599972 100644 --- a/include/PowerMeterSml.h +++ b/include/PowerMeterSml.h @@ -21,7 +21,16 @@ protected: private: mutable std::mutex _mutex; - float _activePower = 0.0; + float _activePowerTotal = 0.0; + float _activePowerL1 = 0.0; + float _activePowerL2 = 0.0; + float _activePowerL3 = 0.0; + float _voltageL1 = 0.0; + float _voltageL2 = 0.0; + float _voltageL3 = 0.0; + float _currentL1 = 0.0; + float _currentL2 = 0.0; + float _currentL3 = 0.0; float _energyImport = 0.0; float _energyExport = 0.0; @@ -33,7 +42,16 @@ private: } OBISHandler; const std::list smlHandlerList{ - {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower, "active power"}, + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePowerTotal, "active power total"}, + {{0x01, 0x00, 0x24, 0x07, 0x00, 0xff}, &smlOBISW, &_activePowerL1, "active power L1"}, + {{0x01, 0x00, 0x38, 0x07, 0x00, 0xff}, &smlOBISW, &_activePowerL2, "active power L2"}, + {{0x01, 0x00, 0x4c, 0x07, 0x00, 0xff}, &smlOBISW, &_activePowerL3, "active power L3"}, + {{0x01, 0x00, 0x20, 0x07, 0x00, 0xff}, &smlOBISVolt, &_voltageL1, "voltage L1"}, + {{0x01, 0x00, 0x34, 0x07, 0x00, 0xff}, &smlOBISVolt, &_voltageL2, "voltage L2"}, + {{0x01, 0x00, 0x48, 0x07, 0x00, 0xff}, &smlOBISVolt, &_voltageL3, "voltage L3"}, + {{0x01, 0x00, 0x1f, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_currentL1, "current L1"}, + {{0x01, 0x00, 0x33, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_currentL2, "current L2"}, + {{0x01, 0x00, 0x47, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_currentL3, "current L3"}, {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyImport, "energy import"}, {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyExport, "energy export"} }; diff --git a/src/PowerMeterSml.cpp b/src/PowerMeterSml.cpp index 0c2f6893..9f46fda2 100644 --- a/src/PowerMeterSml.cpp +++ b/src/PowerMeterSml.cpp @@ -5,12 +5,21 @@ float PowerMeterSml::getPowerTotal() const { std::lock_guard l(_mutex); - return _activePower; + return _activePowerTotal; } void PowerMeterSml::doMqttPublish() const { std::lock_guard l(_mutex); + mqttPublish("power1", _activePowerL1); + mqttPublish("power2", _activePowerL2); + mqttPublish("power3", _activePowerL3); + mqttPublish("voltage1", _voltageL1); + mqttPublish("voltage2", _voltageL2); + mqttPublish("voltage3", _voltageL3); + mqttPublish("current1", _currentL1); + mqttPublish("current2", _currentL2); + mqttPublish("current3", _currentL3); mqttPublish("import", _energyImport); mqttPublish("export", _energyExport); } From 673b9f4fa8b83e30674994aa1a3293c2376e9722 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 9 May 2024 22:23:42 +0200 Subject: [PATCH 13/50] powermeter refactor: use destructors to de-initialize --- include/PowerMeterHttpJson.h | 1 - include/PowerMeterHttpSml.h | 1 - include/PowerMeterMqtt.h | 3 ++- include/PowerMeterProvider.h | 1 - include/PowerMeterSerialSdm.h | 3 ++- include/PowerMeterSerialSml.h | 3 ++- include/PowerMeterUdpSmaHomeManager.h | 3 ++- src/PowerMeter.cpp | 5 +---- src/PowerMeterMqtt.cpp | 2 +- src/PowerMeterSerialSdm.cpp | 2 +- src/PowerMeterSerialSml.cpp | 2 +- src/PowerMeterUdpSmaHomeManager.cpp | 2 +- 12 files changed, 13 insertions(+), 15 deletions(-) diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index 4b7c8675..08c11372 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -16,7 +16,6 @@ public: ~PowerMeterHttpJson(); bool init() final { return true; } - void deinit() final { } void loop() final; float getPowerTotal() const final; void doMqttPublish() const final; diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h index b27cd449..c6e46bdc 100644 --- a/include/PowerMeterHttpSml.h +++ b/include/PowerMeterHttpSml.h @@ -13,7 +13,6 @@ public: ~PowerMeterHttpSml(); bool init() final { return true; } - void deinit() final { } void loop() final; bool updateValues(); char tibberPowerMeterError[256]; diff --git a/include/PowerMeterMqtt.h b/include/PowerMeterMqtt.h index 3786fca6..8880f481 100644 --- a/include/PowerMeterMqtt.h +++ b/include/PowerMeterMqtt.h @@ -8,8 +8,9 @@ class PowerMeterMqtt : public PowerMeterProvider { public: + ~PowerMeterMqtt(); + bool init() final; - void deinit() final; void loop() final { } float getPowerTotal() const final; void doMqttPublish() const final; diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h index 17704135..4cd1c888 100644 --- a/include/PowerMeterProvider.h +++ b/include/PowerMeterProvider.h @@ -21,7 +21,6 @@ public: // returns true if the provider is ready for use, false otherwise virtual bool init() = 0; - virtual void deinit() = 0; virtual void loop() = 0; virtual float getPowerTotal() const = 0; diff --git a/include/PowerMeterSerialSdm.h b/include/PowerMeterSerialSdm.h index 7e01c8f7..ff33bebd 100644 --- a/include/PowerMeterSerialSdm.h +++ b/include/PowerMeterSerialSdm.h @@ -7,8 +7,9 @@ class PowerMeterSerialSdm : public PowerMeterProvider { public: + ~PowerMeterSerialSdm(); + bool init() final; - void deinit() final; void loop() final; float getPowerTotal() const final; void doMqttPublish() const final; diff --git a/include/PowerMeterSerialSml.h b/include/PowerMeterSerialSml.h index 1dbf87d2..58e01961 100644 --- a/include/PowerMeterSerialSml.h +++ b/include/PowerMeterSerialSml.h @@ -6,8 +6,9 @@ class PowerMeterSerialSml : public PowerMeterSml { public: + ~PowerMeterSerialSml(); + bool init() final; - void deinit() final; void loop() final; private: diff --git a/include/PowerMeterUdpSmaHomeManager.h b/include/PowerMeterUdpSmaHomeManager.h index 34e47f4d..5d4b3a8d 100644 --- a/include/PowerMeterUdpSmaHomeManager.h +++ b/include/PowerMeterUdpSmaHomeManager.h @@ -9,8 +9,9 @@ class PowerMeterUdpSmaHomeManager : public PowerMeterProvider { public: + ~PowerMeterUdpSmaHomeManager(); + bool init() final; - void deinit() final; void loop() final; float getPowerTotal() const final { return _powerMeterPower; } void doMqttPublish() const final; diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 37c3ab34..47261318 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -24,10 +24,7 @@ void PowerMeterClass::updateSettings() { std::lock_guard l(_mutex); - if (_upProvider) { - _upProvider->deinit(); - _upProvider = nullptr; - } + if (_upProvider) { _upProvider.reset(); } auto const& config = Configuration.get(); diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp index 3204bf1f..48f6e459 100644 --- a/src/PowerMeterMqtt.cpp +++ b/src/PowerMeterMqtt.cpp @@ -26,7 +26,7 @@ bool PowerMeterMqtt::init() return _mqttSubscriptions.size() > 0; } -void PowerMeterMqtt::deinit() +PowerMeterMqtt::~PowerMeterMqtt() { for (auto const& t: _mqttSubscriptions) { MqttSettings.unsubscribe(t); } _mqttSubscriptions.clear(); diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp index f040c173..1612bcbe 100644 --- a/src/PowerMeterSerialSdm.cpp +++ b/src/PowerMeterSerialSdm.cpp @@ -5,7 +5,7 @@ #include "MessageOutput.h" #include "SerialPortManager.h" -void PowerMeterSerialSdm::deinit() +PowerMeterSerialSdm::~PowerMeterSerialSdm() { if (_upSdmSerial) { _upSdmSerial->end(); diff --git a/src/PowerMeterSerialSml.cpp b/src/PowerMeterSerialSml.cpp index 96580dbe..5f5a64c1 100644 --- a/src/PowerMeterSerialSml.cpp +++ b/src/PowerMeterSerialSml.cpp @@ -25,7 +25,7 @@ bool PowerMeterSerialSml::init() return true; } -void PowerMeterSerialSml::deinit() +PowerMeterSerialSml::~PowerMeterSerialSml() { if (!_upSmlSerial) { return; } _upSmlSerial->end(); diff --git a/src/PowerMeterUdpSmaHomeManager.cpp b/src/PowerMeterUdpSmaHomeManager.cpp index 1347bfb4..2baa9c43 100644 --- a/src/PowerMeterUdpSmaHomeManager.cpp +++ b/src/PowerMeterUdpSmaHomeManager.cpp @@ -29,7 +29,7 @@ bool PowerMeterUdpSmaHomeManager::init() return true; } -void PowerMeterUdpSmaHomeManager::deinit() +PowerMeterUdpSmaHomeManager::~PowerMeterUdpSmaHomeManager() { SMAUdp.stop(); } From ccba7d803668870dfcfdff10ea9271ff0c1ff12e Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 10 May 2024 14:50:02 +0200 Subject: [PATCH 14/50] move JSON path resolver to Utils class for re-use --- include/PowerMeterHttpJson.h | 1 + include/Utils.h | 5 +++ src/PowerMeterHttpJson.cpp | 74 +++------------------------------ src/Utils.cpp | 80 ++++++++++++++++++++++++++++++++++++ src/WebApi_powermeter.cpp | 2 +- 5 files changed, 93 insertions(+), 69 deletions(-) diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index 08c11372..692bd982 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -22,6 +22,7 @@ public: bool queryPhase(int phase, PowerMeterHttpConfig const& config); char httpPowerMeterError[256]; + float getCached(size_t idx) { return _cache[idx]; } private: uint32_t _lastPoll; diff --git a/include/Utils.h b/include/Utils.h index f81e7318..a17b910a 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -3,6 +3,7 @@ #include #include +#include class Utils { public: @@ -12,4 +13,8 @@ public: static void restartDtu(); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); + + /* OpenDTU-OnBatter-specific utils go here: */ + template + static std::pair getJsonValueFromStringByPath(String const& jsonText, String const& path); }; diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index ea9d5bee..73d1439e 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +#include "Utils.h" #include "Configuration.h" #include "PowerMeterHttpJson.h" #include "MessageOutput.h" @@ -237,79 +238,16 @@ String PowerMeterHttpJson::getDigestAuth(String& authReq, const String& username bool PowerMeterHttpJson::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted) { - JsonDocument root; - const DeserializationError error = deserializeJson(root, httpResponse); - if (error) { + auto pathResolutionResult = Utils::getJsonValueFromStringByPath(httpResponse, jsonPath); + if (!pathResolutionResult.second.isEmpty()) { snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[PowerMeterHttpJson] Unable to parse server response as JSON")); - return false; - } - - constexpr char delimiter = '/'; - int start = 0; - int end = jsonPath.indexOf(delimiter); - auto value = root.as(); - - auto getNext = [this, &value, &jsonPath, &start](String const& key) -> bool { - // handle double forward slashes and paths starting or ending with a slash - if (key.isEmpty()) { return true; } - - if (key[0] == '[' && key[key.length() - 1] == ']') { - if (!value.is()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Cannot access non-array JSON node " - "using array index '%s' (JSON path '%s', position %i)"), - key.c_str(), jsonPath.c_str(), start); - return false; - } - - auto idx = key.substring(1, key.length() - 1).toInt(); - value = value[idx]; - - if (value.isNull()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Unable to access JSON array " - "index %li (JSON path '%s', position %i)"), - idx, jsonPath.c_str(), start); - return false; - } - - return true; - } - - value = value[key]; - - if (value.isNull()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Unable to access JSON key " - "'%s' (JSON path '%s', position %i)"), - key.c_str(), jsonPath.c_str(), start); - return false; - } - - return true; - }; - - // NOTE: "Because ArduinoJson implements the Null Object Pattern, it is - // always safe to read the object: if the key doesn't exist, it returns an - // empty value." - while (end != -1) { - if (!getNext(jsonPath.substring(start, end))) { return false; } - start = end + 1; - end = jsonPath.indexOf(delimiter, start); - } - - if (!getNext(jsonPath.substring(start))) { return false; } - - if (!value.is()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[PowerMeterHttpJson] not a float: '%s'"), - value.as().c_str()); + PSTR("[PowerMeterHttpJson] %s"), + pathResolutionResult.second.c_str()); return false; } // this value is supposed to be in Watts and positive if energy is consumed. - _cache[phase] = value.as(); + _cache[phase] = pathResolutionResult.first; switch (unit) { case Unit_t::MilliWatts: diff --git a/src/Utils.cpp b/src/Utils.cpp index 6abe4dd1..aa97455a 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -92,3 +92,83 @@ void Utils::removeAllFiles() file = root.getNextFileName(); } } + +/* OpenDTU-OnBatter-specific utils go here: */ +template +std::pair Utils::getJsonValueFromStringByPath(String const& jsonText, String const& path) +{ + JsonDocument root; + const DeserializationError error = deserializeJson(root, jsonText); + if (error) { + return { T(), "Unable to parse server response as JSON" }; + } + + size_t constexpr kErrBufferSize = 256; + char errBuffer[kErrBufferSize]; + constexpr char delimiter = '/'; + int start = 0; + int end = path.indexOf(delimiter); + auto value = root.as(); + + // NOTE: "Because ArduinoJson implements the Null Object Pattern, it is + // always safe to read the object: if the key doesn't exist, it returns an + // empty value." + auto getNext = [&](String const& key) -> bool { + // handle double forward slashes and paths starting or ending with a slash + if (key.isEmpty()) { return true; } + + if (key[0] == '[' && key[key.length() - 1] == ']') { + if (!value.is()) { + snprintf(errBuffer, kErrBufferSize, "Cannot access non-array " + "JSON node using array index '%s' (JSON path '%s', " + "position %i)", key.c_str(), path.c_str(), start); + return false; + } + + auto idx = key.substring(1, key.length() - 1).toInt(); + value = value[idx]; + + if (value.isNull()) { + snprintf(errBuffer, kErrBufferSize, "Unable to access JSON " + "array index %li (JSON path '%s', position %i)", + idx, path.c_str(), start); + return false; + } + + return true; + } + + value = value[key]; + + if (value.isNull()) { + snprintf(errBuffer, kErrBufferSize, "Unable to access JSON key " + "'%s' (JSON path '%s', position %i)", + key.c_str(), path.c_str(), start); + return false; + } + + return true; + }; + + while (end != -1) { + if (!getNext(path.substring(start, end))) { + return { T(), String(errBuffer) }; + } + start = end + 1; + end = path.indexOf(delimiter, start); + } + + if (!getNext(path.substring(start))) { + return { T(), String(errBuffer) }; + } + + if (!value.is()) { + snprintf(errBuffer, kErrBufferSize, "Value '%s' at JSON path '%s' is not " + "of the expected type", value.as().c_str(), path.c_str()); + return { T(), String(errBuffer) }; + } + + return { value.as(), "" }; +} + +template std::pair Utils::getJsonValueFromStringByPath(String const& jsonText, String const& path); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 0e519dbc..bf60beea 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -258,7 +258,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) auto upMeter = std::make_unique(); if (upMeter->queryPhase(0/*phase*/, phaseConfig)) { retMsg["type"] = "success"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal()); + snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getCached(0)); } else { snprintf_P(response, sizeof(response), "%s", upMeter->httpPowerMeterError); } From 297b149f8454d94ad4a870502580e1dc7042a3a5 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 11 May 2024 21:30:27 +0200 Subject: [PATCH 15/50] powermeter refactor: generalize HTTP request config the parameters to peform an HTTP request by the HTTP(S)+JSON power meter have been generalized by introducing a new config struct. this is now used for all values which the HTTP(S)+JSON power meter can retrieve, and also used by the HTTP+SML power meter implementation. we anticipate that other feature will use this config as well. generalizing also allows to share serialization and deserialization methods in the configuration handler and the web API handler, leading to de-duplication of code and reduced flash memory usage. a new web UI component is implemented to manage a set of HTTP request settings. --- include/Configuration.h | 68 ++++--- include/PowerMeterHttpJson.h | 12 +- include/PowerMeterHttpSml.h | 4 +- include/WebApi_powermeter.h | 6 +- include/defaults.h | 2 + src/Configuration.cpp | 116 +++++++---- src/PowerMeterHttpJson.cpp | 19 +- src/PowerMeterHttpSml.cpp | 8 +- src/WebApi_powermeter.cpp | 189 +++++++----------- webapp/src/components/HttpRequestSettings.vue | 77 +++++++ webapp/src/locales/de.json | 44 ++-- webapp/src/locales/en.json | 48 +++-- webapp/src/types/HttpRequestConfig.ts | 9 + webapp/src/types/PowerMeterConfig.ts | 23 +-- webapp/src/views/PowerMeterAdminView.vue | 162 +++++---------- 15 files changed, 413 insertions(+), 374 deletions(-) create mode 100644 webapp/src/components/HttpRequestSettings.vue create mode 100644 webapp/src/types/HttpRequestConfig.ts diff --git a/include/Configuration.h b/include/Configuration.h index 3c0c41a1..66fb4861 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -3,6 +3,7 @@ #include "PinMapping.h" #include +#include #define CONFIG_FILENAME "/config.json" #define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change @@ -30,14 +31,14 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 -#define POWERMETER_MAX_PHASES 3 -#define POWERMETER_MAX_HTTP_URL_STRLEN 1024 -#define POWERMETER_MAX_USERNAME_STRLEN 64 -#define POWERMETER_MAX_PASSWORD_STRLEN 64 -#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64 -#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256 -#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256 -#define POWERMETER_HTTP_TIMEOUT 1000 +#define HTTP_REQUEST_MAX_URL_STRLEN 1024 +#define HTTP_REQUEST_MAX_USERNAME_STRLEN 64 +#define HTTP_REQUEST_MAX_PASSWORD_STRLEN 64 +#define HTTP_REQUEST_MAX_HEADER_KEY_STRLEN 64 +#define HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN 256 + +#define POWERMETER_HTTP_JSON_MAX_VALUES 3 +#define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256 struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; @@ -61,30 +62,36 @@ struct INVERTER_CONFIG_T { CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; -struct POWERMETER_HTTP_PHASE_CONFIG_T { +struct HTTP_REQUEST_CONFIG_T { + char Url[HTTP_REQUEST_MAX_URL_STRLEN + 1]; + enum Auth { None, Basic, Digest }; - enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; - bool Enabled; - char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1]; Auth AuthType; - char Username[POWERMETER_MAX_USERNAME_STRLEN +1]; - char Password[POWERMETER_MAX_USERNAME_STRLEN +1]; - char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1]; - char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1]; + + char Username[HTTP_REQUEST_MAX_USERNAME_STRLEN + 1]; + char Password[HTTP_REQUEST_MAX_PASSWORD_STRLEN + 1]; + char HeaderKey[HTTP_REQUEST_MAX_HEADER_KEY_STRLEN + 1]; + char HeaderValue[HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN + 1]; uint16_t Timeout; - char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1]; +}; +using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T; + +struct POWERMETER_HTTP_JSON_CONFIG_T { + HttpRequestConfig HttpRequest; + bool Enabled; + char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + + enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; Unit PowerUnit; + bool SignInverted; }; -using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T; +using PowerMeterHttpJsonConfig = struct POWERMETER_HTTP_JSON_CONFIG_T; -struct POWERMETER_TIBBER_CONFIG_T { - char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1]; - char Username[POWERMETER_MAX_USERNAME_STRLEN + 1]; - char Password[POWERMETER_MAX_USERNAME_STRLEN + 1]; - uint16_t Timeout; +struct POWERMETER_HTTP_SML_CONFIG_T { + HttpRequestConfig HttpRequest; }; -using PowerMeterTibberConfig = struct POWERMETER_TIBBER_CONFIG_T; +using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; struct CONFIG_T { struct { @@ -204,10 +211,9 @@ struct CONFIG_T { char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; uint32_t SdmAddress; - uint32_t HttpInterval; bool HttpIndividualRequests; - PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES]; - PowerMeterTibberConfig Tibber; + PowerMeterHttpJsonConfig HttpJson[POWERMETER_HTTP_JSON_MAX_VALUES]; + PowerMeterHttpSmlConfig HttpSml; } PowerMeter; struct { @@ -280,6 +286,14 @@ public: INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); void deleteInverterById(const uint8_t id); + + static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target); + static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); + static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target); + + static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target); + static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); + static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target); }; extern ConfigurationClass Configuration; diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index 692bd982..50bca8c5 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -8,8 +8,8 @@ #include "Configuration.h" #include "PowerMeterProvider.h" -using Auth_t = PowerMeterHttpConfig::Auth; -using Unit_t = PowerMeterHttpConfig::Unit; +using Auth_t = HttpRequestConfig::Auth; +using Unit_t = PowerMeterHttpJsonConfig::Unit; class PowerMeterHttpJson : public PowerMeterProvider { public: @@ -20,19 +20,19 @@ public: float getPowerTotal() const final; void doMqttPublish() const final; - bool queryPhase(int phase, PowerMeterHttpConfig const& config); + bool queryValue(int phase, PowerMeterHttpJsonConfig const& config); char httpPowerMeterError[256]; float getCached(size_t idx) { return _cache[idx]; } private: uint32_t _lastPoll; - std::array _cache; - std::array _powerValues; + std::array _cache; + std::array _powerValues; std::unique_ptr wifiClient; std::unique_ptr httpClient; String httpResponse; - bool httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); + bool httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpJsonConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); String extractParam(String& authReq, const String& param, const char delimit); String getcNonce(const int len); diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h index c6e46bdc..73bc882c 100644 --- a/include/PowerMeterHttpSml.h +++ b/include/PowerMeterHttpSml.h @@ -16,7 +16,7 @@ public: void loop() final; bool updateValues(); char tibberPowerMeterError[256]; - bool query(PowerMeterTibberConfig const& config); + bool query(HttpRequestConfig const& config); private: uint32_t _lastPoll = 0; @@ -24,7 +24,7 @@ private: std::unique_ptr wifiClient; std::unique_ptr httpClient; String httpResponse; - bool httpRequest(const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config); + bool httpRequest(const String& host, uint16_t port, const String& uri, bool https, HttpRequestConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); void prepareRequest(uint32_t timeout); }; diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h index 12e5afae..3cfe2a2d 100644 --- a/include/WebApi_powermeter.h +++ b/include/WebApi_powermeter.h @@ -14,10 +14,8 @@ private: void onStatus(AsyncWebServerRequest* request); void onAdminGet(AsyncWebServerRequest* request); void onAdminPost(AsyncWebServerRequest* request); - void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const; - void decodeJsonTibberConfig(JsonObject const& json, PowerMeterTibberConfig& config) const; - void onTestHttpRequest(AsyncWebServerRequest* request); - void onTestTibberRequest(AsyncWebServerRequest* request); + void onTestHttpJsonRequest(AsyncWebServerRequest* request); + void onTestHttpSmlRequest(AsyncWebServerRequest* request); AsyncWebServer* _server; }; diff --git a/include/defaults.h b/include/defaults.h index 865e595c..6b191cbd 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -119,6 +119,8 @@ #define POWERMETER_SOURCE 2 #define POWERMETER_SDMADDRESS 1 +#define HTTP_REQUEST_TIMEOUT_MS 1000 + #define POWERLIMITER_ENABLED false #define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true #define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index e899de9b..ffe96459 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -6,7 +6,6 @@ #include "MessageOutput.h" #include "Utils.h" #include "defaults.h" -#include #include #include @@ -17,6 +16,33 @@ void ConfigurationClass::init() memset(&config, 0x0, sizeof(config)); } +void ConfigurationClass::serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target) +{ + JsonObject target_http_config = target["http_request"].to(); + target_http_config["url"] = source.Url; + target_http_config["auth_type"] = source.AuthType; + target_http_config["username"] = source.Username; + target_http_config["password"] = source.Password; + target_http_config["header_key"] = source.HeaderKey; + target_http_config["header_value"] = source.HeaderValue; + target_http_config["timeout"] = source.Timeout; +} + +void ConfigurationClass::serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target) +{ + serializeHttpRequestConfig(source.HttpRequest, target); + + target["enabled"] = source.Enabled; + target["json_path"] = source.JsonPath; + target["unit"] = source.PowerUnit; + target["sign_inverted"] = source.SignInverted; +} + +void ConfigurationClass::serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target) +{ + serializeHttpRequestConfig(source.HttpRequest, target); +} + bool ConfigurationClass::write() { File f = LittleFS.open(CONFIG_FILENAME, "w"); @@ -158,27 +184,14 @@ bool ConfigurationClass::write() powermeter["sdmaddress"] = config.PowerMeter.SdmAddress; powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; - JsonObject tibber = powermeter["tibber"].to(); - tibber["url"] = config.PowerMeter.Tibber.Url; - tibber["username"] = config.PowerMeter.Tibber.Username; - tibber["password"] = config.PowerMeter.Tibber.Password; - tibber["timeout"] = config.PowerMeter.Tibber.Timeout; + JsonObject powermeter_http_sml = powermeter["http_sml"].to(); + serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml); - JsonArray powermeter_http_phases = powermeter["http_phases"].to(); - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - JsonObject powermeter_phase = powermeter_http_phases.add(); - - powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled; - powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url; - powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType; - powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username; - powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password; - powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey; - powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue; - powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; - powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath; - powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit; - powermeter_phase["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted; + JsonArray powermeter_http_json = powermeter["http_json"].to(); + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + JsonObject powermeter_json_config = powermeter_http_json.add(); + serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson[i], + powermeter_json_config); } JsonObject powerlimiter = doc["powerlimiter"].to(); @@ -246,6 +259,38 @@ bool ConfigurationClass::write() return true; } +void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target) +{ + JsonObject source_http_config = source["http_request"]; + + // http request parameters of HTTP/JSON power meter were + // previously stored alongside other settings + if (source_http_config.isNull()) { source_http_config = source; } + + strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url)); + target.AuthType = source_http_config["auth_type"] | HttpRequestConfig::Auth::None; + strlcpy(target.Username, source_http_config["username"] | "", sizeof(target.Username)); + strlcpy(target.Password, source_http_config["password"] | "", sizeof(target.Password)); + strlcpy(target.HeaderKey, source_http_config["header_key"] | "", sizeof(target.HeaderKey)); + strlcpy(target.HeaderValue, source_http_config["header_value"] | "", sizeof(target.HeaderValue)); + target.Timeout = source_http_config["timeout"] | HTTP_REQUEST_TIMEOUT_MS; +} + +void ConfigurationClass::deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target) +{ + deserializeHttpRequestConfig(source, target.HttpRequest); + + target.Enabled = source["enabled"] | false; + strlcpy(target.JsonPath, source["json_path"] | "", sizeof(target.JsonPath)); + target.PowerUnit = source["unit"] | PowerMeterHttpJsonConfig::Unit::Watts; + target.SignInverted = source["sign_inverted"] | false; +} + +void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target) +{ + deserializeHttpRequestConfig(source, target.HttpRequest); +} + bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); @@ -424,27 +469,16 @@ bool ConfigurationClass::read() config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false; - JsonObject tibber = powermeter["tibber"]; - strlcpy(config.PowerMeter.Tibber.Url, tibber["url"] | "", sizeof(config.PowerMeter.Tibber.Url)); - strlcpy(config.PowerMeter.Tibber.Username, tibber["username"] | "", sizeof(config.PowerMeter.Tibber.Username)); - strlcpy(config.PowerMeter.Tibber.Password, tibber["password"] | "", sizeof(config.PowerMeter.Tibber.Password)); - config.PowerMeter.Tibber.Timeout = tibber["timeout"] | POWERMETER_HTTP_TIMEOUT; + JsonObject powermeter_sml = powermeter["http_sml"]; + deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml); - JsonArray powermeter_http_phases = powermeter["http_phases"]; - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - JsonObject powermeter_phase = powermeter_http_phases[i].as(); - - config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); - strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url)); - config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | PowerMeterHttpConfig::Auth::None; - strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username)); - strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password)); - strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); - strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); - config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT; - strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); - config.PowerMeter.Http_Phase[i].PowerUnit = powermeter_phase["unit"] | PowerMeterHttpConfig::Unit::Watts; - config.PowerMeter.Http_Phase[i].SignInverted = powermeter_phase["sign_inverted"] | false; + JsonArray powermeter_http_json = powermeter["http_json"]; + if (powermeter_http_json.isNull()) { + powermeter_http_json = powermeter["http_phases"]; // http_phases is a legacy key + } + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + JsonObject powermeter_json_config = powermeter_http_json[i].as(); + deserializePowerMeterHttpJsonConfig(powermeter_json_config, config.PowerMeter.HttpJson[i]); } JsonObject powerlimiter = doc["powerlimiter"]; diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index 73d1439e..785775cc 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -27,16 +27,16 @@ void PowerMeterHttpJson::loop() _lastPoll = millis(); - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - auto const& phaseConfig = config.PowerMeter.Http_Phase[i]; + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + auto const& valueConfig = config.PowerMeter.HttpJson[i]; - if (!phaseConfig.Enabled) { + if (!valueConfig.Enabled) { _cache[i] = 0.0; continue; } if (i == 0 || config.PowerMeter.HttpIndividualRequests) { - if (!queryPhase(i, phaseConfig)) { + if (!queryValue(i, valueConfig)) { MessageOutput.printf("[PowerMeterHttpJson] Getting HTTP response for phase %d failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); return; @@ -44,7 +44,7 @@ void PowerMeterHttpJson::loop() continue; } - if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) { + if(!tryGetFloatValueForPhase(i, valueConfig.JsonPath, valueConfig.PowerUnit, valueConfig.SignInverted)) { MessageOutput.printf("[PowerMeterHttpJson] Reading power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); return; @@ -70,7 +70,7 @@ void PowerMeterHttpJson::doMqttPublish() const mqttPublish("power3", _powerValues[2]); } -bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& config) +bool PowerMeterHttpJson::queryValue(int phase, PowerMeterHttpJsonConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -82,7 +82,7 @@ bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& confi String uri; String base64Authorization; uint16_t port; - extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization); + extractUrlComponents(config.HttpRequest.Url, protocol, host, uri, port, base64Authorization); IPAddress ipaddr((uint32_t)0); //first check if "host" is already an IP adress @@ -123,7 +123,7 @@ bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& confi return httpRequest(phase, ipaddr.toString(), port, uri, https, config); } -bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) +bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpJsonConfig const& powerMeterConfig) { if (!httpClient) { httpClient = std::make_unique(); } @@ -132,6 +132,7 @@ bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t por return false; } + auto const& config = powerMeterConfig.HttpRequest; prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); if (config.AuthType == Auth_t::Digest) { const char *headers[1] = {"WWW-Authenticate"}; @@ -178,7 +179,7 @@ bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t por // TODO(schlimmchen): postpone calling tryGetFloatValueForPhase, as it // will be called twice for each phase when doing separate requests. - return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit, config.SignInverted); + return tryGetFloatValueForPhase(phase, powerMeterConfig.JsonPath, powerMeterConfig.PowerUnit, powerMeterConfig.SignInverted); } String PowerMeterHttpJson::extractParam(String& authReq, const String& param, const char delimit) { diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index 5e0df2c2..17750bba 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -24,15 +24,13 @@ void PowerMeterHttpSml::loop() _lastPoll = millis(); - auto const& tibberConfig = config.PowerMeter.Tibber; - - if (!query(tibberConfig)) { + if (!query(config.PowerMeter.HttpSml.HttpRequest)) { MessageOutput.printf("[PowerMeterHttpSml] Getting the power value failed.\r\n"); MessageOutput.printf("%s\r\n", tibberPowerMeterError); } } -bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config) +bool PowerMeterHttpSml::query(HttpRequestConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -85,7 +83,7 @@ bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config) return httpRequest(ipaddr.toString(), port, uri, https, config); } -bool PowerMeterHttpSml::httpRequest(const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) +bool PowerMeterHttpSml::httpRequest(const String& host, uint16_t port, const String& uri, bool https, HttpRequestConfig const& config) { if (!httpClient) { httpClient = std::make_unique(); } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index bf60beea..51f37353 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -26,31 +26,8 @@ void WebApiPowerMeterClass::init(AsyncWebServer& server, Scheduler& scheduler) _server->on("/api/powermeter/status", HTTP_GET, std::bind(&WebApiPowerMeterClass::onStatus, this, _1)); _server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1)); _server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, this, _1)); - _server->on("/api/powermeter/testhttprequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpRequest, this, _1)); - _server->on("/api/powermeter/testtibberrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestTibberRequest, this, _1)); -} - -void WebApiPowerMeterClass::decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const -{ - config.Enabled = json["enabled"].as(); - strlcpy(config.Url, json["url"].as().c_str(), sizeof(config.Url)); - config.AuthType = json["auth_type"].as(); - strlcpy(config.Username, json["username"].as().c_str(), sizeof(config.Username)); - strlcpy(config.Password, json["password"].as().c_str(), sizeof(config.Password)); - strlcpy(config.HeaderKey, json["header_key"].as().c_str(), sizeof(config.HeaderKey)); - strlcpy(config.HeaderValue, json["header_value"].as().c_str(), sizeof(config.HeaderValue)); - config.Timeout = json["timeout"].as(); - strlcpy(config.JsonPath, json["json_path"].as().c_str(), sizeof(config.JsonPath)); - config.PowerUnit = json["unit"].as(); - config.SignInverted = json["sign_inverted"].as(); -} - -void WebApiPowerMeterClass::decodeJsonTibberConfig(JsonObject const& json, PowerMeterTibberConfig& config) const -{ - strlcpy(config.Url, json["url"].as().c_str(), sizeof(config.Url)); - strlcpy(config.Username, json["username"].as().c_str(), sizeof(config.Username)); - strlcpy(config.Password, json["password"].as().c_str(), sizeof(config.Password)); - config.Timeout = json["timeout"].as(); + _server->on("/api/powermeter/testhttpjsonrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpJsonRequest, this, _1)); + _server->on("/api/powermeter/testhttpsmlrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpSmlRequest, this, _1)); } void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) @@ -69,29 +46,14 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) root["sdmaddress"] = config.PowerMeter.SdmAddress; root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; - auto tibber = root["tibber"].to(); - tibber["url"] = String(config.PowerMeter.Tibber.Url); - tibber["username"] = String(config.PowerMeter.Tibber.Username); - tibber["password"] = String(config.PowerMeter.Tibber.Password); - tibber["timeout"] = config.PowerMeter.Tibber.Timeout; + auto httpSml = root["http_sml"].to(); + Configuration.serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, httpSml); - auto httpPhases = root["http_phases"].to(); - - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - auto phaseObject = httpPhases.add(); - - phaseObject["index"] = i + 1; - phaseObject["enabled"] = config.PowerMeter.Http_Phase[i].Enabled; - phaseObject["url"] = String(config.PowerMeter.Http_Phase[i].Url); - phaseObject["auth_type"]= config.PowerMeter.Http_Phase[i].AuthType; - phaseObject["username"] = String(config.PowerMeter.Http_Phase[i].Username); - phaseObject["password"] = String(config.PowerMeter.Http_Phase[i].Password); - phaseObject["header_key"] = String(config.PowerMeter.Http_Phase[i].HeaderKey); - phaseObject["header_value"] = String(config.PowerMeter.Http_Phase[i].HeaderValue); - phaseObject["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; - phaseObject["json_path"] = String(config.PowerMeter.Http_Phase[i].JsonPath); - phaseObject["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit; - phaseObject["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted; + auto httpJson = root["http_json"].to(); + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + auto valueConfig = httpJson.add(); + valueConfig["index"] = i + 1; + Configuration.serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson[i], valueConfig); } WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -127,44 +89,52 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_JSON) { - JsonArray http_phases = root["http_phases"]; - for (uint8_t i = 0; i < http_phases.size(); i++) { - JsonObject phase = http_phases[i].as(); + auto checkHttpConfig = [&](JsonObject const& cfg) -> bool { + if (!cfg.containsKey("url") + || (!cfg["url"].as().startsWith("http://") + && !cfg["url"].as().startsWith("https://"))) { + retMsg["message"] = "URL must either start with http:// or https://!"; + response->setLength(); + request->send(response); + return false; + } - if (i > 0 && !phase["enabled"].as()) { + if ((cfg["auth_type"].as() != HttpRequestConfig::Auth::None) + && (cfg["username"].as().length() == 0 || cfg["password"].as().length() == 0)) { + retMsg["message"] = "Username or password must not be empty!"; + response->setLength(); + request->send(response); + return false; + } + + if (!cfg.containsKey("timeout") + || cfg["timeout"].as() <= 0) { + retMsg["message"] = "Timeout must be greater than 0 ms!"; + response->setLength(); + request->send(response); + return false; + } + + return true; + }; + + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_JSON) { + JsonArray httpJson = root["http_json"]; + for (uint8_t i = 0; i < httpJson.size(); i++) { + JsonObject valueConfig = httpJson[i].as(); + + if (i > 0 && !valueConfig["enabled"].as()) { continue; } - if (i == 0 || phase["http_individual_requests"].as()) { - if (!phase.containsKey("url") - || (!phase["url"].as().startsWith("http://") - && !phase["url"].as().startsWith("https://"))) { - retMsg["message"] = "URL must either start with http:// or https://!"; - response->setLength(); - request->send(response); - return; - } - - if ((phase["auth_type"].as() != PowerMeterHttpConfig::Auth::None) - && ( phase["username"].as().length() == 0 || phase["password"].as().length() == 0)) { - retMsg["message"] = "Username or password must not be empty!"; - response->setLength(); - request->send(response); - return; - } - - if (!phase.containsKey("timeout") - || phase["timeout"].as() <= 0) { - retMsg["message"] = "Timeout must be greater than 0 ms!"; - response->setLength(); - request->send(response); + if (i == 0 || valueConfig["http_individual_requests"].as()) { + if (!checkHttpConfig(valueConfig["http_request"].as())) { return; } } - if (!phase.containsKey("json_path") - || phase["json_path"].as().length() == 0) { + if (!valueConfig.containsKey("json_path") + || valueConfig["json_path"].as().length() == 0) { retMsg["message"] = "Json path must not be empty!"; response->setLength(); request->send(response); @@ -174,29 +144,8 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_SML) { - JsonObject tibber = root["tibber"]; - - if (!tibber.containsKey("url") - || (!tibber["url"].as().startsWith("http://") - && !tibber["url"].as().startsWith("https://"))) { - retMsg["message"] = "URL must either start with http:// or https://!"; - response->setLength(); - request->send(response); - return; - } - - if ((tibber["username"].as().length() == 0 || tibber["password"].as().length() == 0)) { - retMsg["message"] = "Username or password must not be empty!"; - response->setLength(); - request->send(response); - return; - } - - if (!tibber.containsKey("timeout") - || tibber["timeout"].as() <= 0) { - retMsg["message"] = "Timeout must be greater than 0 ms!"; - response->setLength(); - request->send(response); + JsonObject httpSml = root["http_sml"]; + if (!checkHttpConfig(httpSml["http_request"].as())) { return; } } @@ -212,13 +161,15 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerMeter.SdmAddress = root["sdmaddress"].as(); config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as(); - decodeJsonTibberConfig(root["tibber"].as(), config.PowerMeter.Tibber); + Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as(), + config.PowerMeter.HttpSml); - JsonArray http_phases = root["http_phases"]; - for (uint8_t i = 0; i < http_phases.size(); i++) { - decodeJsonPhaseConfig(http_phases[i].as(), config.PowerMeter.Http_Phase[i]); + JsonArray httpJson = root["http_json"]; + for (uint8_t i = 0; i < httpJson.size(); i++) { + Configuration.deserializePowerMeterHttpJsonConfig(httpJson[i].as(), + config.PowerMeter.HttpJson[i]); } - config.PowerMeter.Http_Phase[0].Enabled = true; + config.PowerMeter.HttpJson[0].Enabled = true; WebApi.writeConfig(retMsg); @@ -227,7 +178,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) PowerMeter.updateSettings(); } -void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) +void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; @@ -241,9 +192,15 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) auto& retMsg = asyncJsonResponse->getRoot(); - if (!root.containsKey("url") || !root.containsKey("auth_type") || !root.containsKey("username") || !root.containsKey("password") - || !root.containsKey("header_key") || !root.containsKey("header_value") - || !root.containsKey("timeout") || !root.containsKey("json_path")) { + JsonObject requestConfig = root["http_request"]; + if (!requestConfig.containsKey("url") + || !requestConfig.containsKey("auth_type") + || !requestConfig.containsKey("username") + || !requestConfig.containsKey("password") + || !requestConfig.containsKey("header_key") + || !requestConfig.containsKey("header_value") + || !requestConfig.containsKey("timeout") + || !root.containsKey("json_path")) { retMsg["message"] = "Missing fields!"; asyncJsonResponse->setLength(); request->send(asyncJsonResponse); @@ -253,10 +210,10 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) char response[256]; - PowerMeterHttpConfig phaseConfig; - decodeJsonPhaseConfig(root.as(), phaseConfig); + PowerMeterHttpJsonConfig httpJsonConfig; + Configuration.deserializePowerMeterHttpJsonConfig(root.as(), httpJsonConfig); auto upMeter = std::make_unique(); - if (upMeter->queryPhase(0/*phase*/, phaseConfig)) { + if (upMeter->queryValue(0/*value index*/, httpJsonConfig)) { retMsg["type"] = "success"; snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getCached(0)); } else { @@ -268,7 +225,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) request->send(asyncJsonResponse); } -void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request) +void WebApiPowerMeterClass::onTestHttpSmlRequest(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; @@ -293,10 +250,10 @@ void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request) char response[256]; - PowerMeterTibberConfig tibberConfig; - decodeJsonTibberConfig(root.as(), tibberConfig); + PowerMeterHttpSmlConfig httpSmlConfig; + Configuration.deserializePowerMeterHttpSmlConfig(root.as(), httpSmlConfig); auto upMeter = std::make_unique(); - if (upMeter->query(tibberConfig)) { + if (upMeter->query(httpSmlConfig.HttpRequest)) { retMsg["type"] = "success"; snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal()); } else { diff --git a/webapp/src/components/HttpRequestSettings.vue b/webapp/src/components/HttpRequestSettings.vue new file mode 100644 index 00000000..7c922c1c --- /dev/null +++ b/webapp/src/components/HttpRequestSettings.vue @@ -0,0 +1,77 @@ + + + diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index c7021b99..a982a563 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -558,37 +558,47 @@ "PowerMeterSource": "Stromzählertyp", "MQTT": "MQTT Konfiguration", "typeMQTT": "MQTT", - "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", - "typeSDM3ph": "SDM 3 phase (SDM72/630)", - "typeHTTP": "HTTP(S) + JSON", - "typeSML": "SML (OBIS 16.7.0)", + "typeSDM1ph": "SDM mit 1 Phase (SDM120/220/230)", + "typeSDM3ph": "SDM mit 3 Phasen (SDM72/630)", + "typeHTTP_JSON": "HTTP(S) + JSON", + "typeSML": "SML/OBIS via serieller Verbindung (z.B. Hichi TTL)", "typeSMAHM2": "SMA Homemanager 2.0", - "typeTIBBER": "Tibber Pulse (via Tibber Bridge)", + "typeHTTP_SML": "HTTP(S) + SML (z.B. Tibber Pulse via Tibber Bridge)", "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", "SDM": "SDM-Stromzähler Konfiguration", "sdmaddress": "Modbus Adresse", - "HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration", - "httpIndividualRequests": "Individuelle HTTP requests pro Phase", + "HTTP_JSON": "HTTP(S) + JSON - Allgemeine Konfiguration", + "httpIndividualRequests": "Individuelle HTTP Anfragen pro Wert", "urlExamplesHeading": "Beispiele für URLs", "jsonPathExamplesHeading": "Beispiele für JSON Pfade", "jsonPathExamplesExplanation": "Die folgenden Pfade finden jeweils den Wert '123.4' im jeweiligen Beispiel-JSON.", - "httpUrlDescription": "Die URL muss mit http:// oder https:// beginnen. Manche Zeichen wie Leerzeichen und = müssen mit URL-Kodierung kodiert werden (%xx). Achtung: Ein Überprüfung von SSL Server Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)!.", - "httpPhase": "HTTP(S) + JSON Konfiguration - Phase {phaseNumber}", - "httpEnabled": "Phase aktiviert", - "httpUrl": "URL", - "httpHeaderKey": "Optional: HTTP request header - Key", - "httpHeaderKeyDescription": "Ein individueller HTTP request header kann hier definiert werden. Das kann z.B. verwendet werden um einen eigenen Authorization header mitzugeben.", - "httpHeaderValue": "Optional: HTTP request header - Wert", + "httpValue": "Konfiguration für Wert {valueNumber}", + "httpEnabled": "Wert aktiviert", "httpJsonPath": "JSON Pfad", "httpJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in the HTTP(S) Antwort zu finden, z.B. 'power/total/watts' oder nur 'total'.", "httpUnit": "Einheit", "httpSignInverted": "Vorzeichen umkehren", "httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", - "httpTimeout": "Timeout", - "testHttpRequest": "Testen", - "TIBBER": "Tibber Pulse (via Tibber Bridge) - Konfiguration" + "testHttpJsonRequest": "Konfiguration testen (HTTP(S)-Anfrage senden)", + "testHttpSmlRequest": "Konfiguration testen (HTTP(S)-Anfrage senden)", + "HTTP_SML": "HTTP(S) + SML - Konfiguration" + }, + "httprequestsettings": { + "url": "URL", + "urlDescription": "Die URL muss mit 'http://' oder 'https://' beginnen. Zeichen wie Leerzeichen und = müssen mit URL-kodiert werden (%xx). Achtung: Eine Überprüfung von SSL-Server-Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)!.", + "authorization": "Authentifizierungsverfahren", + "authTypeNone": "Ohne", + "authTypeBasic": "Basic", + "authTypeDigest": "Digest", + "username": "Benutzername", + "password": "Passwort", + "headerKey": "HTTP Header - Name", + "headerKeyDescription": "Optional. Ein benutzerdefinierter HTTP header kann definiert werden. Nützlich um z.B. ein (zusätzlichen) Authentifizierungstoken zu übermitteln.", + "headerValue": "HTTP Header - Wert", + "timeout": "Zeitüberschreitung", + "milliSeconds": "ms" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 4b7a3959..30407c7d 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -560,41 +560,47 @@ "PowerMeterSource": "Power Meter type", "MQTT": "MQTT Parameter", "typeMQTT": "MQTT", - "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", - "typeSDM3ph": "SDM 3 phase (SDM72/630)", - "typeHTTP": "HTTP(s) + JSON", - "typeSML": "SML (OBIS 16.7.0)", + "typeSDM1ph": "SDM for 1 phase (SDM120/220/230)", + "typeSDM3ph": "SDM for 3 phases (SDM72/630)", + "typeHTTP_JSON": "HTTP(S) + JSON", + "typeSML": "SML/OBIS via serial connection (e.g. Hichi TTL)", "typeSMAHM2": "SMA Homemanager 2.0", - "typeTIBBER": "Tibber Pulse (via Tibber Bridge)", + "typeHTTP_SML": "HTTP(S) + SML (e.g. Tibber Pulse via Tibber Bridge)", "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", "SDM": "SDM-Power Meter Parameter", "sdmaddress": "Modbus Address", - "HTTP": "HTTP(S) + Json - General configuration", - "httpIndividualRequests": "Individual HTTP requests per phase", + "HTTP": "HTTP(S) + JSON - General configuration", + "httpIndividualRequests": "Individual HTTP requests per value", "urlExamplesHeading": "URL Examples", "jsonPathExamplesHeading": "JSON Path Examples", "jsonPathExamplesExplanation": "The following paths each find the value '123.4' in the respective example JSON.", - "httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}", - "httpEnabled": "Phase enabled", - "httpUrl": "URL", - "httpUrlDescription": "URL must start with http:// or https://. Some characters like spaces and = have to be encoded with URL encoding (%xx). Warning: SSL server certificate check is not implemented (MITM attacks are possible)!", - "httpAuthorization": "Authorization Type", - "httpUsername": "Username", - "httpPassword": "Password", - "httpHeaderKey": "Optional: HTTP request header - Key", - "httpHeaderKeyDescription": "A custom HTTP request header can be defined. Might be useful if you have to send something like a custom Authorization header.", - "httpHeaderValue": "Optional: HTTP request header - Value", + "httpValue": "Configuration for value {valueNumber}", + "httpEnabled": "Value enabled", "httpJsonPath": "JSON path", "httpJsonPathDescription": "Application specific JSON path to find the power value in the HTTP(S) response, e.g., 'power/total/watts' or simply 'total'.", "httpUnit": "Unit", "httpSignInverted": "Change Sign", "httpSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", - "httpTimeout": "Timeout", - "testHttpRequest": "Run test", - "milliSeconds": "ms", - "TIBBER": "Tibber Pulse (via Tibber Bridge) - Configuration" + "testHttpJsonRequest": "Test configuration (send HTTP(S) request)", + "testHttpSmlRequest": "Test configuration (send HTTP(S) request)", + "HTTP_SML": "Configuration" + }, + "httprequestsettings": { + "url": "URL", + "urlDescription": "URL must start with 'http://' or 'https://'. Characters like spaces and '=' have to be URL-encoded (%xx). Warning: SSL server certificate check is not implemented (MITM attacks are possible)!", + "authorization": "Authorization Type", + "authTypeNone": "None", + "authTypeBasic": "Basic", + "authTypeDigest": "Digest", + "username": "Username", + "password": "Password", + "headerKey": "HTTP Header - Key", + "headerKeyDescription": "Optional. A custom HTTP header key-value pair can be defined. Useful, e.g., to send an (additional) authentication token.", + "headerValue": "HTTP Header - Value", + "timeout": "Timeout", + "milliSeconds": "ms" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Settings", diff --git a/webapp/src/types/HttpRequestConfig.ts b/webapp/src/types/HttpRequestConfig.ts new file mode 100644 index 00000000..46592457 --- /dev/null +++ b/webapp/src/types/HttpRequestConfig.ts @@ -0,0 +1,9 @@ +export interface HttpRequestConfig { + url: string; + auth_type: number; + username: string; + password: string; + header_key: string; + header_value: string; + timeout: number; +} diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index 1675d8d3..fe9fa09a 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -1,23 +1,16 @@ -export interface PowerMeterHttpPhaseConfig { +import type { HttpRequestConfig } from '@/types/HttpRequestConfig'; + +export interface PowerMeterHttpJsonConfig { index: number; + http_request: HttpRequestConfig; enabled: boolean; - url: string; - auth_type: number; - username: string; - password: string; - header_key: string; - header_value: string; json_path: string; - timeout: number; unit: number; sign_inverted: boolean; } -export interface PowerMeterTibberConfig { - url: string; - username: string; - password: string; - timeout: number; +export interface PowerMeterHttpSmlConfig { + http_request: HttpRequestConfig; } export interface PowerMeterConfig { @@ -30,6 +23,6 @@ export interface PowerMeterConfig { mqtt_topic_powermeter_3: string; sdmaddress: number; http_individual_requests: boolean; - http_phases: Array; - tibber: PowerMeterTibberConfig; + http_json: Array; + http_sml: PowerMeterHttpSmlConfig; } diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index fd600069..a7dbae77 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -114,66 +114,23 @@
-
-
- +
-
- -
- -
-
-
- - - -
- - - - - - -
+
- +
-
- - {{ testHttpRequestAlert[index].message }} + + {{ testHttpJsonRequestAlert[index].message }}
- - - - - - - - +
-
- - {{ testTibberRequestAlert.message }} + + {{ testHttpSmlRequestAlert.message }}
@@ -263,8 +201,9 @@ import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; +import HttpRequestSettings from '@/components/HttpRequestSettings.vue'; import { handleResponse, authHeader } from '@/utils/authentication'; -import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig"; +import type { PowerMeterHttpJsonConfig, PowerMeterConfig } from "@/types/PowerMeterConfig"; export default defineComponent({ components: { @@ -272,6 +211,7 @@ export default defineComponent({ BootstrapAlert, CardElement, FormFooter, + HttpRequestSettings, InputElement }, data() { @@ -282,21 +222,21 @@ export default defineComponent({ { key: 0, value: this.$t('powermeteradmin.typeMQTT') }, { key: 1, value: this.$t('powermeteradmin.typeSDM1ph') }, { key: 2, value: this.$t('powermeteradmin.typeSDM3ph') }, - { key: 3, value: this.$t('powermeteradmin.typeHTTP') }, + { key: 3, value: this.$t('powermeteradmin.typeHTTP_JSON') }, { key: 4, value: this.$t('powermeteradmin.typeSML') }, { key: 5, value: this.$t('powermeteradmin.typeSMAHM2') }, - { key: 6, value: this.$t('powermeteradmin.typeTIBBER') }, + { key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') }, ], - powerMeterAuthList: [ - { key: 0, value: "None" }, - { key: 1, value: "Basic" }, - { key: 2, value: "Digest" }, + unitTypeList: [ + { key: 1, value: "mW" }, + { key: 0, value: "W" }, + { key: 2, value: "kW" }, ], alertMessage: "", alertType: "info", showAlert: false, - testHttpRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[], - testTibberRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; } + testHttpJsonRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[], + testHttpSmlRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; } }; }, created() { @@ -311,8 +251,8 @@ export default defineComponent({ this.powerMeterConfigList = data; this.dataLoading = false; - for (let i = 0; i < this.powerMeterConfigList.http_phases.length; i++) { - this.testHttpRequestAlert.push({ + for (let i = 0; i < this.powerMeterConfigList.http_json.length; i++) { + this.testHttpJsonRequestAlert.push({ message: "", type: "", show: false, @@ -341,27 +281,27 @@ export default defineComponent({ } ); }, - testHttpRequest(index: number) { - let phaseConfig:PowerMeterHttpPhaseConfig; + testHttpJsonRequest(index: number) { + let valueConfig:PowerMeterHttpJsonConfig; if (this.powerMeterConfigList.http_individual_requests) { - phaseConfig = this.powerMeterConfigList.http_phases[index]; + valueConfig = this.powerMeterConfigList.http_json[index]; } else { - phaseConfig = { ...this.powerMeterConfigList.http_phases[0] }; - phaseConfig.index = this.powerMeterConfigList.http_phases[index].index; - phaseConfig.json_path = this.powerMeterConfigList.http_phases[index].json_path; + valueConfig = { ...this.powerMeterConfigList.http_json[0] }; + valueConfig.index = this.powerMeterConfigList.http_json[index].index; + valueConfig.json_path = this.powerMeterConfigList.http_json[index].json_path; } - this.testHttpRequestAlert[index] = { + this.testHttpJsonRequestAlert[index] = { message: "Sending HTTP request...", type: "info", show: true, }; const formData = new FormData(); - formData.append("data", JSON.stringify(phaseConfig)); + formData.append("data", JSON.stringify(valueConfig)); - fetch("/api/powermeter/testhttprequest", { + fetch("/api/powermeter/testhttpjsonrequest", { method: "POST", headers: authHeader(), body: formData, @@ -369,7 +309,7 @@ export default defineComponent({ .then((response) => handleResponse(response, this.$emitter, this.$router)) .then( (response) => { - this.testHttpRequestAlert[index] = { + this.testHttpJsonRequestAlert[index] = { message: response.message, type: response.type, show: true, @@ -377,17 +317,17 @@ export default defineComponent({ } ) }, - testTibberRequest() { - this.testTibberRequestAlert = { - message: "Sending Tibber request...", + testHttpSmlRequest() { + this.testHttpSmlRequestAlert = { + message: "Sending HTTP SML request...", type: "info", show: true, }; const formData = new FormData(); - formData.append("data", JSON.stringify(this.powerMeterConfigList.tibber)); + formData.append("data", JSON.stringify(this.powerMeterConfigList.http_sml)); - fetch("/api/powermeter/testtibberrequest", { + fetch("/api/powermeter/testhttpsmlrequest", { method: "POST", headers: authHeader(), body: formData, @@ -395,7 +335,7 @@ export default defineComponent({ .then((response) => handleResponse(response, this.$emitter, this.$router)) .then( (response) => { - this.testTibberRequestAlert = { + this.testHttpSmlRequestAlert = { message: response.message, type: response.type, show: true, From 6da90de765356e13751b2a3f60febcc7f3034d65 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sun, 12 May 2024 22:12:15 +0200 Subject: [PATCH 16/50] remove extraction of basic auth params from URL the extractUrlComponents method did extract username and password from the URL and encoded it for basic authentication. however, the respective result string was never used. we only perform basic authentication if the auth type is "basic" and if username and password were supplied through the respective inputs. --- include/PowerMeterHttpJson.h | 2 +- src/PowerMeterHttpJson.cpp | 10 ++++------ webapp/src/views/PowerMeterAdminView.vue | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index 50bca8c5..e50fb78f 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -33,7 +33,7 @@ private: String httpResponse; bool httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpJsonConfig const& config); - bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); + bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t); String extractParam(String& authReq, const String& param, const char delimit); String getcNonce(const int len); String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter); diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index 785775cc..6faeb71c 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -80,9 +80,8 @@ bool PowerMeterHttpJson::queryValue(int phase, PowerMeterHttpJsonConfig const& c String protocol; String host; String uri; - String base64Authorization; uint16_t port; - extractUrlComponents(config.HttpRequest.Url, protocol, host, uri, port, base64Authorization); + extractUrlComponents(config.HttpRequest.Url, protocol, host, uri, port); IPAddress ipaddr((uint32_t)0); //first check if "host" is already an IP adress @@ -267,7 +266,7 @@ bool PowerMeterHttpJson::tryGetFloatValueForPhase(int phase, String jsonPath, Un } //extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 -bool PowerMeterHttpJson::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) +bool PowerMeterHttpJson::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port) { // check for : (http: or https: int index = url.indexOf(':'); @@ -295,10 +294,9 @@ bool PowerMeterHttpJson::extractUrlComponents(String url, String& _protocol, Str // get Authorization index = host.indexOf('@'); if(index >= 0) { - // auth info - String auth = host.substring(0, index); + // basic authentication is only supported through setting username + // and password using the respective inputs, not embedded into the URL host.remove(0, index + 1); // remove auth part including @ - _base64Authorization = base64::encode(auth); } // get port diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index a7dbae77..9c787753 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -98,8 +98,8 @@ + + + +
+ +
+ + + {{ testHttpJsonRequestAlert.message }} + +
@@ -203,7 +209,7 @@ import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import HttpRequestSettings from '@/components/HttpRequestSettings.vue'; import { handleResponse, authHeader } from '@/utils/authentication'; -import type { PowerMeterHttpJsonConfig, PowerMeterConfig } from "@/types/PowerMeterConfig"; +import type { PowerMeterConfig } from "@/types/PowerMeterConfig"; export default defineComponent({ components: { @@ -235,7 +241,7 @@ export default defineComponent({ alertMessage: "", alertType: "info", showAlert: false, - testHttpJsonRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[], + testHttpJsonRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; }, testHttpSmlRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; } }; }, @@ -250,14 +256,6 @@ export default defineComponent({ .then((data) => { this.powerMeterConfigList = data; this.dataLoading = false; - - for (let i = 0; i < this.powerMeterConfigList.http_json.length; i++) { - this.testHttpJsonRequestAlert.push({ - message: "", - type: "", - show: false, - }); - } }); }, savePowerMeterConfig(e: Event) { @@ -281,25 +279,15 @@ export default defineComponent({ } ); }, - testHttpJsonRequest(index: number) { - let valueConfig:PowerMeterHttpJsonConfig; - - if (this.powerMeterConfigList.http_individual_requests) { - valueConfig = this.powerMeterConfigList.http_json[index]; - } else { - valueConfig = { ...this.powerMeterConfigList.http_json[0] }; - valueConfig.index = this.powerMeterConfigList.http_json[index].index; - valueConfig.json_path = this.powerMeterConfigList.http_json[index].json_path; - } - - this.testHttpJsonRequestAlert[index] = { + testHttpJsonRequest() { + this.testHttpJsonRequestAlert = { message: "Sending HTTP request...", type: "info", show: true, }; const formData = new FormData(); - formData.append("data", JSON.stringify(valueConfig)); + formData.append("data", JSON.stringify(this.powerMeterConfigList)); fetch("/api/powermeter/testhttpjsonrequest", { method: "POST", @@ -309,7 +297,7 @@ export default defineComponent({ .then((response) => handleResponse(response, this.$emitter, this.$router)) .then( (response) => { - this.testHttpJsonRequestAlert[index] = { + this.testHttpJsonRequestAlert = { message: response.message, type: response.type, show: true, From e1778eba76c3d4fbc85a578678f0ce89143b2d3b Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 17 May 2024 22:00:37 +0200 Subject: [PATCH 20/50] powermeter refactor: use HttpGetter in HTTP SML implementation --- include/PowerMeterHttpSml.h | 21 +-- src/PowerMeterHttpSml.cpp | 189 +++++------------------ src/WebApi_powermeter.cpp | 27 ++-- webapp/src/locales/de.json | 3 +- webapp/src/locales/en.json | 3 +- webapp/src/views/PowerMeterAdminView.vue | 28 ++-- 6 files changed, 76 insertions(+), 195 deletions(-) diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h index 73bc882c..b351674b 100644 --- a/include/PowerMeterHttpSml.h +++ b/include/PowerMeterHttpSml.h @@ -4,27 +4,20 @@ #include #include #include -#include -#include "Configuration.h" +#include "HttpGetter.h" #include "PowerMeterSml.h" class PowerMeterHttpSml : public PowerMeterSml { public: - ~PowerMeterHttpSml(); - - bool init() final { return true; } + bool init() final; void loop() final; - bool updateValues(); - char tibberPowerMeterError[256]; - bool query(HttpRequestConfig const& config); + + // returns an empty string on success, + // returns an error message otherwise. + String poll(); private: uint32_t _lastPoll = 0; - std::unique_ptr wifiClient; - std::unique_ptr httpClient; - String httpResponse; - bool httpRequest(const String& host, uint16_t port, const String& uri, bool https, HttpRequestConfig const& config); - bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); - void prepareRequest(uint32_t timeout); + std::unique_ptr _upHttpGetter; }; diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index 17750bba..ecc1ef5f 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -6,13 +6,20 @@ #include #include -PowerMeterHttpSml::~PowerMeterHttpSml() +bool PowerMeterHttpSml::init() { - // the wifiClient instance must live longer than the httpClient instance, - // as the httpClient holds a pointer to the wifiClient and uses it in its - // destructor. - httpClient.reset(); - wifiClient.reset(); + auto const& config = Configuration.get(); + + _upHttpGetter = std::make_unique(config.PowerMeter.HttpSml.HttpRequest); + + if (_upHttpGetter->init()) { return true; } + + MessageOutput.printf("[PowerMeterHttpSml] Initializing HTTP getter failed:\r\n"); + MessageOutput.printf("[PowerMeterHttpSml] %s\r\n", _upHttpGetter->getErrorText()); + + _upHttpGetter = nullptr; + + return false; } void PowerMeterHttpSml::loop() @@ -24,160 +31,34 @@ void PowerMeterHttpSml::loop() _lastPoll = millis(); - if (!query(config.PowerMeter.HttpSml.HttpRequest)) { - MessageOutput.printf("[PowerMeterHttpSml] Getting the power value failed.\r\n"); - MessageOutput.printf("%s\r\n", tibberPowerMeterError); + auto res = poll(); + if (!res.isEmpty()) { + MessageOutput.printf("[PowerMeterHttpJson] %s\r\n", res.c_str()); + return; } + + gotUpdate(); } -bool PowerMeterHttpSml::query(HttpRequestConfig const& config) +String PowerMeterHttpSml::poll() { - //hostByName in WiFiGeneric fails to resolve local names. issue described in - //https://github.com/espressif/arduino-esp32/issues/3822 - //and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 - //in conclusion: we cannot rely on httpClient->begin(*wifiClient, url) to resolve IP adresses. - //have to do it manually here. Feels Hacky... - String protocol; - String host; - String uri; - String base64Authorization; - uint16_t port; - extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization); - - IPAddress ipaddr((uint32_t)0); - //first check if "host" is already an IP adress - if (!ipaddr.fromString(host)) - { - //"host"" is not an IP address so try to resolve the IP adress - //first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() will spam the console if done the otherway around. - const bool mdnsEnabled = Configuration.get().Mdns.Enabled; - if (!mdnsEnabled) { - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str()); - //ensure we try resolving via DNS even if mDNS is disabled - if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); - } - } - else - { - ipaddr = MDNS.queryHost(host); - if (ipaddr == INADDR_NONE){ - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str()); - //when we cannot find local server via mDNS, try resolving via DNS - if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); - } - } - } + if (!_upHttpGetter) { + return "Initialization of HTTP request failed"; } - bool https = protocol == "https"; - if (https) { - auto secureWifiClient = std::make_unique(); - secureWifiClient->setInsecure(); - wifiClient = std::move(secureWifiClient); - } else { - wifiClient = std::make_unique(); + auto res = _upHttpGetter->performGetRequest(); + if (!res) { + return _upHttpGetter->getErrorText(); } - return httpRequest(ipaddr.toString(), port, uri, https, config); -} - -bool PowerMeterHttpSml::httpRequest(const String& host, uint16_t port, const String& uri, bool https, HttpRequestConfig const& config) -{ - if (!httpClient) { httpClient = std::make_unique(); } - - if(!httpClient->begin(*wifiClient, host, port, uri, https)){ - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("httpClient->begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); - return false; - } - - prepareRequest(config.Timeout); - - String authString = config.Username; - authString += ":"; - authString += config.Password; - String auth = "Basic "; - auth.concat(base64::encode(authString)); - httpClient->addHeader("Authorization", auth); - - int httpCode = httpClient->GET(); - - if (httpCode <= 0) { - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("HTTP Error %s"), httpClient->errorToString(httpCode).c_str()); - return false; - } - - if (httpCode != HTTP_CODE_OK) { - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode); - return false; - } - - auto& stream = httpClient->getStream(); - while (stream.available()) { - processSmlByte(stream.read()); - } - httpClient->end(); - - return true; -} - -//extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 -bool PowerMeterHttpSml::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) -{ - // check for : (http: or https: - int index = url.indexOf(':'); - if(index < 0) { - snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("failed to parse protocol")); - return false; - } - - _protocol = url.substring(0, index); - - //initialize port to default values for http or https. - //port will be overwritten below in case port is explicitly defined - _port = (_protocol == "https" ? 443 : 80); - - url.remove(0, (index + 3)); // remove http:// or https:// - - index = url.indexOf('/'); - if (index == -1) { - index = url.length(); - url += '/'; - } - String host = url.substring(0, index); - url.remove(0, index); // remove host part - - // get Authorization - index = host.indexOf('@'); - if(index >= 0) { - // auth info - String auth = host.substring(0, index); - host.remove(0, index + 1); // remove auth part including @ - _base64Authorization = base64::encode(auth); - } - - // get port - index = host.indexOf(':'); - String the_host; - if(index >= 0) { - the_host = host.substring(0, index); // hostname - host.remove(0, (index + 1)); // remove hostname + : - _port = host.toInt(); // get port - } else { - the_host = host; - } - - _host = the_host; - _uri = url; - return true; -} - -void PowerMeterHttpSml::prepareRequest(uint32_t timeout) { - httpClient->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - httpClient->setUserAgent("OpenDTU-OnBattery"); - httpClient->setConnectTimeout(timeout); - httpClient->setTimeout(timeout); - httpClient->addHeader("Content-Type", "application/json"); - httpClient->addHeader("Accept", "application/json"); + auto pStream = res.getStream(); + if (!pStream) { + return "Programmer error: HTTP request yields no stream"; + } + + while (pStream->available()) { + processSmlByte(pStream->read()); + } + + return ""; } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 0178eb90..ac9a4438 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -196,6 +196,7 @@ void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request auto powerMeterConfig = std::make_unique(); powerMeterConfig->HttpIndividualRequests = root["http_individual_requests"].as(); + powerMeterConfig->VerboseLogging = true; JsonArray httpJson = root["http_json"]; for (uint8_t i = 0; i < httpJson.size(); i++) { Configuration.deserializePowerMeterHttpJsonConfig(httpJson[i].as(), @@ -240,25 +241,23 @@ void WebApiPowerMeterClass::onTestHttpSmlRequest(AsyncWebServerRequest* request) auto& retMsg = asyncJsonResponse->getRoot(); - if (!root.containsKey("url") || !root.containsKey("username") || !root.containsKey("password") - || !root.containsKey("timeout")) { - retMsg["message"] = "Missing fields!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); - return; - } - - char response[256]; - PowerMeterHttpSmlConfig httpSmlConfig; - Configuration.deserializePowerMeterHttpSmlConfig(root.as(), httpSmlConfig); + auto powerMeterConfig = std::make_unique(); + Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as(), + powerMeterConfig->HttpSml); + powerMeterConfig->VerboseLogging = true; + auto backup = std::make_unique(Configuration.get().PowerMeter); + Configuration.get().PowerMeter = *powerMeterConfig; auto upMeter = std::make_unique(); - if (upMeter->query(httpSmlConfig.HttpRequest)) { + upMeter->init(); + auto res = upMeter->poll(); + Configuration.get().PowerMeter = *backup; + if (res.isEmpty()) { retMsg["type"] = "success"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal()); + snprintf(response, sizeof(response), "Result: %5.2fW", upMeter->getPowerTotal()); } else { - snprintf_P(response, sizeof(response), "%s", upMeter->tibberPowerMeterError); + snprintf(response, sizeof(response), "%s", res.c_str()); } retMsg["message"] = response; diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index e7e3317f..e1d604aa 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -583,7 +583,8 @@ "httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", "testHttpJsonHeader": "Konfiguration testen", "testHttpJsonRequest": "HTTP(S)-Anfrage(n) senden und Antwort(en) verarbeiten", - "testHttpSmlRequest": "Konfiguration testen (HTTP(S)-Anfrage senden)", + "testHttpSmlHeader": "Konfiguration testen", + "testHttpSmlRequest": "HTTP(S)-Anfrage senden und Antwort verarbeiten", "HTTP_SML": "HTTP(S) + SML - Konfiguration" }, "httprequestsettings": { diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 218eef94..1fe86f36 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -585,7 +585,8 @@ "httpSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", "testHttpJsonHeader": "Test Configuration", "testHttpJsonRequest": "Send HTTP(S) request(s) and process response(s)", - "testHttpSmlRequest": "Test configuration (send HTTP(S) request)", + "testHttpSmlHeader": "Test Configuration", + "testHttpSmlRequest": "Send HTTP(S) request and process response", "HTTP_SML": "Configuration" }, "httprequestsettings": { diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index fd2934f8..7575afc3 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -180,16 +180,22 @@ add-space> + -
- -
+ - - {{ testHttpSmlRequestAlert.message }} - +
+ +
+ + + {{ testHttpSmlRequestAlert.message }} +
@@ -281,7 +287,7 @@ export default defineComponent({ }, testHttpJsonRequest() { this.testHttpJsonRequestAlert = { - message: "Sending HTTP request...", + message: "Triggering HTTP request...", type: "info", show: true, }; @@ -307,13 +313,13 @@ export default defineComponent({ }, testHttpSmlRequest() { this.testHttpSmlRequestAlert = { - message: "Sending HTTP SML request...", + message: "Triggering HTTP request...", type: "info", show: true, }; const formData = new FormData(); - formData.append("data", JSON.stringify(this.powerMeterConfigList.http_sml)); + formData.append("data", JSON.stringify(this.powerMeterConfigList)); fetch("/api/powermeter/testhttpsmlrequest", { method: "POST", From 3f2d9d38faa279e2d80aab106bbab3c22464e562 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 22 May 2024 22:27:49 +0200 Subject: [PATCH 21/50] powermeter refactor: fully structure settings per provider all power meter providers now have their own configuration struct defined. a respective method to serialize and deserialize the provider config is implemented for each provider. --- include/Configuration.h | 41 ++++-- include/PowerMeterHttpJson.h | 2 +- include/defaults.h | 2 +- src/Configuration.cpp | 151 +++++++++++++++++------ src/PowerMeterHttpJson.cpp | 8 +- src/PowerMeterHttpSml.cpp | 2 +- src/PowerMeterMqtt.cpp | 6 +- src/PowerMeterSerialSdm.cpp | 4 +- src/WebApi_powermeter.cpp | 66 +++++----- webapp/src/locales/de.json | 6 +- webapp/src/locales/en.json | 6 +- webapp/src/types/PowerMeterConfig.ts | 32 +++-- webapp/src/views/PowerMeterAdminView.vue | 47 ++----- 13 files changed, 227 insertions(+), 146 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 79bdd736..1f185bef 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -37,6 +37,7 @@ #define HTTP_REQUEST_MAX_HEADER_KEY_STRLEN 64 #define HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN 256 +#define POWERMETER_MQTT_MAX_VALUES 3 #define POWERMETER_HTTP_JSON_MAX_VALUES 3 #define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256 @@ -76,7 +77,23 @@ struct HTTP_REQUEST_CONFIG_T { }; using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T; -struct POWERMETER_HTTP_JSON_CONFIG_T { +struct POWERMETER_MQTT_VALUE_T { + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; +}; +using PowerMeterMqttValue = struct POWERMETER_MQTT_VALUE_T; + +struct POWERMETER_MQTT_CONFIG_T { + PowerMeterMqttValue Values[POWERMETER_MQTT_MAX_VALUES]; +}; +using PowerMeterMqttConfig = struct POWERMETER_MQTT_CONFIG_T; + +struct POWERMETER_SERIAL_SDM_CONFIG_T { + uint32_t Address; + uint32_t PollingInterval; +}; +using PowerMeterSerialSdmConfig = struct POWERMETER_SERIAL_SDM_CONFIG_T; + +struct POWERMETER_HTTP_JSON_VALUE_T { HttpRequestConfig HttpRequest; bool Enabled; char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; @@ -86,9 +103,17 @@ struct POWERMETER_HTTP_JSON_CONFIG_T { bool SignInverted; }; +using PowerMeterHttpJsonValue = struct POWERMETER_HTTP_JSON_VALUE_T; + +struct POWERMETER_HTTP_JSON_CONFIG_T { + uint32_t PollingInterval; + bool IndividualRequests; + PowerMeterHttpJsonValue Values[POWERMETER_HTTP_JSON_MAX_VALUES]; +}; using PowerMeterHttpJsonConfig = struct POWERMETER_HTTP_JSON_CONFIG_T; struct POWERMETER_HTTP_SML_CONFIG_T { + uint32_t PollingInterval; HttpRequestConfig HttpRequest; }; using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; @@ -205,14 +230,10 @@ struct CONFIG_T { struct PowerMeterConfig { bool Enabled; bool VerboseLogging; - uint32_t Interval; uint32_t Source; - char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; - uint32_t SdmAddress; - bool HttpIndividualRequests; - PowerMeterHttpJsonConfig HttpJson[POWERMETER_HTTP_JSON_MAX_VALUES]; + PowerMeterMqttConfig Mqtt; + PowerMeterSerialSdmConfig SerialSdm; + PowerMeterHttpJsonConfig HttpJson; PowerMeterHttpSmlConfig HttpSml; } PowerMeter; @@ -288,10 +309,14 @@ public: void deleteInverterById(const uint8_t id); static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target); + static void serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target); + static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target); static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target); static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target); + static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target); + static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target); static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target); }; diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index 7f81ff37..8810ca05 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -10,7 +10,7 @@ #include "PowerMeterProvider.h" using Auth_t = HttpRequestConfig::Auth; -using Unit_t = PowerMeterHttpJsonConfig::Unit; +using Unit_t = PowerMeterHttpJsonValue::Unit; class PowerMeterHttpJson : public PowerMeterProvider { public: diff --git a/include/defaults.h b/include/defaults.h index 6b191cbd..0f83eaaf 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -115,7 +115,7 @@ #define VEDIRECT_UPDATESONLY true #define POWERMETER_ENABLED false -#define POWERMETER_INTERVAL 10 +#define POWERMETER_POLLING_INTERVAL 10 #define POWERMETER_SOURCE 2 #define POWERMETER_SDMADDRESS 1 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index ffe96459..e2067fb1 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -28,18 +28,45 @@ void ConfigurationClass::serializeHttpRequestConfig(HttpRequestConfig const& sou target_http_config["timeout"] = source.Timeout; } +void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target) +{ + JsonArray values = target["values"].to(); + for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { + JsonObject t = values.add(); + PowerMeterMqttValue const& s = source.Values[i]; + + t["topic"] = s.Topic; + } +} + +void ConfigurationClass::serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target) +{ + target["address"] = source.Address; + target["polling_interval"] = source.PollingInterval; +} + void ConfigurationClass::serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target) { - serializeHttpRequestConfig(source.HttpRequest, target); + target["polling_interval"] = source.PollingInterval; + target["individual_requests"] = source.IndividualRequests; - target["enabled"] = source.Enabled; - target["json_path"] = source.JsonPath; - target["unit"] = source.PowerUnit; - target["sign_inverted"] = source.SignInverted; + JsonArray values = target["values"].to(); + for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { + JsonObject t = values.add(); + PowerMeterHttpJsonValue const& s = source.Values[i]; + + serializeHttpRequestConfig(s.HttpRequest, t); + + t["enabled"] = s.Enabled; + t["json_path"] = s.JsonPath; + t["unit"] = s.PowerUnit; + t["sign_inverted"] = s.SignInverted; + } } void ConfigurationClass::serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target) { + target["polling_interval"] = source.PollingInterval; serializeHttpRequestConfig(source.HttpRequest, target); } @@ -176,24 +203,20 @@ bool ConfigurationClass::write() JsonObject powermeter = doc["powermeter"].to(); powermeter["enabled"] = config.PowerMeter.Enabled; powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging; - powermeter["interval"] = config.PowerMeter.Interval; powermeter["source"] = config.PowerMeter.Source; - powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1; - powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2; - powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3; - powermeter["sdmaddress"] = config.PowerMeter.SdmAddress; - powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; + + JsonObject powermeter_mqtt = powermeter["mqtt"].to(); + serializePowerMeterMqttConfig(config.PowerMeter.Mqtt, powermeter_mqtt); + + JsonObject powermeter_serial_sdm = powermeter["serial_sdm"].to(); + serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, powermeter_serial_sdm); + + JsonObject powermeter_http_json = powermeter["http_json"].to(); + serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, powermeter_http_json); JsonObject powermeter_http_sml = powermeter["http_sml"].to(); serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml); - JsonArray powermeter_http_json = powermeter["http_json"].to(); - for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - JsonObject powermeter_json_config = powermeter_http_json.add(); - serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson[i], - powermeter_json_config); - } - JsonObject powerlimiter = doc["powerlimiter"].to(); powerlimiter["enabled"] = config.PowerLimiter.Enabled; powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging; @@ -263,8 +286,8 @@ void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, { JsonObject source_http_config = source["http_request"]; - // http request parameters of HTTP/JSON power meter were - // previously stored alongside other settings + // http request parameters of HTTP/JSON power meter were previously stored + // alongside other settings. TODO(schlimmchen): remove in early 2025. if (source_http_config.isNull()) { source_http_config = source; } strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url)); @@ -276,18 +299,43 @@ void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, target.Timeout = source_http_config["timeout"] | HTTP_REQUEST_TIMEOUT_MS; } +void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target) +{ + JsonArray s = source["values"].as(); + for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { + PowerMeterMqttValue& t = target.Values[i]; + strlcpy(t.Topic, s[i]["topic"] | "", sizeof(t.Topic)); + } +} + +void ConfigurationClass::deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target) +{ + target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL; + target.Address = source["address"] | POWERMETER_SDMADDRESS; +} + void ConfigurationClass::deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target) { - deserializeHttpRequestConfig(source, target.HttpRequest); + target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL; + target.IndividualRequests = source["individual_requests"] | false; - target.Enabled = source["enabled"] | false; - strlcpy(target.JsonPath, source["json_path"] | "", sizeof(target.JsonPath)); - target.PowerUnit = source["unit"] | PowerMeterHttpJsonConfig::Unit::Watts; - target.SignInverted = source["sign_inverted"] | false; + JsonArray values = source["values"].as(); + for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { + PowerMeterHttpJsonValue& t = target.Values[i]; + JsonObject s = values[i]; + + deserializeHttpRequestConfig(s, t.HttpRequest); + + t.Enabled = s["enabled"] | false; + strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath)); + t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts; + t.SignInverted = s["sign_inverted"] | false; + } } void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target) { + target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL; deserializeHttpRequestConfig(source, target.HttpRequest); } @@ -461,24 +509,51 @@ bool ConfigurationClass::read() JsonObject powermeter = doc["powermeter"]; config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED; config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING; - config.PowerMeter.Interval = powermeter["interval"] | POWERMETER_INTERVAL; config.PowerMeter.Source = powermeter["source"] | POWERMETER_SOURCE; - strlcpy(config.PowerMeter.MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter1)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter2)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter3)); - config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; - config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false; + + deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt); + + // process settings from legacy config if they are present + // TODO(schlimmchen): remove in early 2025. + if (!powermeter["mqtt_topic_powermeter_1"].isNull()) { + auto& values = config.PowerMeter.Mqtt.Values; + strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic)); + strlcpy(values[1].Topic, powermeter["mqtt_topic_powermeter_2"], sizeof(values[1].Topic)); + strlcpy(values[2].Topic, powermeter["mqtt_topic_powermeter_3"], sizeof(values[2].Topic)); + } + + deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm); + + // process settings from legacy config if they are present + // TODO(schlimmchen): remove in early 2025. + if (!powermeter["sdmaddress"].isNull()) { + config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"]; + } + + JsonObject powermeter_http_json = powermeter["http_json"]; + deserializePowerMeterHttpJsonConfig(powermeter_http_json, config.PowerMeter.HttpJson); JsonObject powermeter_sml = powermeter["http_sml"]; deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml); - JsonArray powermeter_http_json = powermeter["http_json"]; - if (powermeter_http_json.isNull()) { - powermeter_http_json = powermeter["http_phases"]; // http_phases is a legacy key - } - for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - JsonObject powermeter_json_config = powermeter_http_json[i].as(); - deserializePowerMeterHttpJsonConfig(powermeter_json_config, config.PowerMeter.HttpJson[i]); + // process settings from legacy config if they are present + // TODO(schlimmchen): remove in early 2025. + if (!powermeter["http_phases"].isNull()) { + auto& target = config.PowerMeter.HttpJson; + + for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { + PowerMeterHttpJsonValue& t = target.Values[i]; + JsonObject s = powermeter["http_phases"][i]; + + deserializeHttpRequestConfig(s, t.HttpRequest); + + t.Enabled = s["enabled"] | false; + strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath)); + t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts; + t.SignInverted = s["sign_inverted"] | false; + } + + target.IndividualRequests = powermeter["http_individual_requests"] | false; } JsonObject powerlimiter = doc["powerlimiter"]; diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index b1b530d7..621670d3 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -14,11 +14,11 @@ bool PowerMeterHttpJson::init() auto const& config = Configuration.get(); for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - auto const& valueConfig = config.PowerMeter.HttpJson[i]; + auto const& valueConfig = config.PowerMeter.HttpJson.Values[i]; _httpGetters[i] = nullptr; - if (i == 0 || (config.PowerMeter.HttpIndividualRequests && valueConfig.Enabled)) { + if (i == 0 || (config.PowerMeter.HttpJson.IndividualRequests && valueConfig.Enabled)) { _httpGetters[i] = std::make_unique(valueConfig.HttpRequest); } @@ -41,7 +41,7 @@ bool PowerMeterHttpJson::init() void PowerMeterHttpJson::loop() { auto const& config = Configuration.get(); - if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + if ((millis() - _lastPoll) < (config.PowerMeter.HttpJson.PollingInterval * 1000)) { return; } @@ -68,7 +68,7 @@ PowerMeterHttpJson::poll_result_t PowerMeterHttpJson::poll() }; for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - auto const& cfg = Configuration.get().PowerMeter.HttpJson[i]; + auto const& cfg = Configuration.get().PowerMeter.HttpJson.Values[i]; if (!cfg.Enabled) { cache[i] = 0.0; diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index ecc1ef5f..e9a38389 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -25,7 +25,7 @@ bool PowerMeterHttpSml::init() void PowerMeterHttpSml::loop() { auto const& config = Configuration.get(); - if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + if ((millis() - _lastPoll) < (config.PowerMeter.HttpSml.PollingInterval * 1000)) { return; } diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp index 48f6e459..9a4654b7 100644 --- a/src/PowerMeterMqtt.cpp +++ b/src/PowerMeterMqtt.cpp @@ -19,9 +19,9 @@ bool PowerMeterMqtt::init() }; auto const& config = Configuration.get(); - subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerValueOne); - subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerValueTwo); - subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerValueThree); + subscribe(config.PowerMeter.Mqtt.Values[0].Topic, &_powerValueOne); + subscribe(config.PowerMeter.Mqtt.Values[1].Topic, &_powerValueTwo); + subscribe(config.PowerMeter.Mqtt.Values[2].Topic, &_powerValueThree); return _mqttSubscriptions.size() > 0; } diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp index 1612bcbe..a425f986 100644 --- a/src/PowerMeterSerialSdm.cpp +++ b/src/PowerMeterSerialSdm.cpp @@ -63,11 +63,11 @@ void PowerMeterSerialSdm::loop() auto const& config = Configuration.get(); - if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + if ((millis() - _lastPoll) < (config.PowerMeter.SerialSdm.PollingInterval * 1000)) { return; } - uint8_t addr = config.PowerMeter.SdmAddress; + uint8_t addr = config.PowerMeter.SerialSdm.Address; // reading takes a "very long" time as each readVal() is a synchronous // exchange of serial messages. cache the values and write later to diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index ac9a4438..0fea84ed 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -39,23 +39,19 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) root["enabled"] = config.PowerMeter.Enabled; root["verbose_logging"] = config.PowerMeter.VerboseLogging; root["source"] = config.PowerMeter.Source; - root["interval"] = config.PowerMeter.Interval; - root["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1; - root["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2; - root["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3; - root["sdmaddress"] = config.PowerMeter.SdmAddress; - root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; + + auto mqtt = root["mqtt"].to(); + Configuration.serializePowerMeterMqttConfig(config.PowerMeter.Mqtt, mqtt); + + auto serialSdm = root["serial_sdm"].to(); + Configuration.serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, serialSdm); + + auto httpJson = root["http_json"].to(); + Configuration.serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, httpJson); auto httpSml = root["http_sml"].to(); Configuration.serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, httpSml); - auto httpJson = root["http_json"].to(); - for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - auto valueConfig = httpJson.add(); - valueConfig["index"] = i + 1; - Configuration.serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson[i], valueConfig); - } - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -119,15 +115,16 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) }; if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_JSON) { - JsonArray httpJson = root["http_json"]; - for (uint8_t i = 0; i < httpJson.size(); i++) { - JsonObject valueConfig = httpJson[i].as(); + JsonObject httpJson = root["http_json"]; + JsonArray valueConfigs = httpJson["values"]; + for (uint8_t i = 0; i < valueConfigs.size(); i++) { + JsonObject valueConfig = valueConfigs[i].as(); if (i > 0 && !valueConfig["enabled"].as()) { continue; } - if (i == 0 || valueConfig["http_individual_requests"].as()) { + if (i == 0 || httpJson["individual_requests"].as()) { if (!checkHttpConfig(valueConfig["http_request"].as())) { return; } @@ -154,23 +151,20 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerMeter.Enabled = root["enabled"].as(); config.PowerMeter.VerboseLogging = root["verbose_logging"].as(); config.PowerMeter.Source = root["source"].as(); - config.PowerMeter.Interval = root["interval"].as(); - strlcpy(config.PowerMeter.MqttTopicPowerMeter1, root["mqtt_topic_powermeter_1"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter1)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter2, root["mqtt_topic_powermeter_2"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter2)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter3, root["mqtt_topic_powermeter_3"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter3)); - config.PowerMeter.SdmAddress = root["sdmaddress"].as(); - config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as(); + + Configuration.deserializePowerMeterMqttConfig(root["mqtt"].as(), + config.PowerMeter.Mqtt); + + Configuration.deserializePowerMeterSerialSdmConfig(root["serial_sdm"].as(), + config.PowerMeter.SerialSdm); + + Configuration.deserializePowerMeterHttpJsonConfig(root["http_json"].as(), + config.PowerMeter.HttpJson); + config.PowerMeter.HttpJson.Values[0].Enabled = true; Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as(), config.PowerMeter.HttpSml); - JsonArray httpJson = root["http_json"]; - for (uint8_t i = 0; i < httpJson.size(); i++) { - Configuration.deserializePowerMeterHttpJsonConfig(httpJson[i].as(), - config.PowerMeter.HttpJson[i]); - } - config.PowerMeter.HttpJson[0].Enabled = true; - WebApi.writeConfig(retMsg); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -195,13 +189,11 @@ void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request char response[256]; auto powerMeterConfig = std::make_unique(); - powerMeterConfig->HttpIndividualRequests = root["http_individual_requests"].as(); + JsonObject httpJson = root["http_json"]; + powerMeterConfig->HttpJson.IndividualRequests = httpJson["individual_requests"].as(); powerMeterConfig->VerboseLogging = true; - JsonArray httpJson = root["http_json"]; - for (uint8_t i = 0; i < httpJson.size(); i++) { - Configuration.deserializePowerMeterHttpJsonConfig(httpJson[i].as(), - powerMeterConfig->HttpJson[i]); - } + Configuration.deserializePowerMeterHttpJsonConfig(httpJson, + powerMeterConfig->HttpJson); auto backup = std::make_unique(Configuration.get().PowerMeter); Configuration.get().PowerMeter = *powerMeterConfig; auto upMeter = std::make_unique(); @@ -214,7 +206,7 @@ void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request auto vals = std::get(res); auto pos = snprintf(response, sizeof(response), "Result: %5.2fW", vals[0]); for (size_t i = 1; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { - if (!powerMeterConfig->HttpJson[i].Enabled) { continue; } + if (!powerMeterConfig->HttpJson.Values[i].Enabled) { continue; } pos += snprintf(response + pos, sizeof(response) - pos, ", %5.2fW", vals[i]); } snprintf(response + pos, sizeof(response) - pos, ", Total: %5.2f", upMeter->getPowerTotal()); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index e1d604aa..25630a8d 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -556,7 +556,6 @@ "VerboseLogging": "@:base.VerboseLogging", "PowerMeterParameter": "Power Meter Parameter", "PowerMeterSource": "Stromzählertyp", - "MQTT": "MQTT Konfiguration", "typeMQTT": "MQTT", "typeSDM1ph": "SDM mit 1 Phase (SDM120/220/230)", "typeSDM3ph": "SDM mit 3 Phasen (SDM72/630)", @@ -564,9 +563,8 @@ "typeSML": "SML/OBIS via serieller Verbindung (z.B. Hichi TTL)", "typeSMAHM2": "SMA Homemanager 2.0", "typeHTTP_SML": "HTTP(S) + SML (z.B. Tibber Pulse via Tibber Bridge)", - "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", - "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", - "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", + "MqttValue": "Konfiguration für Wert {valueNumber}", + "MqttTopic": "MQTT Topic", "SDM": "SDM-Stromzähler Konfiguration", "sdmaddress": "Modbus Adresse", "HTTP_JSON": "HTTP(S) + JSON - Allgemeine Konfiguration", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 1fe86f36..8b5f7d0e 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -558,7 +558,6 @@ "VerboseLogging": "@:base.VerboseLogging", "PowerMeterParameter": "Power Meter Parameter", "PowerMeterSource": "Power Meter type", - "MQTT": "MQTT Parameter", "typeMQTT": "MQTT", "typeSDM1ph": "SDM for 1 phase (SDM120/220/230)", "typeSDM3ph": "SDM for 3 phases (SDM72/630)", @@ -566,9 +565,8 @@ "typeSML": "SML/OBIS via serial connection (e.g. Hichi TTL)", "typeSMAHM2": "SMA Homemanager 2.0", "typeHTTP_SML": "HTTP(S) + SML (e.g. Tibber Pulse via Tibber Bridge)", - "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", - "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", - "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", + "MqttValue": "Configuration for value {valueNumber}", + "MqttTopic": "MQTT topic", "SDM": "SDM-Power Meter Parameter", "sdmaddress": "Modbus Address", "HTTP": "HTTP(S) + JSON - General configuration", diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index fe9fa09a..07c4c7de 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -1,7 +1,19 @@ import type { HttpRequestConfig } from '@/types/HttpRequestConfig'; -export interface PowerMeterHttpJsonConfig { - index: number; +export interface PowerMeterMqttValue { + topic: string; +} + +export interface PowerMeterMqttConfig { + values: Array; +} + +export interface PowerMeterSerialSdmConfig { + polling_interval: number; + address: number; +}; + +export interface PowerMeterHttpJsonValue { http_request: HttpRequestConfig; enabled: boolean; json_path: string; @@ -9,7 +21,14 @@ export interface PowerMeterHttpJsonConfig { sign_inverted: boolean; } +export interface PowerMeterHttpJsonConfig { + polling_interval: number; + individual_requests: boolean; + values: Array; +} + export interface PowerMeterHttpSmlConfig { + polling_interval: number; http_request: HttpRequestConfig; } @@ -18,11 +37,8 @@ export interface PowerMeterConfig { verbose_logging: boolean; source: number; interval: number; - mqtt_topic_powermeter_1: string; - mqtt_topic_powermeter_2: string; - mqtt_topic_powermeter_3: string; - sdmaddress: number; - http_individual_requests: boolean; - http_json: Array; + mqtt: PowerMeterMqttConfig; + serial_sdm: PowerMeterSerialSdmConfig; + http_json: PowerMeterHttpJsonConfig; http_sml: PowerMeterHttpSmlConfig; } diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index 7575afc3..c389af4d 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -36,38 +36,15 @@
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- -
- -
-
- -
-
-
+
+ placeholder="1" v-model="powerMeterConfigList.serial_sdm.address" />
@@ -90,7 +67,7 @@ textVariant="text-bg-primary" add-space> @@ -114,9 +91,9 @@ - + Date: Thu, 23 May 2024 20:08:16 +0200 Subject: [PATCH 22/50] powermeter refactor: instanciate power meters with config instead of reading the main config's powermeter struct(s), the individual power meters now are instanciated using a copy of their respective config. this allows to instanciate different power meters with different configs. as a first step, this simplifies instanciating power meters for test purposes. --- include/PowerMeterHttpJson.h | 5 +++++ include/PowerMeterHttpSml.h | 6 ++++++ include/PowerMeterMqtt.h | 6 ++++++ include/PowerMeterSerialSdm.h | 13 +++++++++++++ src/PowerMeter.cpp | 18 +++++++++++------- src/PowerMeterHttpJson.cpp | 12 ++++-------- src/PowerMeterHttpSml.cpp | 8 ++------ src/PowerMeterMqtt.cpp | 8 +++----- src/PowerMeterSerialSdm.cpp | 9 +++------ src/WebApi_powermeter.cpp | 28 +++++++++------------------- 10 files changed, 62 insertions(+), 51 deletions(-) diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index 8810ca05..afccb05a 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -14,6 +14,9 @@ using Unit_t = PowerMeterHttpJsonValue::Unit; class PowerMeterHttpJson : public PowerMeterProvider { public: + explicit PowerMeterHttpJson(PowerMeterHttpJsonConfig const& cfg) + : _cfg(cfg) { } + bool init() final; void loop() final; float getPowerTotal() const final; @@ -24,6 +27,8 @@ public: poll_result_t poll(); private: + PowerMeterHttpJsonConfig const _cfg; + uint32_t _lastPoll; power_values_t _powerValues; diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h index b351674b..30444b6a 100644 --- a/include/PowerMeterHttpSml.h +++ b/include/PowerMeterHttpSml.h @@ -5,10 +5,14 @@ #include #include #include "HttpGetter.h" +#include "Configuration.h" #include "PowerMeterSml.h" class PowerMeterHttpSml : public PowerMeterSml { public: + explicit PowerMeterHttpSml(PowerMeterHttpSmlConfig const& cfg) + : _cfg(cfg) { } + bool init() final; void loop() final; @@ -17,6 +21,8 @@ public: String poll(); private: + PowerMeterHttpSmlConfig const _cfg; + uint32_t _lastPoll = 0; std::unique_ptr _upHttpGetter; diff --git a/include/PowerMeterMqtt.h b/include/PowerMeterMqtt.h index 8880f481..39257187 100644 --- a/include/PowerMeterMqtt.h +++ b/include/PowerMeterMqtt.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Configuration.h" #include "PowerMeterProvider.h" #include #include @@ -8,6 +9,9 @@ class PowerMeterMqtt : public PowerMeterProvider { public: + explicit PowerMeterMqtt(PowerMeterMqttConfig const& cfg) + : _cfg(cfg) { } + ~PowerMeterMqtt(); bool init() final; @@ -21,6 +25,8 @@ private: uint8_t const* payload, size_t len, size_t index, size_t total, float* targetVariable); + PowerMeterMqttConfig const _cfg; + float _powerValueOne = 0; float _powerValueTwo = 0; float _powerValueThree = 0; diff --git a/include/PowerMeterSerialSdm.h b/include/PowerMeterSerialSdm.h index ff33bebd..e98b286e 100644 --- a/include/PowerMeterSerialSdm.h +++ b/include/PowerMeterSerialSdm.h @@ -2,11 +2,21 @@ #pragma once #include +#include "Configuration.h" #include "PowerMeterProvider.h" #include "SDM.h" class PowerMeterSerialSdm : public PowerMeterProvider { public: + enum class Phases { + One, + Three + }; + + PowerMeterSerialSdm(Phases phases, PowerMeterSerialSdmConfig const& cfg) + : _phases(phases) + , _cfg(cfg) { } + ~PowerMeterSerialSdm(); bool init() final; @@ -15,6 +25,9 @@ public: void doMqttPublish() const final; private: + Phases _phases; + PowerMeterSerialSdmConfig const _cfg; + uint32_t _lastPoll; float _phase1Power = 0.0; diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 47261318..3787dfe7 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -26,20 +26,24 @@ void PowerMeterClass::updateSettings() if (_upProvider) { _upProvider.reset(); } - auto const& config = Configuration.get(); + auto const& pmcfg = Configuration.get().PowerMeter; - if (!config.PowerMeter.Enabled) { return; } + if (!pmcfg.Enabled) { return; } - switch(static_cast(config.PowerMeter.Source)) { + switch(static_cast(pmcfg.Source)) { case PowerMeterProvider::Type::MQTT: - _upProvider = std::make_unique(); + _upProvider = std::make_unique(pmcfg.Mqtt); break; case PowerMeterProvider::Type::SDM1PH: + _upProvider = std::make_unique( + PowerMeterSerialSdm::Phases::One, pmcfg.SerialSdm); + break; case PowerMeterProvider::Type::SDM3PH: - _upProvider = std::make_unique(); + _upProvider = std::make_unique( + PowerMeterSerialSdm::Phases::Three, pmcfg.SerialSdm); break; case PowerMeterProvider::Type::HTTP_JSON: - _upProvider = std::make_unique(); + _upProvider = std::make_unique(pmcfg.HttpJson); break; case PowerMeterProvider::Type::SERIAL_SML: _upProvider = std::make_unique(); @@ -48,7 +52,7 @@ void PowerMeterClass::updateSettings() _upProvider = std::make_unique(); break; case PowerMeterProvider::Type::HTTP_SML: - _upProvider = std::make_unique(); + _upProvider = std::make_unique(pmcfg.HttpSml); break; } diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index 621670d3..822f2eb2 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "Utils.h" -#include "Configuration.h" #include "PowerMeterHttpJson.h" #include "MessageOutput.h" #include @@ -11,14 +10,12 @@ bool PowerMeterHttpJson::init() { - auto const& config = Configuration.get(); - for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - auto const& valueConfig = config.PowerMeter.HttpJson.Values[i]; + auto const& valueConfig = _cfg.Values[i]; _httpGetters[i] = nullptr; - if (i == 0 || (config.PowerMeter.HttpJson.IndividualRequests && valueConfig.Enabled)) { + if (i == 0 || (_cfg.IndividualRequests && valueConfig.Enabled)) { _httpGetters[i] = std::make_unique(valueConfig.HttpRequest); } @@ -40,8 +37,7 @@ bool PowerMeterHttpJson::init() void PowerMeterHttpJson::loop() { - auto const& config = Configuration.get(); - if ((millis() - _lastPoll) < (config.PowerMeter.HttpJson.PollingInterval * 1000)) { + if ((millis() - _lastPoll) < (_cfg.PollingInterval * 1000)) { return; } @@ -68,7 +64,7 @@ PowerMeterHttpJson::poll_result_t PowerMeterHttpJson::poll() }; for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - auto const& cfg = Configuration.get().PowerMeter.HttpJson.Values[i]; + auto const& cfg = _cfg.Values[i]; if (!cfg.Enabled) { cache[i] = 0.0; diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index e9a38389..7d64f442 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-2.0-or-later -#include "Configuration.h" #include "PowerMeterHttpSml.h" #include "MessageOutput.h" #include @@ -8,9 +7,7 @@ bool PowerMeterHttpSml::init() { - auto const& config = Configuration.get(); - - _upHttpGetter = std::make_unique(config.PowerMeter.HttpSml.HttpRequest); + _upHttpGetter = std::make_unique(_cfg.HttpRequest); if (_upHttpGetter->init()) { return true; } @@ -24,8 +21,7 @@ bool PowerMeterHttpSml::init() void PowerMeterHttpSml::loop() { - auto const& config = Configuration.get(); - if ((millis() - _lastPoll) < (config.PowerMeter.HttpSml.PollingInterval * 1000)) { + if ((millis() - _lastPoll) < (_cfg.PollingInterval * 1000)) { return; } diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp index 9a4654b7..1ce9fbb0 100644 --- a/src/PowerMeterMqtt.cpp +++ b/src/PowerMeterMqtt.cpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "PowerMeterMqtt.h" -#include "Configuration.h" #include "MqttSettings.h" #include "MessageOutput.h" @@ -18,10 +17,9 @@ bool PowerMeterMqtt::init() _mqttSubscriptions.push_back(topic); }; - auto const& config = Configuration.get(); - subscribe(config.PowerMeter.Mqtt.Values[0].Topic, &_powerValueOne); - subscribe(config.PowerMeter.Mqtt.Values[1].Topic, &_powerValueTwo); - subscribe(config.PowerMeter.Mqtt.Values[2].Topic, &_powerValueThree); + subscribe(_cfg.Values[0].Topic, &_powerValueOne); + subscribe(_cfg.Values[1].Topic, &_powerValueTwo); + subscribe(_cfg.Values[2].Topic, &_powerValueThree); return _mqttSubscriptions.size() > 0; } diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp index a425f986..f5552aaa 100644 --- a/src/PowerMeterSerialSdm.cpp +++ b/src/PowerMeterSerialSdm.cpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "PowerMeterSerialSdm.h" -#include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" #include "SerialPortManager.h" @@ -61,13 +60,11 @@ void PowerMeterSerialSdm::loop() { if (!_upSdm) { return; } - auto const& config = Configuration.get(); - - if ((millis() - _lastPoll) < (config.PowerMeter.SerialSdm.PollingInterval * 1000)) { + if ((millis() - _lastPoll) < (_cfg.PollingInterval * 1000)) { return; } - uint8_t addr = config.PowerMeter.SerialSdm.Address; + uint8_t addr = _cfg.Address; // reading takes a "very long" time as each readVal() is a synchronous // exchange of serial messages. cache the values and write later to @@ -81,7 +78,7 @@ void PowerMeterSerialSdm::loop() float energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, addr); float energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, addr); - if (static_cast(config.PowerMeter.Source) == PowerMeterProvider::Type::SDM3PH) { + if (_phases == Phases::Three) { phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, addr); phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, addr); phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, addr); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 0fea84ed..b0e2bbcd 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -188,25 +188,19 @@ void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request char response[256]; - auto powerMeterConfig = std::make_unique(); - JsonObject httpJson = root["http_json"]; - powerMeterConfig->HttpJson.IndividualRequests = httpJson["individual_requests"].as(); - powerMeterConfig->VerboseLogging = true; - Configuration.deserializePowerMeterHttpJsonConfig(httpJson, - powerMeterConfig->HttpJson); - auto backup = std::make_unique(Configuration.get().PowerMeter); - Configuration.get().PowerMeter = *powerMeterConfig; - auto upMeter = std::make_unique(); + auto powerMeterConfig = std::make_unique(); + Configuration.deserializePowerMeterHttpJsonConfig(root["http_json"].as(), + *powerMeterConfig); + auto upMeter = std::make_unique(*powerMeterConfig); upMeter->init(); auto res = upMeter->poll(); - Configuration.get().PowerMeter = *backup; using values_t = PowerMeterHttpJson::power_values_t; if (std::holds_alternative(res)) { retMsg["type"] = "success"; auto vals = std::get(res); auto pos = snprintf(response, sizeof(response), "Result: %5.2fW", vals[0]); - for (size_t i = 1; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { - if (!powerMeterConfig->HttpJson.Values[i].Enabled) { continue; } + for (size_t i = 1; i < vals.size(); ++i) { + if (!powerMeterConfig->Values[i].Enabled) { continue; } pos += snprintf(response + pos, sizeof(response) - pos, ", %5.2fW", vals[i]); } snprintf(response + pos, sizeof(response) - pos, ", Total: %5.2f", upMeter->getPowerTotal()); @@ -235,16 +229,12 @@ void WebApiPowerMeterClass::onTestHttpSmlRequest(AsyncWebServerRequest* request) char response[256]; - auto powerMeterConfig = std::make_unique(); + auto powerMeterConfig = std::make_unique(); Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as(), - powerMeterConfig->HttpSml); - powerMeterConfig->VerboseLogging = true; - auto backup = std::make_unique(Configuration.get().PowerMeter); - Configuration.get().PowerMeter = *powerMeterConfig; - auto upMeter = std::make_unique(); + *powerMeterConfig); + auto upMeter = std::make_unique(*powerMeterConfig); upMeter->init(); auto res = upMeter->poll(); - Configuration.get().PowerMeter = *backup; if (res.isEmpty()) { retMsg["type"] = "success"; snprintf(response, sizeof(response), "Result: %5.2fW", upMeter->getPowerTotal()); From a2a9debd025dc7d1598f95b072173e94b0751218 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 23 May 2024 20:26:39 +0200 Subject: [PATCH 23/50] Feature: make power meter polling intervals configurable this change makes the respective setting accessible from the web UI. --- include/PowerMeterHttpJson.h | 1 + include/PowerMeterHttpSml.h | 1 + include/PowerMeterProvider.h | 2 +- include/PowerMeterSerialSdm.h | 1 + src/PowerMeterHttpJson.cpp | 6 ++++++ src/PowerMeterHttpSml.cpp | 6 ++++++ src/PowerMeterSerialSdm.cpp | 6 ++++++ webapp/src/locales/de.json | 5 +++-- webapp/src/locales/en.json | 5 +++-- webapp/src/views/PowerMeterAdminView.vue | 23 +++++++++++++++++++++++ 10 files changed, 51 insertions(+), 5 deletions(-) diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index afccb05a..110517c9 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -20,6 +20,7 @@ public: bool init() final; void loop() final; float getPowerTotal() const final; + bool isDataValid() const final; void doMqttPublish() const final; using power_values_t = std::array; diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h index 30444b6a..eb201a1b 100644 --- a/include/PowerMeterHttpSml.h +++ b/include/PowerMeterHttpSml.h @@ -15,6 +15,7 @@ public: bool init() final; void loop() final; + bool isDataValid() const final; // returns an empty string on success, // returns an error message otherwise. diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h index 4cd1c888..0ca7bcdf 100644 --- a/include/PowerMeterProvider.h +++ b/include/PowerMeterProvider.h @@ -23,9 +23,9 @@ public: virtual void loop() = 0; virtual float getPowerTotal() const = 0; + virtual bool isDataValid() const; uint32_t getLastUpdate() const { return _lastUpdate; } - bool isDataValid() const; void mqttLoop() const; protected: diff --git a/include/PowerMeterSerialSdm.h b/include/PowerMeterSerialSdm.h index e98b286e..56a52e6e 100644 --- a/include/PowerMeterSerialSdm.h +++ b/include/PowerMeterSerialSdm.h @@ -22,6 +22,7 @@ public: bool init() final; void loop() final; float getPowerTotal() const final; + bool isDataValid() const final; void doMqttPublish() const final; private: diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index 822f2eb2..c199c41c 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -124,6 +124,12 @@ float PowerMeterHttpJson::getPowerTotal() const return sum; } +bool PowerMeterHttpJson::isDataValid() const +{ + uint32_t age = millis() - getLastUpdate(); + return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000)); +} + void PowerMeterHttpJson::doMqttPublish() const { mqttPublish("power1", _powerValues[0]); diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp index 7d64f442..799355c6 100644 --- a/src/PowerMeterHttpSml.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -36,6 +36,12 @@ void PowerMeterHttpSml::loop() gotUpdate(); } +bool PowerMeterHttpSml::isDataValid() const +{ + uint32_t age = millis() - getLastUpdate(); + return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000)); +} + String PowerMeterHttpSml::poll() { if (!_upHttpGetter) { diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp index f5552aaa..3c9941f6 100644 --- a/src/PowerMeterSerialSdm.cpp +++ b/src/PowerMeterSerialSdm.cpp @@ -43,6 +43,12 @@ float PowerMeterSerialSdm::getPowerTotal() const return _phase1Power + _phase2Power + _phase3Power; } +bool PowerMeterSerialSdm::isDataValid() const +{ + uint32_t age = millis() - getLastUpdate(); + return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000)); +} + void PowerMeterSerialSdm::doMqttPublish() const { std::lock_guard l(_mutex); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 25630a8d..2d668aa0 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -554,8 +554,9 @@ "PowerMeterConfiguration": "Stromzähler Konfiguration", "PowerMeterEnable": "Aktiviere Stromzähler", "VerboseLogging": "@:base.VerboseLogging", - "PowerMeterParameter": "Power Meter Parameter", "PowerMeterSource": "Stromzählertyp", + "pollingInterval": "Abfrageintervall", + "seconds": "@:base.Seconds", "typeMQTT": "MQTT", "typeSDM1ph": "SDM mit 1 Phase (SDM120/220/230)", "typeSDM3ph": "SDM mit 3 Phasen (SDM72/630)", @@ -598,7 +599,7 @@ "headerKeyDescription": "Optional. Ein benutzerdefinierter HTTP header kann definiert werden. Nützlich um z.B. ein (zusätzlichen) Authentifizierungstoken zu übermitteln.", "headerValue": "HTTP Header - Wert", "timeout": "Zeitüberschreitung", - "milliSeconds": "ms" + "milliSeconds": "Millisekunden" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 8b5f7d0e..66328d82 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -556,8 +556,9 @@ "PowerMeterConfiguration": "Power Meter Configuration", "PowerMeterEnable": "Enable Power Meter", "VerboseLogging": "@:base.VerboseLogging", - "PowerMeterParameter": "Power Meter Parameter", "PowerMeterSource": "Power Meter type", + "pollingInterval": "Polling Interval", + "seconds": "@:base.Seconds", "typeMQTT": "MQTT", "typeSDM1ph": "SDM for 1 phase (SDM120/220/230)", "typeSDM3ph": "SDM for 3 phases (SDM72/630)", @@ -600,7 +601,7 @@ "headerKeyDescription": "Optional. A custom HTTP header key-value pair can be defined. Useful, e.g., to send an (additional) authentication token.", "headerValue": "HTTP Header - Value", "timeout": "Timeout", - "milliSeconds": "ms" + "milliSeconds": "Milliseconds" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Settings", diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index c389af4d..63535285 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -51,6 +51,14 @@ :text="$t('powermeteradmin.SDM')" textVariant="text-bg-primary" add-space> + + +
@@ -70,6 +78,14 @@ v-model="powerMeterConfigList.http_json.individual_requests" type="checkbox" wide /> + +