test: add json stability and discovery payload coverage
This commit is contained in:
60
lib/dd3_legacy_core/include/data_model.h
Normal file
60
lib/dd3_legacy_core/include/data_model.h
Normal 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);
|
||||
6
lib/dd3_legacy_core/include/json_codec.h
Normal file
6
lib/dd3_legacy_core/include/json_codec.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "data_model.h"
|
||||
|
||||
bool meterDataToJson(const MeterData &data, String &out_json);
|
||||
29
lib/dd3_legacy_core/src/data_model.cpp
Normal file
29
lib/dd3_legacy_core/src/data_model.cpp
Normal 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";
|
||||
}
|
||||
}
|
||||
96
lib/dd3_legacy_core/src/json_codec.cpp
Normal file
96
lib/dd3_legacy_core/src/json_codec.cpp
Normal 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;
|
||||
}
|
||||
7
lib/dd3_transport_logic/include/ha_discovery_json.h
Normal file
7
lib/dd3_transport_logic/include/ha_discovery_json.h
Normal 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);
|
||||
37
lib/dd3_transport_logic/src/ha_discovery_json.cpp
Normal file
37
lib/dd3_transport_logic/src/ha_discovery_json.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user