From b8e0733a897d5357c1ce9c8004467ba304cf0635 Mon Sep 17 00:00:00 2001 From: acidburns Date: Fri, 20 Feb 2026 21:32:35 +0100 Subject: [PATCH] test: add json stability and discovery payload coverage --- lib/dd3_legacy_core/include/data_model.h | 60 ++++++++ lib/dd3_legacy_core/include/json_codec.h | 6 + .../dd3_legacy_core/src}/data_model.cpp | 0 .../dd3_legacy_core/src}/json_codec.cpp | 0 .../include/ha_discovery_json.h | 7 + .../src/ha_discovery_json.cpp | 37 +++++ src/mqtt_client.cpp | 34 ++--- test/test_json_codec/test_json_codec.cpp | 129 ++++++++++++++++++ 8 files changed, 249 insertions(+), 24 deletions(-) create mode 100644 lib/dd3_legacy_core/include/data_model.h create mode 100644 lib/dd3_legacy_core/include/json_codec.h rename {src => lib/dd3_legacy_core/src}/data_model.cpp (100%) rename {src => lib/dd3_legacy_core/src}/json_codec.cpp (100%) create mode 100644 lib/dd3_transport_logic/include/ha_discovery_json.h create mode 100644 lib/dd3_transport_logic/src/ha_discovery_json.cpp create mode 100644 test/test_json_codec/test_json_codec.cpp diff --git a/lib/dd3_legacy_core/include/data_model.h b/lib/dd3_legacy_core/include/data_model.h new file mode 100644 index 0000000..eadf2d3 --- /dev/null +++ b/lib/dd3_legacy_core/include/data_model.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +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); diff --git a/lib/dd3_legacy_core/include/json_codec.h b/lib/dd3_legacy_core/include/json_codec.h new file mode 100644 index 0000000..fbfa3b7 --- /dev/null +++ b/lib/dd3_legacy_core/include/json_codec.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include "data_model.h" + +bool meterDataToJson(const MeterData &data, String &out_json); diff --git a/src/data_model.cpp b/lib/dd3_legacy_core/src/data_model.cpp similarity index 100% rename from src/data_model.cpp rename to lib/dd3_legacy_core/src/data_model.cpp diff --git a/src/json_codec.cpp b/lib/dd3_legacy_core/src/json_codec.cpp similarity index 100% rename from src/json_codec.cpp rename to lib/dd3_legacy_core/src/json_codec.cpp diff --git a/lib/dd3_transport_logic/include/ha_discovery_json.h b/lib/dd3_transport_logic/include/ha_discovery_json.h new file mode 100644 index 0000000..222f14f --- /dev/null +++ b/lib/dd3_transport_logic/include/ha_discovery_json.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +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); diff --git a/lib/dd3_transport_logic/src/ha_discovery_json.cpp b/lib/dd3_transport_logic/src/ha_discovery_json.cpp new file mode 100644 index 0000000..4056466 --- /dev/null +++ b/lib/dd3_transport_logic/src/ha_discovery_json.cpp @@ -0,0 +1,37 @@ +#include "ha_discovery_json.h" + +#include + +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; +} diff --git a/src/mqtt_client.cpp b/src/mqtt_client.cpp index 630a690..0a09e6b 100644 --- a/src/mqtt_client.cpp +++ b/src/mqtt_client.cpp @@ -2,6 +2,7 @@ #include #include #include +#include "ha_discovery_json.h" #include "config.h" #include "json_codec.h" @@ -10,6 +11,13 @@ static PubSubClient mqtt_client(wifi_client); static WifiMqttConfig g_cfg; 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) { switch (fault) { 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, 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; - size_t len = serializeJson(doc, payload); - if (len == 0) { + if (!ha_build_discovery_sensor_payload(device_id, key, name, unit, device_class, state_topic, value_template, + ha_manufacturer_anchor(), payload)) { return false; } diff --git a/test/test_json_codec/test_json_codec.cpp b/test/test_json_codec/test_json_codec.cpp new file mode 100644 index 0000000..cb3aa94 --- /dev/null +++ b/test/test_json_codec/test_json_codec.cpp @@ -0,0 +1,129 @@ +#include +#include + +#include + +#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(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(FaultType::Decode), doc["err_last"] | 0U); + TEST_ASSERT_EQUAL_UINT8(static_cast(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(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(); + 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() {}