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);
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <PubSubClient.h>
|
#include <PubSubClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include "ha_discovery_json.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "json_codec.h"
|
#include "json_codec.h"
|
||||||
|
|
||||||
@@ -10,6 +11,13 @@ static PubSubClient mqtt_client(wifi_client);
|
|||||||
static WifiMqttConfig g_cfg;
|
static WifiMqttConfig g_cfg;
|
||||||
static String g_client_id;
|
static String g_client_id;
|
||||||
|
|
||||||
|
static const char *ha_manufacturer_anchor() {
|
||||||
|
StaticJsonDocument<32> doc;
|
||||||
|
JsonObject device = doc.createNestedObject("device");
|
||||||
|
device["manufacturer"] = HA_MANUFACTURER;
|
||||||
|
return HA_MANUFACTURER;
|
||||||
|
}
|
||||||
|
|
||||||
static const char *fault_text(FaultType fault) {
|
static const char *fault_text(FaultType fault) {
|
||||||
switch (fault) {
|
switch (fault) {
|
||||||
case FaultType::MeterRead:
|
case FaultType::MeterRead:
|
||||||
@@ -94,31 +102,9 @@ bool mqtt_publish_faults(const char *device_id, const FaultCounters &counters, F
|
|||||||
|
|
||||||
static bool publish_discovery_sensor(const char *device_id, const char *key, const char *name, const char *unit, const char *device_class,
|
static bool publish_discovery_sensor(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 *state_topic, const char *value_template) {
|
||||||
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"] = HA_MANUFACTURER;
|
|
||||||
|
|
||||||
String payload;
|
String payload;
|
||||||
size_t len = serializeJson(doc, payload);
|
if (!ha_build_discovery_sensor_payload(device_id, key, name, unit, device_class, state_topic, value_template,
|
||||||
if (len == 0) {
|
ha_manufacturer_anchor(), payload)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
129
test/test_json_codec/test_json_codec.cpp
Normal file
129
test/test_json_codec/test_json_codec.cpp
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "data_model.h"
|
||||||
|
#include "dd3_legacy_core.h"
|
||||||
|
#include "ha_discovery_json.h"
|
||||||
|
#include "json_codec.h"
|
||||||
|
|
||||||
|
static void fill_state_sample(MeterData &data) {
|
||||||
|
data = {};
|
||||||
|
data.ts_utc = 1769905000;
|
||||||
|
data.short_id = 0xF19C;
|
||||||
|
strncpy(data.device_id, "dd3-F19C", sizeof(data.device_id));
|
||||||
|
data.energy_total_kwh = 1234.5678f;
|
||||||
|
data.total_power_w = 321.6f;
|
||||||
|
data.phase_power_w[0] = 100.4f;
|
||||||
|
data.phase_power_w[1] = 110.4f;
|
||||||
|
data.phase_power_w[2] = 110.8f;
|
||||||
|
data.battery_voltage_v = 3.876f;
|
||||||
|
data.battery_percent = 77;
|
||||||
|
data.link_valid = true;
|
||||||
|
data.link_rssi_dbm = -71;
|
||||||
|
data.link_snr_db = 7.25f;
|
||||||
|
data.err_meter_read = 1;
|
||||||
|
data.err_decode = 2;
|
||||||
|
data.err_lora_tx = 3;
|
||||||
|
data.last_error = FaultType::Decode;
|
||||||
|
data.rx_reject_reason = static_cast<uint8_t>(RxRejectReason::CrcFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_state_json_required_keys_and_stability() {
|
||||||
|
MeterData data = {};
|
||||||
|
fill_state_sample(data);
|
||||||
|
|
||||||
|
String out_json;
|
||||||
|
TEST_ASSERT_TRUE(meterDataToJson(data, out_json));
|
||||||
|
|
||||||
|
StaticJsonDocument<512> doc;
|
||||||
|
DeserializationError err = deserializeJson(doc, out_json);
|
||||||
|
TEST_ASSERT_TRUE(err == DeserializationError::Ok);
|
||||||
|
|
||||||
|
const char *required_keys[] = {
|
||||||
|
"id", "ts", "e_kwh", "p_w", "p1_w", "p2_w", "p3_w",
|
||||||
|
"bat_v", "bat_pct", "rssi", "snr", "err_m", "err_d",
|
||||||
|
"err_tx", "err_last", "rx_reject", "rx_reject_text"};
|
||||||
|
for (size_t i = 0; i < (sizeof(required_keys) / sizeof(required_keys[0])); ++i) {
|
||||||
|
TEST_ASSERT_TRUE_MESSAGE(doc.containsKey(required_keys[i]), required_keys[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("F19C", doc["id"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(data.ts_utc, doc["ts"] | 0U);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(FaultType::Decode), doc["err_last"] | 0U);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(RxRejectReason::CrcFail), doc["rx_reject"] | 0U);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("crc_fail", doc["rx_reject_text"] | "");
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("energy_total_kwh"));
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("power_w"));
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("battery_voltage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_state_json_optional_keys_when_not_available() {
|
||||||
|
MeterData data = {};
|
||||||
|
fill_state_sample(data);
|
||||||
|
data.link_valid = false;
|
||||||
|
data.err_meter_read = 0;
|
||||||
|
data.err_decode = 0;
|
||||||
|
data.err_lora_tx = 0;
|
||||||
|
data.rx_reject_reason = static_cast<uint8_t>(RxRejectReason::None);
|
||||||
|
|
||||||
|
String out_json;
|
||||||
|
TEST_ASSERT_TRUE(meterDataToJson(data, out_json));
|
||||||
|
|
||||||
|
StaticJsonDocument<512> doc;
|
||||||
|
DeserializationError err = deserializeJson(doc, out_json);
|
||||||
|
TEST_ASSERT_TRUE(err == DeserializationError::Ok);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("rssi"));
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("snr"));
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("err_m"));
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("err_d"));
|
||||||
|
TEST_ASSERT_FALSE(doc.containsKey("err_tx"));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("none", doc["rx_reject_text"] | "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_ha_discovery_manufacturer_and_key_stability() {
|
||||||
|
String payload;
|
||||||
|
TEST_ASSERT_TRUE(ha_build_discovery_sensor_payload(
|
||||||
|
"dd3-F19C", "energy", "Energy", "kWh", "energy",
|
||||||
|
"smartmeter/dd3-F19C/state", "{{ value_json.e_kwh }}",
|
||||||
|
HA_MANUFACTURER, payload));
|
||||||
|
|
||||||
|
StaticJsonDocument<384> doc;
|
||||||
|
DeserializationError err = deserializeJson(doc, payload);
|
||||||
|
TEST_ASSERT_TRUE(err == DeserializationError::Ok);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(doc.containsKey("name"));
|
||||||
|
TEST_ASSERT_TRUE(doc.containsKey("state_topic"));
|
||||||
|
TEST_ASSERT_TRUE(doc.containsKey("unique_id"));
|
||||||
|
TEST_ASSERT_TRUE(doc.containsKey("value_template"));
|
||||||
|
TEST_ASSERT_TRUE(doc.containsKey("device"));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("dd3-F19C_energy", doc["unique_id"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("smartmeter/dd3-F19C/state", doc["state_topic"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("{{ value_json.e_kwh }}", doc["value_template"] | "");
|
||||||
|
|
||||||
|
JsonObject device = doc["device"].as<JsonObject>();
|
||||||
|
TEST_ASSERT_TRUE(device.containsKey("identifiers"));
|
||||||
|
TEST_ASSERT_TRUE(device.containsKey("name"));
|
||||||
|
TEST_ASSERT_TRUE(device.containsKey("model"));
|
||||||
|
TEST_ASSERT_TRUE(device.containsKey("manufacturer"));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("DD3-LoRa-Bridge", device["model"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("AcidBurns", device["manufacturer"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("dd3-F19C", device["name"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("dd3-F19C", device["identifiers"][0] | "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
dd3_legacy_core_force_link();
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_state_json_required_keys_and_stability);
|
||||||
|
RUN_TEST(test_state_json_optional_keys_when_not_available);
|
||||||
|
RUN_TEST(test_ha_discovery_manufacturer_and_key_stability);
|
||||||
|
UNITY_END();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
Reference in New Issue
Block a user