246 lines
7.4 KiB
C++
246 lines
7.4 KiB
C++
#include "json_codec.h"
|
|
#include <ArduinoJson.h>
|
|
#include <math.h>
|
|
#include "power_manager.h"
|
|
|
|
static float round2(float value) {
|
|
if (isnan(value)) {
|
|
return value;
|
|
}
|
|
return roundf(value * 100.0f) / 100.0f;
|
|
}
|
|
|
|
static const char *short_id_from_device_id(const char *device_id) {
|
|
if (!device_id) {
|
|
return "";
|
|
}
|
|
size_t len = strlen(device_id);
|
|
if (len >= 4) {
|
|
return device_id + (len - 4);
|
|
}
|
|
return device_id;
|
|
}
|
|
|
|
static void format_float_2(char *buf, size_t buf_len, float value) {
|
|
if (!buf || buf_len == 0) {
|
|
return;
|
|
}
|
|
if (isnan(value)) {
|
|
snprintf(buf, buf_len, "null");
|
|
return;
|
|
}
|
|
snprintf(buf, buf_len, "%.2f", round2(value));
|
|
}
|
|
|
|
bool meterDataToJson(const MeterData &data, String &out_json) {
|
|
StaticJsonDocument<256> doc;
|
|
doc["id"] = short_id_from_device_id(data.device_id);
|
|
doc["ts"] = data.ts_utc;
|
|
char buf[16];
|
|
format_float_2(buf, sizeof(buf), data.energy_total_kwh);
|
|
doc["e_kwh"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.total_power_w);
|
|
doc["p_w"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.phase_power_w[0]);
|
|
doc["p1_w"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.phase_power_w[1]);
|
|
doc["p2_w"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.phase_power_w[2]);
|
|
doc["p3_w"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.phase_voltage_v[0]);
|
|
doc["v1_v"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.phase_voltage_v[1]);
|
|
doc["v2_v"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.phase_voltage_v[2]);
|
|
doc["v3_v"] = serialized(buf);
|
|
format_float_2(buf, sizeof(buf), data.battery_voltage_v);
|
|
doc["bat_v"] = serialized(buf);
|
|
doc["bat_pct"] = data.battery_percent;
|
|
if (data.link_valid) {
|
|
doc["rssi"] = data.link_rssi_dbm;
|
|
doc["snr"] = data.link_snr_db;
|
|
}
|
|
if (data.err_meter_read > 0) {
|
|
doc["err_m"] = data.err_meter_read;
|
|
}
|
|
if (data.err_decode > 0) {
|
|
doc["err_d"] = data.err_decode;
|
|
}
|
|
if (data.err_lora_tx > 0) {
|
|
doc["err_tx"] = data.err_lora_tx;
|
|
}
|
|
if (data.last_error != FaultType::None) {
|
|
doc["err_last"] = static_cast<uint8_t>(data.last_error);
|
|
}
|
|
|
|
out_json = "";
|
|
size_t len = serializeJson(doc, out_json);
|
|
return len > 0 && len < 256;
|
|
}
|
|
|
|
static float read_float_or_legacy(JsonDocument &doc, const char *key, const char *legacy_key) {
|
|
if (doc[key].isNull()) {
|
|
return doc[legacy_key] | NAN;
|
|
}
|
|
return doc[key] | NAN;
|
|
}
|
|
|
|
bool jsonToMeterData(const String &json, MeterData &data) {
|
|
StaticJsonDocument<256> doc;
|
|
DeserializationError err = deserializeJson(doc, json);
|
|
if (err) {
|
|
return false;
|
|
}
|
|
|
|
const char *id = doc["id"] | "";
|
|
if (strlen(id) == 4) {
|
|
snprintf(data.device_id, sizeof(data.device_id), "dd3-%s", id);
|
|
} else {
|
|
strncpy(data.device_id, id, sizeof(data.device_id));
|
|
}
|
|
data.device_id[sizeof(data.device_id) - 1] = '\0';
|
|
|
|
data.ts_utc = doc["ts"] | 0;
|
|
data.energy_total_kwh = read_float_or_legacy(doc, "e_kwh", "energy_kwh");
|
|
data.total_power_w = read_float_or_legacy(doc, "p_w", "p_total_w");
|
|
data.phase_power_w[0] = doc["p1_w"] | NAN;
|
|
data.phase_power_w[1] = doc["p2_w"] | NAN;
|
|
data.phase_power_w[2] = doc["p3_w"] | NAN;
|
|
data.phase_voltage_v[0] = doc["v1_v"] | NAN;
|
|
data.phase_voltage_v[1] = doc["v2_v"] | NAN;
|
|
data.phase_voltage_v[2] = doc["v3_v"] | NAN;
|
|
data.battery_voltage_v = doc["bat_v"] | NAN;
|
|
if (doc["bat_pct"].isNull() && !isnan(data.battery_voltage_v)) {
|
|
data.battery_percent = battery_percent_from_voltage(data.battery_voltage_v);
|
|
} else {
|
|
data.battery_percent = doc["bat_pct"] | 0;
|
|
}
|
|
data.valid = true;
|
|
data.link_valid = false;
|
|
data.link_rssi_dbm = 0;
|
|
data.link_snr_db = NAN;
|
|
data.err_meter_read = doc["err_m"] | 0;
|
|
data.err_decode = doc["err_d"] | 0;
|
|
data.err_lora_tx = doc["err_tx"] | 0;
|
|
data.last_error = static_cast<FaultType>(doc["err_last"] | 0);
|
|
|
|
if (strlen(data.device_id) >= 8) {
|
|
const char *suffix = data.device_id + strlen(data.device_id) - 4;
|
|
data.short_id = static_cast<uint16_t>(strtoul(suffix, nullptr, 16));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool meterBatchToJson(const MeterData *samples, size_t count, String &out_json, const FaultCounters *faults, FaultType last_error) {
|
|
if (!samples || count == 0) {
|
|
return false;
|
|
}
|
|
|
|
DynamicJsonDocument doc(8192);
|
|
doc["id"] = short_id_from_device_id(samples[count - 1].device_id);
|
|
doc["bat_v"] = round2(samples[count - 1].battery_voltage_v);
|
|
doc["bat_pct"] = samples[count - 1].battery_percent;
|
|
if (faults) {
|
|
if (faults->meter_read_fail > 0) {
|
|
doc["err_m"] = faults->meter_read_fail;
|
|
}
|
|
if (faults->lora_tx_fail > 0) {
|
|
doc["err_tx"] = faults->lora_tx_fail;
|
|
}
|
|
}
|
|
if (last_error != FaultType::None) {
|
|
doc["err_last"] = static_cast<uint8_t>(last_error);
|
|
}
|
|
JsonArray arr = doc.createNestedArray("s");
|
|
for (size_t i = 0; i < count; ++i) {
|
|
JsonArray row = arr.createNestedArray();
|
|
row.add(samples[i].ts_utc);
|
|
row.add(round2(samples[i].energy_total_kwh));
|
|
row.add(round2(samples[i].total_power_w));
|
|
row.add(round2(samples[i].phase_power_w[0]));
|
|
row.add(round2(samples[i].phase_power_w[1]));
|
|
row.add(round2(samples[i].phase_power_w[2]));
|
|
row.add(round2(samples[i].phase_voltage_v[0]));
|
|
row.add(round2(samples[i].phase_voltage_v[1]));
|
|
row.add(round2(samples[i].phase_voltage_v[2]));
|
|
row.add(samples[i].valid ? 1 : 0);
|
|
}
|
|
|
|
out_json = "";
|
|
size_t len = serializeJson(doc, out_json);
|
|
return len > 0;
|
|
}
|
|
|
|
bool jsonToMeterBatch(const String &json, MeterData *out_samples, size_t max_count, size_t &out_count) {
|
|
out_count = 0;
|
|
if (!out_samples || max_count == 0) {
|
|
return false;
|
|
}
|
|
|
|
DynamicJsonDocument doc(8192);
|
|
DeserializationError err = deserializeJson(doc, json);
|
|
if (err) {
|
|
return false;
|
|
}
|
|
|
|
JsonArray arr = doc["s"].as<JsonArray>();
|
|
if (arr.isNull()) {
|
|
return false;
|
|
}
|
|
|
|
const char *id = doc["id"] | "";
|
|
float bat_v = doc["bat_v"] | NAN;
|
|
uint8_t bat_pct = doc["bat_pct"] | 0;
|
|
uint32_t err_m = doc["err_m"] | 0;
|
|
uint32_t err_tx = doc["err_tx"] | 0;
|
|
FaultType last_error = static_cast<FaultType>(doc["err_last"] | 0);
|
|
|
|
size_t idx = 0;
|
|
for (JsonArray row : arr) {
|
|
if (idx >= max_count) {
|
|
break;
|
|
}
|
|
MeterData &data = out_samples[idx];
|
|
data = {};
|
|
if (strlen(id) == 4) {
|
|
snprintf(data.device_id, sizeof(data.device_id), "dd3-%s", id);
|
|
} else {
|
|
strncpy(data.device_id, id, sizeof(data.device_id));
|
|
}
|
|
data.device_id[sizeof(data.device_id) - 1] = '\0';
|
|
data.ts_utc = row[0] | 0;
|
|
data.energy_total_kwh = row[1] | NAN;
|
|
data.total_power_w = row[2] | NAN;
|
|
data.phase_power_w[0] = row[3] | NAN;
|
|
data.phase_power_w[1] = row[4] | NAN;
|
|
data.phase_power_w[2] = row[5] | NAN;
|
|
data.phase_voltage_v[0] = row[6] | NAN;
|
|
data.phase_voltage_v[1] = row[7] | NAN;
|
|
data.phase_voltage_v[2] = row[8] | NAN;
|
|
data.valid = (row[9] | 1) != 0;
|
|
data.battery_voltage_v = bat_v;
|
|
if (doc["bat_pct"].isNull() && !isnan(bat_v)) {
|
|
data.battery_percent = battery_percent_from_voltage(bat_v);
|
|
} else {
|
|
data.battery_percent = bat_pct;
|
|
}
|
|
data.link_valid = false;
|
|
data.link_rssi_dbm = 0;
|
|
data.link_snr_db = NAN;
|
|
data.err_meter_read = err_m;
|
|
data.err_decode = 0;
|
|
data.err_lora_tx = err_tx;
|
|
data.last_error = last_error;
|
|
|
|
if (strlen(data.device_id) >= 8) {
|
|
const char *suffix = data.device_id + strlen(data.device_id) - 4;
|
|
data.short_id = static_cast<uint16_t>(strtoul(suffix, nullptr, 16));
|
|
}
|
|
idx++;
|
|
}
|
|
|
|
out_count = idx;
|
|
return idx > 0;
|
|
}
|