move JSON path resolver to Utils class for re-use

This commit is contained in:
Bernhard Kirchen 2024-05-10 14:50:02 +02:00
parent 673b9f4fa8
commit ccba7d8036
5 changed files with 93 additions and 69 deletions

View File

@ -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;

View File

@ -3,6 +3,7 @@
#include <ArduinoJson.h>
#include <cstdint>
#include <utility>
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<typename T>
static std::pair<T, String> getJsonValueFromStringByPath(String const& jsonText, String const& path);
};

View File

@ -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<float>(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<JsonVariantConst>();
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<JsonArrayConst>()) {
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<float>()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[PowerMeterHttpJson] not a float: '%s'"),
value.as<String>().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<float>();
_cache[phase] = pathResolutionResult.first;
switch (unit) {
case Unit_t::MilliWatts:

View File

@ -92,3 +92,83 @@ void Utils::removeAllFiles()
file = root.getNextFileName();
}
}
/* OpenDTU-OnBatter-specific utils go here: */
template<typename T>
std::pair<T, String> 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<JsonVariant>();
// 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<JsonArrayConst>()) {
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<T>()) {
snprintf(errBuffer, kErrBufferSize, "Value '%s' at JSON path '%s' is not "
"of the expected type", value.as<String>().c_str(), path.c_str());
return { T(), String(errBuffer) };
}
return { value.as<T>(), "" };
}
template std::pair<float, String> Utils::getJsonValueFromStringByPath(String const& jsonText, String const& path);

View File

@ -258,7 +258,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
auto upMeter = std::make_unique<PowerMeterHttpJson>();
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);
}