diff --git a/include/Configuration.h b/include/Configuration.h index 66fb4861..79bdd736 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -202,7 +202,7 @@ struct CONFIG_T { bool UpdatesOnly; } Vedirect; - struct { + struct PowerMeterConfig { bool Enabled; bool VerboseLogging; uint32_t Interval; diff --git a/include/HttpGetter.h b/include/HttpGetter.h new file mode 100644 index 00000000..11ece109 --- /dev/null +++ b/include/HttpGetter.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include +#include +#include +#include + +using up_http_client_t = std::unique_ptr; +using sp_wifi_client_t = std::shared_ptr; + +class HttpRequestResult { +public: + HttpRequestResult(bool success, + up_http_client_t upHttpClient = nullptr, + sp_wifi_client_t spWiFiClient = nullptr) + : _success(success) + , _upHttpClient(std::move(upHttpClient)) + , _spWiFiClient(std::move(spWiFiClient)) { } + + ~HttpRequestResult() { + // the wifi client *must* die *after* the http client, as the http + // client uses the wifi client in its destructor. + if (_upHttpClient) { _upHttpClient->end(); } + _upHttpClient = nullptr; + _spWiFiClient = nullptr; + } + + HttpRequestResult(HttpRequestResult const&) = delete; + HttpRequestResult(HttpRequestResult&&) = delete; + HttpRequestResult& operator=(HttpRequestResult const&) = delete; + HttpRequestResult& operator=(HttpRequestResult&&) = delete; + + operator bool() const { return _success; } + + Stream* getStream() { + if(!_upHttpClient) { return nullptr; } + return _upHttpClient->getStreamPtr(); + } + +private: + bool _success; + up_http_client_t _upHttpClient; + sp_wifi_client_t _spWiFiClient; +}; + +class HttpGetter { +public: + explicit HttpGetter(HttpRequestConfig const& cfg) + : _config(cfg) { } + + bool init(); + void addHeader(char const* key, char const* value); + HttpRequestResult performGetRequest(); + + char const* getErrorText() const { return _errBuffer; } + +private: + String getAuthDigest(String const& authReq, unsigned int counter); + HttpRequestConfig const& _config; + + template + void logError(char const* format, Args... args); + char _errBuffer[256]; + + bool _useHttps; + String _host; + String _uri; + uint16_t _port; + + sp_wifi_client_t _spWiFiClient; // reused for multiple HTTP requests + + std::vector> _additionalHeaders; +}; diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index e50fb78f..7f81ff37 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include +#include #include #include -#include -#include +#include "HttpGetter.h" #include "Configuration.h" #include "PowerMeterProvider.h" @@ -13,31 +14,19 @@ using Unit_t = PowerMeterHttpJsonConfig::Unit; class PowerMeterHttpJson : public PowerMeterProvider { public: - ~PowerMeterHttpJson(); - - bool init() final { return true; } + bool init() final; void loop() final; float getPowerTotal() const final; void doMqttPublish() const final; - bool queryValue(int phase, PowerMeterHttpJsonConfig const& config); - char httpPowerMeterError[256]; - float getCached(size_t idx) { return _cache[idx]; } + using power_values_t = std::array; + using poll_result_t = std::variant; + poll_result_t poll(); private: uint32_t _lastPoll; - 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, PowerMeterHttpJsonConfig const& config); - 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); - bool tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted); - void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); - String sha256(const String& data); + power_values_t _powerValues; + + std::array, POWERMETER_HTTP_JSON_MAX_VALUES> _httpGetters; }; diff --git a/include/Utils.h b/include/Utils.h index a17b910a..38905b28 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -16,5 +16,5 @@ public: /* OpenDTU-OnBatter-specific utils go here: */ template - static std::pair getJsonValueFromStringByPath(String const& jsonText, String const& path); + static std::pair getJsonValueByPath(JsonDocument const& root, String const& path); }; diff --git a/src/HttpGetter.cpp b/src/HttpGetter.cpp new file mode 100644 index 00000000..2f409fea --- /dev/null +++ b/src/HttpGetter.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "HttpGetter.h" +#include +#include "mbedtls/sha256.h" +#include +#include + +template +void HttpGetter::logError(char const* format, Args... args) { + snprintf(_errBuffer, sizeof(_errBuffer), format, args...); +} + +bool HttpGetter::init() +{ + String url(_config.Url); + + int index = url.indexOf(':'); + if (index < 0) { + logError("failed to parse URL protocol: no colon in URL"); + return false; + } + + String protocol = url.substring(0, index); + if (protocol != "http" && protocol != "https") { + logError("failed to parse URL protocol: '%s' is neither 'http' nor 'https'", protocol.c_str()); + return false; + } + + _useHttps = (protocol == "https"); + + // initialize port to default values for http or https. + // port will be overwritten below in case port is explicitly defined + _port = _useHttps ? 443 : 80; + + String slashes = url.substring(index + 1, index + 3); + if (slashes != "//") { + logError("expected two forward slashes after first colon in URL"); + return false; + } + + _uri = url.substring(index + 3); // without protocol identifier + + index = _uri.indexOf('/'); + if (index == -1) { + index = _uri.length(); + _uri += '/'; + } + _host = _uri.substring(0, index); + _uri.remove(0, index); // remove host part + + index = _host.indexOf('@'); + if (index >= 0) { + // basic authentication is only supported through setting username + // and password using the respective inputs, not embedded into the URL. + // to avoid regressions, we remove username and password from the host + // part of the URL. + _host.remove(0, index + 1); // remove auth part including @ + } + + // get port + index = _host.indexOf(':'); + if (index >= 0) { + _host = _host.substring(0, index); // up until colon + _port = _host.substring(index + 1).toInt(); // after colon + } + + if (_useHttps) { + auto secureWifiClient = std::make_shared(); + secureWifiClient->setInsecure(); + _spWiFiClient = std::move(secureWifiClient); + } else { + _spWiFiClient = std::make_shared(); + } + + return true; +} + +HttpRequestResult HttpGetter::performGetRequest() +{ + // hostByName in WiFiGeneric fails to resolve local names. issue described at + // https://github.com/espressif/arduino-esp32/issues/3822 and in analyzed in + // depth at https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 + // in conclusion: we cannot rely on _upHttpClient->begin(*wifiClient, url) to resolve + // IP adresses. have to do it manually. + IPAddress ipaddr((uint32_t)0); + + if (!ipaddr.fromString(_host)) { + // host is not an IP address, so try to resolve the name to an address. + // first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() + // will spam the console if done the other way around. + ipaddr = INADDR_NONE; + + if (Configuration.get().Mdns.Enabled) { + ipaddr = MDNS.queryHost(_host); // INADDR_NONE if failed + } + + if (ipaddr == INADDR_NONE && !WiFiGenericClass::hostByName(_host.c_str(), ipaddr)) { + logError("failed to resolve host '%s' via DNS", _host.c_str()); + return { false }; + } + } + + auto upTmpHttpClient = std::make_unique(); + if (!upTmpHttpClient->begin(*_spWiFiClient, ipaddr.toString(), _port, _uri, _useHttps)) { + logError("HTTP client begin() failed for %s://%s", + (_useHttps ? "https" : "http"), _host.c_str()); + return { false }; + } + + upTmpHttpClient->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + upTmpHttpClient->setUserAgent("OpenDTU-OnBattery"); + upTmpHttpClient->setConnectTimeout(_config.Timeout); + upTmpHttpClient->setTimeout(_config.Timeout); + for (auto const& h : _additionalHeaders) { + upTmpHttpClient->addHeader(h.first.c_str(), h.second.c_str()); + } + + if (strlen(_config.HeaderKey) > 0) { + upTmpHttpClient->addHeader(_config.HeaderKey, _config.HeaderValue); + } + + using Auth_t = HttpRequestConfig::Auth; + switch (_config.AuthType) { + case Auth_t::None: + break; + case Auth_t::Basic: { + String credentials = String(_config.Username) + ":" + _config.Password; + String authorization = "Basic " + base64::encode(credentials); + upTmpHttpClient->addHeader("Authorization", authorization); + break; + } + case Auth_t::Digest: { + const char *headers[1] = {"WWW-Authenticate"}; + upTmpHttpClient->collectHeaders(headers, 1); + break; + } + } + + int httpCode = upTmpHttpClient->GET(); + + if (httpCode == HTTP_CODE_UNAUTHORIZED && _config.AuthType == Auth_t::Digest) { + if (!upTmpHttpClient->hasHeader("WWW-Authenticate")) { + logError("Cannot perform digest authentication as server did " + "not send a WWW-Authenticate header"); + return { false }; + } + String authReq = upTmpHttpClient->header("WWW-Authenticate"); + String authorization = getAuthDigest(authReq, 1); + upTmpHttpClient->addHeader("Authorization", authorization); + httpCode = upTmpHttpClient->GET(); + } + + if (httpCode <= 0) { + logError("HTTP Error: %s", upTmpHttpClient->errorToString(httpCode).c_str()); + return { false }; + } + + if (httpCode != HTTP_CODE_OK) { + logError("Bad HTTP code: %d", httpCode); + return { false }; + } + + return { true, std::move(upTmpHttpClient), _spWiFiClient }; +} + +static String sha256(const String& data) { + uint8_t hash[32]; + + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // select SHA256 + mbedtls_sha256_update(&ctx, reinterpret_cast(data.c_str()), data.length()); + mbedtls_sha256_finish(&ctx, hash); + mbedtls_sha256_free(&ctx); + + char res[sizeof(hash) * 2 + 1]; + for (int i = 0; i < sizeof(hash); i++) { + snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]); + } + + return res; +} + +static String extractParam(String const& authReq, String const& param, char delimiter) { + auto begin = authReq.indexOf(param); + if (begin == -1) { return ""; } + auto end = authReq.indexOf(delimiter, begin + param.length()); + return authReq.substring(begin + param.length(), end); +} + +static String getcNonce(int len) { + static const char alphanum[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + String s = ""; + + for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; } + + return s; +} + +String HttpGetter::getAuthDigest(String const& authReq, unsigned int counter) { + // extracting required parameters for RFC 2617 Digest + String realm = extractParam(authReq, "realm=\"", '"'); + String nonce = extractParam(authReq, "nonce=\"", '"'); + String cNonce = getcNonce(8); + + char nc[9]; + snprintf(nc, sizeof(nc), "%08x", counter); + + // sha256 of the user:realm:password + String ha1 = sha256(String(_config.Username) + ":" + realm + ":" + _config.Password); + + // sha256 of method:uri + String ha2 = sha256("GET:" + _uri); + + // sha256 of h1:nonce:nc:cNonce:auth:h2 + String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + + ":" + cNonce + ":" + "auth" + ":" + ha2); + + // Final authorization String + return String("Digest username=\"") + _config.Username + + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + + _uri + "\", cnonce=\"" + cNonce + "\", nc=" + nc + + ", qop=auth, response=\"" + response + "\", algorithm=SHA-256"; +} + +void HttpGetter::addHeader(char const* key, char const* value) +{ + _additionalHeaders.push_back({ key, value }); +} diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp index 6faeb71c..cc8b600a 100644 --- a/src/PowerMeterHttpJson.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -9,13 +9,33 @@ #include #include -PowerMeterHttpJson::~PowerMeterHttpJson() +bool PowerMeterHttpJson::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(); + + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + auto const& valueConfig = config.PowerMeter.HttpJson[i]; + + _httpGetters[i] = nullptr; + + if (i == 0 || (config.PowerMeter.HttpIndividualRequests && valueConfig.Enabled)) { + _httpGetters[i] = std::make_unique(valueConfig.HttpRequest); + } + + if (!_httpGetters[i]) { continue; } + + if (_httpGetters[i]->init()) { + _httpGetters[i]->addHeader("Content-Type", "application/json"); + _httpGetters[i]->addHeader("Accept", "application/json"); + continue; + } + + MessageOutput.printf("[PowerMeterHttpJson] Initializing HTTP getter for value %d failed:\r\n", i + 1); + MessageOutput.printf("[PowerMeterHttpJson] %s\r\n", _httpGetters[i]->getErrorText()); + return false; + } + + return true; } void PowerMeterHttpJson::loop() @@ -27,33 +47,72 @@ void PowerMeterHttpJson::loop() _lastPoll = millis(); - for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { - auto const& valueConfig = config.PowerMeter.HttpJson[i]; - - if (!valueConfig.Enabled) { - _cache[i] = 0.0; - continue; - } - - if (i == 0 || config.PowerMeter.HttpIndividualRequests) { - 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; - } - continue; - } - - 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; - } + auto res = poll(); + if (std::holds_alternative(res)) { + MessageOutput.printf("[PowerMeterHttpJson] %s\r\n", std::get(res).c_str()); + return; } + _powerValues = std::get(res); gotUpdate(); +} - _powerValues = _cache; +PowerMeterHttpJson::poll_result_t PowerMeterHttpJson::poll() +{ + power_values_t cache; + JsonDocument jsonResponse; + + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + auto const& cfg = Configuration.get().PowerMeter.HttpJson[i]; + + if (!cfg.Enabled) { + cache[i] = 0.0; + continue; + } + + auto const& upGetter = _httpGetters[i]; + + if (upGetter) { + auto res = upGetter->performGetRequest(); + if (!res) { + return upGetter->getErrorText(); + } + + auto pStream = res.getStream(); + if (!pStream) { + return String("Programmer error: HTTP request yields no stream"); + } + + const DeserializationError error = deserializeJson(jsonResponse, *pStream); + if (error) { + String msg("Unable to parse server response as JSON: "); + return msg + error.c_str(); + } + } + + auto pathResolutionResult = Utils::getJsonValueByPath(jsonResponse, cfg.JsonPath); + if (!pathResolutionResult.second.isEmpty()) { + return pathResolutionResult.second; + } + + // this value is supposed to be in Watts and positive if energy is consumed + cache[i] = pathResolutionResult.first; + + switch (cfg.PowerUnit) { + case Unit_t::MilliWatts: + cache[i] /= 1000; + break; + case Unit_t::KiloWatts: + cache[i] *= 1000; + break; + default: + break; + } + + if (cfg.SignInverted) { cache[i] *= -1; } + } + + return cache; } float PowerMeterHttpJson::getPowerTotal() const @@ -69,279 +128,3 @@ void PowerMeterHttpJson::doMqttPublish() const mqttPublish("power2", _powerValues[1]); mqttPublish("power3", _powerValues[2]); } - -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 - //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; - uint16_t port; - extractUrlComponents(config.HttpRequest.Url, protocol, host, uri, port); - - 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(httpPowerMeterError, sizeof(httpPowerMeterError), 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(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); - } - } - else - { - ipaddr = MDNS.queryHost(host); - if (ipaddr == INADDR_NONE){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), 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(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); - } - } - } - } - - bool https = protocol == "https"; - if (https) { - auto secureWifiClient = std::make_unique(); - secureWifiClient->setInsecure(); - wifiClient = std::move(secureWifiClient); - } else { - wifiClient = std::make_unique(); - } - - 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, PowerMeterHttpJsonConfig const& powerMeterConfig) -{ - 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; - } - - auto const& config = powerMeterConfig.HttpRequest; - prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); - if (config.AuthType == Auth_t::Digest) { - const char *headers[1] = {"WWW-Authenticate"}; - 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); - } - 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"); - 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()); - return false; - } - - prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); - httpClient->addHeader("Authorization", authorization); - httpCode = httpClient->GET(); - } - } - - if (httpCode <= 0) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient->errorToString(httpCode).c_str()); - return false; - } - - if (httpCode != HTTP_CODE_OK) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode); - return false; - } - - 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. - return tryGetFloatValueForPhase(phase, powerMeterConfig.JsonPath, powerMeterConfig.PowerUnit, powerMeterConfig.SignInverted); -} - -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 PowerMeterHttpJson::getcNonce(const int len) { - static const char alphanum[] = "0123456789" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz"; - String s = ""; - - for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; } - - return s; -} - -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=\"", '"'); - String cNonce = getcNonce(8); - - char nc[9]; - snprintf(nc, sizeof(nc), "%08x", counter); - - //sha256 of the user:realm:password - String ha1 = sha256(username + ":" + realm + ":" + password); - - //sha256 of method:uri - String ha2 = sha256(method + ":" + uri); - - //sha256 of h1:nonce:nc:cNonce:auth:h2 - String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2); - - //Final authorization String; - String authorization = "Digest username=\""; - authorization += username; - authorization += "\", realm=\""; - authorization += realm; - authorization += "\", nonce=\""; - authorization += nonce; - authorization += "\", uri=\""; - authorization += uri; - authorization += "\", cnonce=\""; - authorization += cNonce; - authorization += "\", nc="; - authorization += String(nc); - authorization += ", qop=auth, response=\""; - authorization += response; - authorization += "\", algorithm=SHA-256"; - - return authorization; -} - -bool PowerMeterHttpJson::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted) -{ - auto pathResolutionResult = Utils::getJsonValueFromStringByPath(httpResponse, jsonPath); - if (!pathResolutionResult.second.isEmpty()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - 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] = pathResolutionResult.first; - - switch (unit) { - case Unit_t::MilliWatts: - _cache[phase] /= 1000; - break; - case Unit_t::KiloWatts: - _cache[phase] *= 1000; - break; - default: - break; - } - - 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 PowerMeterHttpJson::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port) -{ - // check for : (http: or https: - int index = url.indexOf(':'); - if(index < 0) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), 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) { - // 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 @ - } - - // 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; -} - -String PowerMeterHttpJson::sha256(const String& data) { - uint8_t hash[32]; - - mbedtls_sha256_context ctx; - mbedtls_sha256_init(&ctx); - mbedtls_sha256_starts(&ctx, 0); // select SHA256 - mbedtls_sha256_update(&ctx, reinterpret_cast(data.c_str()), data.length()); - mbedtls_sha256_finish(&ctx, hash); - mbedtls_sha256_free(&ctx); - - char res[sizeof(hash) * 2 + 1]; - for (int i = 0; i < sizeof(hash); i++) { - snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]); - } - - return res; -} - -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"); - - if (strlen(httpHeader) > 0) { - httpClient->addHeader(httpHeader, httpValue); - } -} diff --git a/src/Utils.cpp b/src/Utils.cpp index aa97455a..b7b42f56 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -95,20 +95,14 @@ void Utils::removeAllFiles() /* OpenDTU-OnBatter-specific utils go here: */ template -std::pair Utils::getJsonValueFromStringByPath(String const& jsonText, String const& path) +std::pair Utils::getJsonValueByPath(JsonDocument const& root, 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(); + 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 @@ -171,4 +165,4 @@ std::pair Utils::getJsonValueFromStringByPath(String const& jsonText, return { value.as(), "" }; } -template std::pair Utils::getJsonValueFromStringByPath(String const& jsonText, String const& path); +template std::pair Utils::getJsonValueByPath(JsonDocument const& root, String const& path); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 51f37353..72db067e 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -210,14 +210,21 @@ void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request char response[256]; - PowerMeterHttpJsonConfig httpJsonConfig; - Configuration.deserializePowerMeterHttpJsonConfig(root.as(), httpJsonConfig); + auto powerMeterConfig = std::make_unique(); + Configuration.deserializePowerMeterHttpJsonConfig(root.as(), powerMeterConfig->HttpJson[0]); + auto backup = std::make_unique(Configuration.get().PowerMeter); + Configuration.get().PowerMeter = *powerMeterConfig; auto upMeter = std::make_unique(); - if (upMeter->queryValue(0/*value index*/, httpJsonConfig)) { + 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"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getCached(0)); + auto vals = std::get(res); + snprintf_P(response, sizeof(response), "Result: %5.2fW", vals[0]); } else { - snprintf_P(response, sizeof(response), "%s", upMeter->httpPowerMeterError); + snprintf_P(response, sizeof(response), "%s", std::get(res).c_str()); } retMsg["message"] = response;