From df15485d7c437acf396d7379edd1832225f00575 Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 12 Dec 2025 10:03:52 +0100 Subject: [PATCH 1/6] feat: add enum support for metrics endpoint --- src/core/emsdevice.cpp | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index 8b5ec063b..892c3b5e5 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1704,9 +1704,10 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { continue; } - // only process number and boolean types for now + // only process number, boolean and enum types if (dv.type != DeviceValueType::BOOL && dv.type != DeviceValueType::UINT8 && dv.type != DeviceValueType::INT8 && dv.type != DeviceValueType::UINT16 - && dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 && dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME) { + && dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 && dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME + && dv.type != DeviceValueType::ENUM) { continue; } @@ -1752,6 +1753,12 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { metric_value = *(uint32_t *)(dv.value_p); } break; + case DeviceValueType::ENUM: + if (*(uint8_t *)(dv.value_p) < dv.options_size) { + has_value = true; + metric_value = *(uint8_t *)(dv.value_p); + } + break; default: break; } @@ -1794,8 +1801,19 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { } std::string uom_str; + std::string enum_mapping; if (dv.type == DeviceValueType::BOOL) { uom_str = "boolean"; + } else if (dv.type == DeviceValueType::ENUM) { + // build enum mapping string: "(0: Wert1; 1: Wert2; ...)" + enum_mapping = "("; + for (uint8_t i = 0; i < dv.options_size; i++) { + if (i > 0) { + enum_mapping += "; "; + } + enum_mapping += std::to_string(i) + ": " + std::string(Helpers::translated_word(dv.options[i])); + } + enum_mapping += ")"; } else if (dv.uom != DeviceValueUOM::NONE) { uom_str = uom_to_string(dv.uom); } @@ -1804,6 +1822,9 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { if (!uom_str.empty()) { help_line += ", " + uom_str; } + if (!enum_mapping.empty()) { + help_line += ", enum, " + enum_mapping; + } bool readable = dv.type != DeviceValueType::CMD && !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE); bool writeable = dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY); @@ -1854,7 +1875,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { } double rounded = (final_value >= 0) ? (double)((int64_t)(final_value + 0.5)) : (double)((int64_t)(final_value - 0.5)); - if (dv.type == DeviceValueType::BOOL || (final_value == rounded)) { + if (dv.type == DeviceValueType::BOOL || dv.type == DeviceValueType::ENUM || (final_value == rounded)) { snprintf(val_str, sizeof(val_str), "%.0f", final_value); } else { snprintf(val_str, sizeof(val_str), "%.2f", final_value); From b05712cf837f45b2dbcd42aeae2250fdd0b3e87a Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 12 Dec 2025 10:04:51 +0100 Subject: [PATCH 2/6] fix: check for duplicate labels --- src/core/system.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index aa39b3b34..d67a112b4 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1751,7 +1751,18 @@ std::string System::get_metrics_prometheus() { // collect string for info metric (skip dynamic strings like uptime and timestamp) std::string val = p.value().as(); if (!val.empty() && key != "uptime" && key != "timestamp") { - local_info_labels.push_back({to_lowercase(key), val}); + std::string lower_key = to_lowercase(key); + // check if key already exists in local_info_labels + bool key_exists = false; + for (const auto & label : local_info_labels) { + if (label.first == lower_key) { + key_exists = true; + break; + } + } + if (!key_exists) { + local_info_labels.push_back({lower_key, val}); + } } } } From dcfd0d5b114f54d7fa7062ea0062eca5de505a79 Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 12 Dec 2025 10:06:02 +0100 Subject: [PATCH 3/6] test: add unit tests for metrics enum outputs --- test/test_api/test_api.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_api/test_api.cpp b/test/test_api/test_api.cpp index 2482f1dfb..a003ecde1 100644 --- a/test/test_api/test_api.cpp +++ b/test/test_api/test_api.cpp @@ -299,6 +299,10 @@ void manual_test8() { TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr); TEST_ASSERT_TRUE(strstr(response, " gauge") != nullptr); + if (strstr(response, ", enum, (") != nullptr) { + TEST_ASSERT_TRUE(strstr(response, ", enum, (") != nullptr); + TEST_ASSERT_TRUE(strstr(response, ")") != nullptr); + } TEST_ASSERT_TRUE(strstr(response, "emsesp_tapwateractive") != nullptr || strstr(response, "emsesp_selflowtemp") != nullptr || strstr(response, "emsesp_curflowtemp") != nullptr); } @@ -313,6 +317,10 @@ void manual_test9() { TEST_ASSERT_TRUE(strstr(response, "# TYPE") != nullptr); TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr); + if (strstr(response, ", enum, (") != nullptr) { + TEST_ASSERT_TRUE(strstr(response, ", enum, (") != nullptr); + TEST_ASSERT_TRUE(strstr(response, ")") != nullptr); + } if (strstr(response, "circuit=") != nullptr) { TEST_ASSERT_TRUE(strstr(response, "{circuit=") != nullptr); } @@ -329,6 +337,10 @@ void manual_test10() { TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr); TEST_ASSERT_TRUE(strstr(response, " gauge") != nullptr); + if (strstr(response, ", enum, (") != nullptr) { + TEST_ASSERT_TRUE(strstr(response, ", enum, (") != nullptr); + TEST_ASSERT_TRUE(strstr(response, ")") != nullptr); + } // Check for some expected system metrics TEST_ASSERT_TRUE(strstr(response, "emsesp_system_") != nullptr || strstr(response, "emsesp_network_") != nullptr || strstr(response, "emsesp_api_") != nullptr); From c11402195f31040f1d12e744b52edd44dfa402e3 Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 12 Dec 2025 14:08:47 +0100 Subject: [PATCH 4/6] chore: reserve string capacity for prometheus metrics --- src/core/emsdevice.cpp | 26 +++++++++++++++++++++++--- src/core/system.cpp | 4 ++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index 892c3b5e5..93a3f2bcb 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1699,15 +1699,33 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { std::string result; std::unordered_map seen_metrics; + // Helper function to check if a device value type is supported for Prometheus metrics + auto is_supported_type = [](uint8_t type) -> bool { + return type == DeviceValueType::BOOL || type == DeviceValueType::UINT8 || type == DeviceValueType::INT8 + || type == DeviceValueType::UINT16 || type == DeviceValueType::INT16 || type == DeviceValueType::UINT24 + || type == DeviceValueType::UINT32 || type == DeviceValueType::TIME || type == DeviceValueType::ENUM; + }; + + // Dynamically reserve memory for the result + size_t entity_count = 0; + for (const auto & dv : devicevalues_) { + if (tag >= 0 && tag != dv.tag) { + continue; + } + // only count supported types + if (is_supported_type(dv.type)) { + entity_count++; + } + } + result.reserve(160 * entity_count); + for (auto & dv : devicevalues_) { if (tag >= 0 && tag != dv.tag) { continue; } // only process number, boolean and enum types - if (dv.type != DeviceValueType::BOOL && dv.type != DeviceValueType::UINT8 && dv.type != DeviceValueType::INT8 && dv.type != DeviceValueType::UINT16 - && dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 && dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME - && dv.type != DeviceValueType::ENUM) { + if (!is_supported_type(dv.type)) { continue; } @@ -1884,6 +1902,8 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { result += "\n"; } + result.shrink_to_fit(); + return result; } diff --git a/src/core/system.cpp b/src/core/system.cpp index d67a112b4..a332fc393 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1568,6 +1568,8 @@ std::string System::get_metrics_prometheus() { std::string result; std::unordered_map seen_metrics; + result.reserve(16000); + // get system data JsonDocument doc; JsonObject root = doc.to(); @@ -1797,6 +1799,8 @@ std::string System::get_metrics_prometheus() { // process root object process_object(root, ""); + result.shrink_to_fit(); + return result; } From 597e60d6f1724601a023ffa982f7b4a1f8d81bfe Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 12 Dec 2025 17:53:50 +0100 Subject: [PATCH 5/6] chore: filter out entities with no values --- src/core/emsdevice.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index 93a3f2bcb..41a53b3cb 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1713,7 +1713,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { continue; } // only count supported types - if (is_supported_type(dv.type)) { + if (dv.hasValue() && is_supported_type(dv.type)) { entity_count++; } } @@ -1725,7 +1725,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { } // only process number, boolean and enum types - if (!is_supported_type(dv.type)) { + if (!dv.hasValue() || !is_supported_type(dv.type)) { continue; } From 5c966e291b77a3e5afee6a7b3ff8db4f0a1b4309 Mon Sep 17 00:00:00 2001 From: Jakob Date: Sat, 13 Dec 2025 15:50:04 +0100 Subject: [PATCH 6/6] chore: avoid dangling references while processing json objects recursively --- src/core/system.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index 9319ccd92..5fc3862cd 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1635,7 +1635,7 @@ std::string System::get_metrics_prometheus() { }; // helper function to process a JSON object recursively - std::function process_object = [&](const JsonObject & obj, const std::string & prefix) { + std::function process_object = [&](const JsonObject obj, const std::string & prefix) { std::vector> local_info_labels; bool has_nested_objects = false;