test: add json stability and discovery payload coverage

This commit is contained in:
2026-02-20 21:32:35 +01:00
parent ca2cd1880a
commit b8e0733a89
8 changed files with 249 additions and 24 deletions

View File

@@ -0,0 +1,60 @@
#pragma once
#include <Arduino.h>
enum class FaultType : uint8_t {
None = 0,
MeterRead = 1,
Decode = 2,
LoraTx = 3
};
enum class RxRejectReason : uint8_t {
None = 0,
CrcFail = 1,
InvalidMsgKind = 2,
LengthMismatch = 3,
DeviceIdMismatch = 4,
BatchIdMismatch = 5,
UnknownSender = 6
};
struct FaultCounters {
uint32_t meter_read_fail;
uint32_t decode_fail;
uint32_t lora_tx_fail;
};
struct MeterData {
uint32_t ts_utc;
uint32_t meter_seconds;
uint16_t short_id;
char device_id[16];
float energy_total_kwh;
float phase_power_w[3];
float total_power_w;
float battery_voltage_v;
uint8_t battery_percent;
bool meter_seconds_valid;
bool valid;
int16_t link_rssi_dbm;
float link_snr_db;
bool link_valid;
uint32_t err_meter_read;
uint32_t err_decode;
uint32_t err_lora_tx;
FaultType last_error;
uint8_t rx_reject_reason;
};
struct SenderStatus {
MeterData last_data;
uint32_t last_update_ts_utc;
uint32_t rx_batches_total;
uint32_t rx_batches_duplicate;
uint32_t rx_last_duplicate_ts_utc;
bool has_data;
};
void init_device_ids(uint16_t &short_id, char *device_id, size_t device_id_len);
const char *rx_reject_reason_text(RxRejectReason reason);

View File

@@ -0,0 +1,6 @@
#pragma once
#include <Arduino.h>
#include "data_model.h"
bool meterDataToJson(const MeterData &data, String &out_json);

View File

@@ -0,0 +1,29 @@
#include "data_model.h"
#include <esp_mac.h>
void init_device_ids(uint16_t &short_id, char *device_id, size_t device_id_len) {
uint8_t mac[6] = {0};
// Read base MAC without needing WiFi to be started.
esp_read_mac(mac, ESP_MAC_WIFI_STA);
short_id = (static_cast<uint16_t>(mac[4]) << 8) | mac[5];
snprintf(device_id, device_id_len, "dd3-%04X", short_id);
}
const char *rx_reject_reason_text(RxRejectReason reason) {
switch (reason) {
case RxRejectReason::CrcFail:
return "crc_fail";
case RxRejectReason::InvalidMsgKind:
return "invalid_msg_kind";
case RxRejectReason::LengthMismatch:
return "length_mismatch";
case RxRejectReason::DeviceIdMismatch:
return "device_id_mismatch";
case RxRejectReason::BatchIdMismatch:
return "batch_id_mismatch";
case RxRejectReason::UnknownSender:
return "unknown_sender";
default:
return "none";
}
}

View File

@@ -0,0 +1,96 @@
#include "json_codec.h"
#include <ArduinoJson.h>
#include <limits.h>
#include <math.h>
static constexpr size_t STATE_JSON_DOC_CAPACITY = 512;
static float round2(float value) {
if (isnan(value)) {
return value;
}
return roundf(value * 100.0f) / 100.0f;
}
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<int32_t>(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 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));
}
static void set_int_or_null(JsonDocument &doc, const char *key, float value) {
if (!key || key[0] == '\0') {
return;
}
if (isnan(value)) {
doc[key] = nullptr;
return;
}
doc[key] = round_to_i32(value);
}
bool meterDataToJson(const MeterData &data, String &out_json) {
StaticJsonDocument<STATE_JSON_DOC_CAPACITY> 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);
set_int_or_null(doc, "p_w", data.total_power_w);
set_int_or_null(doc, "p1_w", data.phase_power_w[0]);
set_int_or_null(doc, "p2_w", data.phase_power_w[1]);
set_int_or_null(doc, "p3_w", data.phase_power_w[2]);
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;
}
doc["err_last"] = static_cast<uint8_t>(data.last_error);
doc["rx_reject"] = data.rx_reject_reason;
doc["rx_reject_text"] = rx_reject_reason_text(static_cast<RxRejectReason>(data.rx_reject_reason));
out_json = "";
size_t len = serializeJson(doc, out_json);
return len > 0;
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include <Arduino.h>
bool ha_build_discovery_sensor_payload(const char *device_id, const char *key, const char *name, const char *unit,
const char *device_class, const char *state_topic, const char *value_template,
const char *manufacturer, String &out_payload);

View File

@@ -0,0 +1,37 @@
#include "ha_discovery_json.h"
#include <ArduinoJson.h>
bool ha_build_discovery_sensor_payload(const char *device_id, const char *key, const char *name, const char *unit,
const char *device_class, const char *state_topic, const char *value_template,
const char *manufacturer, String &out_payload) {
if (!device_id || !key || !name || !state_topic || !value_template || !manufacturer) {
return false;
}
StaticJsonDocument<256> doc;
String unique_id = String(device_id) + "_" + key;
String sensor_name = String(device_id) + " " + name;
doc["name"] = sensor_name;
doc["state_topic"] = state_topic;
doc["unique_id"] = unique_id;
if (unit && unit[0] != '\0') {
doc["unit_of_measurement"] = unit;
}
if (device_class && device_class[0] != '\0') {
doc["device_class"] = device_class;
}
doc["value_template"] = value_template;
JsonObject device = doc.createNestedObject("device");
JsonArray identifiers = device.createNestedArray("identifiers");
identifiers.add(String(device_id));
device["name"] = String(device_id);
device["model"] = "DD3-LoRa-Bridge";
device["manufacturer"] = manufacturer;
out_payload = "";
size_t len = serializeJson(doc, out_payload);
return len > 0;
}