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); }