#include "json_codec.h" #include #include #include #include "config.h" #include "power_manager.h" static float round2(float value) { if (isnan(value)) { return value; } return roundf(value * 100.0f) / 100.0f; } static uint32_t kwh_to_wh(float value) { if (isnan(value)) { return 0; } double wh = static_cast(value) * 1000.0; if (wh < 0.0) { wh = 0.0; } if (wh > static_cast(UINT32_MAX)) { wh = static_cast(UINT32_MAX); } return static_cast(llround(wh)); } static int32_t round_to_i32(float value) { if (isnan(value)) { return 0; } long rounded = lroundf(value); if (rounded > INT32_MAX) { return INT32_MAX; } if (rounded < INT32_MIN) { return INT32_MIN; } return static_cast(rounded); } 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 sender_label_from_short_id(uint16_t short_id, char *out, size_t out_len) { if (!out || out_len == 0) { return; } for (uint8_t i = 0; i < NUM_SENDERS; ++i) { if (EXPECTED_SENDER_IDS[i] == short_id) { snprintf(out, out_len, "s%02u", static_cast(i + 1)); return; } } snprintf(out, out_len, "s00"); } static uint16_t short_id_from_sender_label(const char *sender_label) { if (!sender_label || strlen(sender_label) < 2 || sender_label[0] != 's') { return 0; } char *end = nullptr; long idx = strtol(sender_label + 1, &end, 10); if (end == sender_label + 1 || idx <= 0 || idx > NUM_SENDERS) { return 0; } return EXPECTED_SENDER_IDS[idx - 1]; } 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.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(data.last_error); } out_json = ""; size_t len = serializeJson(doc, out_json); return len > 0 && len < 256; } 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 = doc["e_kwh"] | NAN; data.total_power_w = doc["p_w"] | NAN; 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.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(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(strtoul(suffix, nullptr, 16)); } return true; } bool meterBatchToJson(const MeterData *samples, size_t count, uint16_t batch_id, String &out_json, const FaultCounters *faults, FaultType last_error) { if (!samples || count == 0) { return false; } DynamicJsonDocument doc(8192); doc["schema"] = 1; char sender_label[8] = {}; sender_label_from_short_id(samples[count - 1].short_id, sender_label, sizeof(sender_label)); doc["sender"] = sender_label; doc["batch_id"] = batch_id; doc["t0"] = samples[0].ts_utc; doc["t_first"] = samples[0].ts_utc; doc["t_last"] = samples[count - 1].ts_utc; uint32_t dt_s = METER_SAMPLE_INTERVAL_MS / 1000; doc["dt_s"] = dt_s > 0 ? dt_s : 1; doc["n"] = static_cast(count); 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(last_error); } if (!isnan(samples[count - 1].battery_voltage_v)) { char bat_buf[16]; format_float_2(bat_buf, sizeof(bat_buf), samples[count - 1].battery_voltage_v); doc["bat_v"] = serialized(bat_buf); } JsonArray energy = doc.createNestedArray("e_wh"); JsonArray p_w = doc.createNestedArray("p_w"); JsonArray p1_w = doc.createNestedArray("p1_w"); JsonArray p2_w = doc.createNestedArray("p2_w"); JsonArray p3_w = doc.createNestedArray("p3_w"); for (size_t i = 0; i < count; ++i) { energy.add(kwh_to_wh(samples[i].energy_total_kwh)); p_w.add(round_to_i32(samples[i].total_power_w)); p1_w.add(round_to_i32(samples[i].phase_power_w[0])); p2_w.add(round_to_i32(samples[i].phase_power_w[1])); p3_w.add(round_to_i32(samples[i].phase_power_w[2])); } 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; } const char *id = doc["id"] | ""; const char *sender = doc["sender"] | ""; uint32_t err_m = doc["err_m"] | 0; uint32_t err_tx = doc["err_tx"] | 0; FaultType last_error = static_cast(doc["err_last"] | 0); float bat_v = doc["bat_v"] | NAN; if (!doc["schema"].isNull()) { if ((doc["schema"] | 0) != 1) { return false; } size_t count = doc["n"] | 0; if (count == 0) { return false; } if (count > max_count) { count = max_count; } uint32_t t0 = doc["t0"] | 0; uint32_t t_first = doc["t_first"] | t0; uint32_t t_last = doc["t_last"] | t_first; uint32_t dt_s = doc["dt_s"] | 1; JsonArray energy = doc["e_wh"].as(); JsonArray p_w = doc["p_w"].as(); JsonArray p1_w = doc["p1_w"].as(); JsonArray p2_w = doc["p2_w"].as(); JsonArray p3_w = doc["p3_w"].as(); for (size_t idx = 0; idx < count; ++idx) { MeterData &data = out_samples[idx]; data = {}; uint16_t short_id = short_id_from_sender_label(sender); if (short_id != 0) { snprintf(data.device_id, sizeof(data.device_id), "dd3-%04X", short_id); data.short_id = short_id; } else if (id[0] != '\0') { strncpy(data.device_id, id, sizeof(data.device_id)); data.device_id[sizeof(data.device_id) - 1] = '\0'; } else { snprintf(data.device_id, sizeof(data.device_id), "dd3-0000"); } if (count > 1 && t_last >= t_first) { uint32_t span = t_last - t_first; uint32_t step = span / static_cast(count - 1); data.ts_utc = t_first + static_cast(idx) * step; } else { data.ts_utc = t0 + static_cast(idx) * dt_s; } data.energy_total_kwh = static_cast((energy[idx] | 0)) / 1000.0f; data.total_power_w = static_cast(p_w[idx] | 0); data.phase_power_w[0] = static_cast(p1_w[idx] | 0); data.phase_power_w[1] = static_cast(p2_w[idx] | 0); data.phase_power_w[2] = static_cast(p3_w[idx] | 0); data.battery_voltage_v = bat_v; if (!isnan(bat_v)) { data.battery_percent = battery_percent_from_voltage(bat_v); } else { data.battery_percent = 0; } data.valid = true; 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 (data.short_id == 0 && strlen(data.device_id) >= 8) { const char *suffix = data.device_id + strlen(data.device_id) - 4; data.short_id = static_cast(strtoul(suffix, nullptr, 16)); } } out_count = count; return count > 0; } return false; }