From 7d15e15d8adc36401d65f5fb1079cc8770f3bf53 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Sat, 17 Jul 2021 09:17:19 +0200 Subject: [PATCH 1/3] prometheus: Fix wrong format on ESP32, incorrect names in types When built for ESP32 the memory fragmentation metric was incorrectly terminated (likely a typo). The type information for "tasmota_global_humidity_percentage" didn't include the "_percentage" and "tasmota_sensors" wsa incorrect too. Signed-off-by: Michael Hanselmann --- tasmota/xsns_75_prometheus.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tasmota/xsns_75_prometheus.ino b/tasmota/xsns_75_prometheus.ino index f15c569f6..2a560c743 100644 --- a/tasmota/xsns_75_prometheus.ino +++ b/tasmota/xsns_75_prometheus.ino @@ -113,7 +113,7 @@ void HandleMetrics(void) { } if (TasmotaGlobal.humidity != 0) { dtostrfd(TasmotaGlobal.humidity, Settings->flag2.humidity_resolution, parameter); - WSContentSend_P(PSTR("# TYPE tasmota_global_humidity gauge\ntasmota_global_humidity_percentage %s\n"), parameter); + WSContentSend_P(PSTR("# TYPE tasmota_global_humidity_percentage gauge\ntasmota_global_humidity_percentage %s\n"), parameter); } if (TasmotaGlobal.pressure_hpa != 0) { dtostrfd(TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution, parameter); @@ -124,7 +124,7 @@ void HandleMetrics(void) { #ifdef ESP32 int32_t freeMaxMem = 100 - (int32_t)(ESP_getMaxAllocHeap() * 100 / ESP_getFreeHeap()); WSContentSend_PD(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"Ram\"} %d\n"), ESP_getFreeHeap()); - WSContentSend_PD(PSTR("# TYPE tasmota_memory_ratio gauge\ntasmota_memory_ratio{memory=\"Fragmentation\"} %d)"), freeMaxMem / 100); + WSContentSend_PD(PSTR("# TYPE tasmota_memory_ratio gauge\ntasmota_memory_ratio{memory=\"Fragmentation\"} %d\n"), freeMaxMem / 100); if (UsePSRAM()) { WSContentSend_P(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"Psram\"} %d\n"), ESP.getFreePsram() ); } @@ -196,7 +196,7 @@ void HandleMetrics(void) { const char *value = value1.getStr(nullptr); String sensor = FormatMetricName(key1.getStr()); if (value != nullptr && isdigit(value[0] && strcmp(sensor.c_str(), "time") != 0)) { //remove false 'time' metric - WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s gauge\ntasmota_sensors{sensor=\"%s\"} %s\n"), sensor.c_str(), sensor.c_str(), value); + WSContentSend_P(PSTR("# TYPE tasmota_sensors gauge\ntasmota_sensors{sensor=\"%s\"} %s\n"), sensor.c_str(), value); } } } From 1b96833d6afc386c7b870e1ddb840ef43a93ed26 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Mon, 19 Jul 2021 09:58:50 +0200 Subject: [PATCH 2/3] prometheus: Use utility functions to format metrics Format strings for Prometheus metrics were written manually and the `# TYPE` lines needed to be kept in sync with actual metrics. As indicated by the previous commit it wasn't always as consistent as desired. In addition there was a lot of repetition among the strings which couldn't be reduced at build time. With this change utility functions are introduced which eliminate the need for specifying the same metric name more than once. At the same time the proper escaping for label values, initially added in commit 16b5f2fe9, is now applied for all labels. The size of the program shrinks slightly by 212 bytes on ESP8266 in the "tasmota" configuration with Prometheus enabled and 412 bytes on ESP32 with the "tasmota32" configuration. Signed-off-by: Michael Hanselmann --- tasmota/xsns_75_prometheus.ino | 223 +++++++++++++++++++++++++-------- 1 file changed, 171 insertions(+), 52 deletions(-) diff --git a/tasmota/xsns_75_prometheus.ino b/tasmota/xsns_75_prometheus.ino index 2a560c743..8640a8d02 100644 --- a/tasmota/xsns_75_prometheus.ino +++ b/tasmota/xsns_75_prometheus.ino @@ -74,14 +74,91 @@ String FormatMetricName(const char *metric) { return formatted; } -// Labels can be any sequence of UTF-8 characters, but backslash, double-quote -// and line feed must be escaped. -String FormatLabelValue(const char *value) { - String formatted = value; - formatted.replace("\\", "\\\\"); - formatted.replace("\"", "\\\""); - formatted.replace("\n", "\\n"); - return formatted; +const uint8_t + kPromMetricNoPrefix = _BV(1), + kPromMetricGauge = _BV(2), + kPromMetricCounter = _BV(3), + kPromMetricTypeMask = kPromMetricGauge | kPromMetricCounter; + +// Format and send a Prometheus metric to the client. Use flags to configure +// the type. Labels must be supplied in tuples of two character array pointers +// and terminated by nullptr. +void WritePromMetric(const char *name, uint8_t flags, const char *value, va_list labels) { + PGM_P const prefix = (flags & kPromMetricNoPrefix) ? PSTR("") : PSTR("tasmota_"); + PGM_P tmp; + String lval; + + switch (flags & kPromMetricTypeMask) { + case kPromMetricGauge: + tmp = PSTR("gauge"); + break; + case kPromMetricCounter: + tmp = PSTR("counter"); + break; + default: + tmp = nullptr; + break; + } + + if (tmp != nullptr) { + WSContentSend_P(PSTR("# TYPE %s%s %s\n"), prefix, name, tmp); + } + + WSContentSend_P(PSTR("%s%s{"), prefix, name); + + for (const char *sep = PSTR(""); ; sep = PSTR(",")) { + if ((tmp = va_arg(labels, PGM_P)) == nullptr) { + break; + } + + // A few label values are stored in PROGMEM. The _P functions, e.g. + // snprintf_P, support both program and heap/stack memory with the "%s" + // format on ESP8266/ESP32. Casting the pointer to __FlashStringHelper has + // the same effect with String::operator=. + if (!(lval = va_arg(labels, const __FlashStringHelper *))) { + break; + } + + // Labels can be any sequence of UTF-8 characters, but backslash, + // double-quote and line feed must be escaped. + lval.replace("\\", "\\\\"); + lval.replace("\"", "\\\""); + lval.replace("\n", "\\n"); + + WSContentSend_P(PSTR("%s%s=\"%s\""), sep, tmp, lval.c_str()); + } + + WSContentSend_P(PSTR("} %s\n"), value); +} + +void WritePromMetricInt32(const char *name, uint8_t flags, const int32_t value, ...) { + char str[16]; + + snprintf_P(str, sizeof(str), PSTR("%d"), value); + + va_list labels; + va_start(labels, value); + WritePromMetric(name, flags, str, labels); + va_end(labels); +} + +void WritePromMetricDec(const char *name, uint8_t flags, double number, unsigned char prec, ...) { + char value[FLOATSZ]; + + // Prometheus always requires "." as the decimal separator. + dtostrfd(number, prec, value); + + va_list labels; + va_start(labels, prec); + WritePromMetric(name, flags, value, labels); + va_end(labels); +} + +void WritePromMetricStr(const char *name, uint8_t flags, const char *value, ...) { + va_list labels; + va_start(labels, value); + WritePromMetric(name, flags, value, labels); + va_end(labels); } void HandleMetrics(void) { @@ -91,63 +168,94 @@ void HandleMetrics(void) { WSContentBegin(200, CT_PLAIN); - char parameter[FLOATSZ]; + char namebuf[64]; // Pseudo-metric providing metadata about the running firmware version. - WSContentSend_P(PSTR("# TYPE tasmota_info gauge\ntasmota_info{version=\"%s\",image=\"%s\",build_timestamp=\"%s\",devicename=\"%s\"} 1\n"), - TasmotaGlobal.version, TasmotaGlobal.image_name, GetBuildDateAndTime().c_str(), FormatLabelValue(SettingsText(SET_DEVICENAME)).c_str()); - WSContentSend_P(PSTR("# TYPE tasmota_uptime_seconds gauge\ntasmota_uptime_seconds %d\n"), TasmotaGlobal.uptime); - WSContentSend_P(PSTR("# TYPE tasmota_boot_count counter\ntasmota_boot_count %d\n"), Settings->bootcount); - WSContentSend_P(PSTR("# TYPE tasmota_flash_writes_total counter\ntasmota_flash_writes_total %d\n"), Settings->save_flag); + WritePromMetricInt32(PSTR("info"), kPromMetricGauge, 1, + PSTR("version"), TasmotaGlobal.version, + PSTR("image"), TasmotaGlobal.image_name, + PSTR("build_timestamp"), GetBuildDateAndTime().c_str(), + PSTR("devicename"), SettingsText(SET_DEVICENAME), + nullptr); + WritePromMetricInt32(PSTR("uptime_seconds"), kPromMetricGauge, TasmotaGlobal.uptime, nullptr); + WritePromMetricInt32(PSTR("boot_count"), kPromMetricCounter, Settings->bootcount, nullptr); + WritePromMetricInt32(PSTR("flash_writes_total"), kPromMetricCounter, Settings->save_flag, nullptr); // Pseudo-metric providing metadata about the WiFi station. - WSContentSend_P(PSTR("# TYPE tasmota_wifi_station_info gauge\ntasmota_wifi_station_info{bssid=\"%s\",ssid=\"%s\"} 1\n"), WiFi.BSSIDstr().c_str(), WiFi.SSID().c_str()); + WritePromMetricInt32(PSTR("wifi_station_info"), kPromMetricGauge, 1, + PSTR("bssid"), WiFi.BSSIDstr().c_str(), + PSTR("ssid"), WiFi.SSID().c_str(), + nullptr); // Wi-Fi Signal strength - WSContentSend_P(PSTR("# TYPE tasmota_wifi_station_signal_dbm gauge\ntasmota_wifi_station_signal_dbm{mac_address=\"%s\"} %d\n"), WiFi.BSSIDstr().c_str(), WiFi.RSSI()); + WritePromMetricInt32(PSTR("wifi_station_signal_dbm"), kPromMetricGauge, WiFi.RSSI(), + PSTR("mac_address"), WiFi.BSSIDstr().c_str(), + nullptr); if (!isnan(TasmotaGlobal.temperature_celsius)) { - dtostrfd(TasmotaGlobal.temperature_celsius, Settings->flag2.temperature_resolution, parameter); - WSContentSend_P(PSTR("# TYPE tasmota_global_temperature_celsius gauge\ntasmota_global_temperature_celsius %s\n"), parameter); + WritePromMetricDec(PSTR("global_temperature_celsius"), kPromMetricGauge, + TasmotaGlobal.temperature_celsius, Settings->flag2.temperature_resolution, + nullptr); } + if (TasmotaGlobal.humidity != 0) { - dtostrfd(TasmotaGlobal.humidity, Settings->flag2.humidity_resolution, parameter); - WSContentSend_P(PSTR("# TYPE tasmota_global_humidity_percentage gauge\ntasmota_global_humidity_percentage %s\n"), parameter); + WritePromMetricDec(PSTR("global_humidity_percentage"), kPromMetricGauge, + TasmotaGlobal.humidity, Settings->flag2.humidity_resolution, + nullptr); } + if (TasmotaGlobal.pressure_hpa != 0) { - dtostrfd(TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution, parameter); - WSContentSend_P(PSTR("# TYPE tasmota_global_pressure_hpa gauge\ntasmota_global_pressure_hpa %s\n"), parameter); + WritePromMetricDec(PSTR("global_pressure_hpa"), kPromMetricGauge, + TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution, + nullptr); } // Pseudo-metric providing metadata about the free memory. - #ifdef ESP32 - int32_t freeMaxMem = 100 - (int32_t)(ESP_getMaxAllocHeap() * 100 / ESP_getFreeHeap()); - WSContentSend_PD(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"Ram\"} %d\n"), ESP_getFreeHeap()); - WSContentSend_PD(PSTR("# TYPE tasmota_memory_ratio gauge\ntasmota_memory_ratio{memory=\"Fragmentation\"} %d\n"), freeMaxMem / 100); - if (UsePSRAM()) { - WSContentSend_P(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"Psram\"} %d\n"), ESP.getFreePsram() ); - } - #else // ESP32 - WSContentSend_PD(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"ram\"} %d\n"), ESP_getFreeHeap()); - #endif // ESP32 +#ifdef ESP32 + int32_t freeMaxMem = 100 - (int32_t)(ESP_getMaxAllocHeap() * 100 / ESP_getFreeHeap()); + + WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge, + ESP_getFreeHeap(), PSTR("memory"), PSTR("Ram"), nullptr); + + // FIXME: Always truncated to integer + WritePromMetricInt32(PSTR("memory_ratio"), kPromMetricGauge, + freeMaxMem / 100, PSTR("memory"), PSTR("Fragmentation"), nullptr); + + if (UsePSRAM()) { + WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge, + ESP.getFreePsram(), PSTR("memory"), PSTR("Psram"), nullptr); + } +#else // ESP32 + WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge, + ESP_getFreeHeap(), PSTR("memory"), PSTR("ram"), nullptr); +#endif // ESP32 #ifdef USE_ENERGY_SENSOR - dtostrfd(Energy.voltage[0], Settings->flag2.voltage_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_voltage_volts gauge\nenergy_voltage_volts %s\n"), parameter); - dtostrfd(Energy.current[0], Settings->flag2.current_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_current_amperes gauge\nenergy_current_amperes %s\n"), parameter); - dtostrfd(Energy.active_power[0], Settings->flag2.wattage_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_power_active_watts gauge\nenergy_power_active_watts %s\n"), parameter); - dtostrfd(Energy.daily, Settings->flag2.energy_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_power_kilowatts_daily counter\nenergy_power_kilowatts_daily %s\n"), parameter); - dtostrfd(Energy.total, Settings->flag2.energy_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_power_kilowatts_total counter\nenergy_power_kilowatts_total %s\n"), parameter); + // TODO: Don't disable prefix on energy metrics + WritePromMetricDec(PSTR("energy_voltage_volts"), + kPromMetricGauge | kPromMetricNoPrefix, + Energy.voltage[0], Settings->flag2.voltage_resolution, nullptr); + WritePromMetricDec(PSTR("energy_current_amperes"), + kPromMetricGauge | kPromMetricNoPrefix, + Energy.current[0], Settings->flag2.current_resolution, nullptr); + WritePromMetricDec(PSTR("energy_power_active_watts"), + kPromMetricGauge | kPromMetricNoPrefix, + Energy.active_power[0], Settings->flag2.wattage_resolution, nullptr); + WritePromMetricDec(PSTR("energy_power_kilowatts_daily"), + kPromMetricCounter | kPromMetricNoPrefix, + Energy.daily, Settings->flag2.energy_resolution, nullptr); + WritePromMetricDec(PSTR("energy_power_kilowatts_total"), + kPromMetricCounter | kPromMetricNoPrefix, + Energy.total, Settings->flag2.energy_resolution, nullptr); #endif for (uint32_t device = 0; device < TasmotaGlobal.devices_present; device++) { power_t mask = 1 << device; - WSContentSend_P(PSTR("# TYPE relay%d_state gauge\nrelay%d_state %d\n"), device+1, device+1, (TasmotaGlobal.power & mask)); + // TODO: Don't disable prefix + snprintf_P(namebuf, sizeof(namebuf), PSTR("relay%d_state"), device + 1); + WritePromMetricInt32(namebuf, kPromMetricGauge | kPromMetricNoPrefix, + (TasmotaGlobal.power & mask), nullptr); } ResponseClear(); @@ -169,9 +277,12 @@ void HandleMetrics(void) { if (value != nullptr && isdigit(value[0])) { String sensor = FormatMetricName(key2.getStr()); String type = FormatMetricName(key3.getStr()); - const char *unit = UnitfromType(type.c_str()); //grab base unit corresponding to type - WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\"} %s\n"), - type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value); //build metric as "# TYPE tasmota_sensors_%type%_%unit% gauge\ntasmotasensors_%type%_%unit%{sensor=%sensor%"} %value%"" + + snprintf_P(namebuf, sizeof(namebuf), PSTR("sensors_%s_%s"), + type.c_str(), UnitfromType(type.c_str())); + WritePromMetricStr(namebuf, kPromMetricGauge, value, + PSTR("sensor"), sensor.c_str(), + nullptr); } } } else { @@ -179,14 +290,19 @@ void HandleMetrics(void) { if (value != nullptr && isdigit(value[0])) { String sensor = FormatMetricName(key1.getStr()); String type = FormatMetricName(key2.getStr()); - const char *unit = UnitfromType(type.c_str()); if (strcmp(type.c_str(), "totalstarttime") != 0) { // this metric causes Prometheus of fail + snprintf_P(namebuf, sizeof(namebuf), PSTR("sensors_%s_%s"), + type.c_str(), UnitfromType(type.c_str())); + if (strcmp(type.c_str(), "id") == 0) { // this metric is NaN, so convert it to a label, see Wi-Fi metrics above - WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\",id=\"%s\"} 1\n"), - type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value); + WritePromMetricInt32(namebuf, kPromMetricGauge, 1, + PSTR("sensor"), sensor.c_str(), + PSTR("id"), value, + nullptr); } else { - WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\"} %s\n"), - type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value); + WritePromMetricStr(namebuf, kPromMetricGauge, value, + PSTR("sensor"), sensor.c_str(), + nullptr); } } } @@ -195,8 +311,11 @@ void HandleMetrics(void) { } else { const char *value = value1.getStr(nullptr); String sensor = FormatMetricName(key1.getStr()); + if (value != nullptr && isdigit(value[0] && strcmp(sensor.c_str(), "time") != 0)) { //remove false 'time' metric - WSContentSend_P(PSTR("# TYPE tasmota_sensors gauge\ntasmota_sensors{sensor=\"%s\"} %s\n"), sensor.c_str(), value); + WritePromMetricStr(PSTR("sensors"), kPromMetricGauge, value, + PSTR("sensor"), sensor.c_str(), + nullptr); } } } From 718f5fc9ab090911bb68f82bdcb0840802820ff8 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Mon, 19 Jul 2021 13:09:55 +0200 Subject: [PATCH 3/3] prometheus: Unify memory metrics The "memory fragmentation" value named "memory_ratio" was always truncated to an integer, so in all likelyhood 0. It didn't work anyway until the incorrect line termination was fixed in an earlier commit. Neither could the Psram metric be parsed correctly due to the the same incorrect line termination. With this change memory usage is reported in line with Prometheus' upstream recommendations (https://prometheus.io/docs/practices/naming/). Labels are no longer used to separate distinct dimensions. Total and free memory as well as the maximum allocation size are reported as separate metrics where available while labels are used to differenciate the separate kinds of memory (heap on all, psram on ESP32). Label values are now also lowercase on ESP32 to match ESP8266. Metrics should report their base values, not the result of a calculation. Therefore the already non-working "fragmentation" metric is dropped. It can easily be calculated in PromQL instead. The renaming of metrics and label values makes this a breaking change, especially on ESP32. With the aforementioned formatting errors which made them unusable that shouldn't be a problem. Signed-off-by: Michael Hanselmann --- tasmota/xsns_75_prometheus.ino | 58 ++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/tasmota/xsns_75_prometheus.ino b/tasmota/xsns_75_prometheus.ino index 8640a8d02..b4a3e602c 100644 --- a/tasmota/xsns_75_prometheus.ino +++ b/tasmota/xsns_75_prometheus.ino @@ -22,10 +22,13 @@ * Prometheus support * * The text format for metrics, labels and values is documented at [1]. Only - * the UTF-8 text encoding is supported. + * the UTF-8 text encoding is supported. [2] describes how metrics and labels + * should be named. * * [1] * https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md + * [2] + * https://github.com/prometheus/docs/blob/master/content/docs/practices/naming.md * \*********************************************************************************************/ @@ -161,6 +164,28 @@ void WritePromMetricStr(const char *name, uint8_t flags, const char *value, ...) va_end(labels); } +// Sentinel value for known memory metrics, chosen to unlikely match actual +// values. +const uint32_t kPromMemoryUnknown = 0xFFFFFFFF - 1; + +// Write metrics providing information about used and available memory. +void WritePromMemoryMetrics(const char *type, uint32_t size, uint32_t avail, uint32_t max_alloc) { + if (size != kPromMemoryUnknown) { + WritePromMetricInt32(PSTR("memory_size_bytes"), kPromMetricGauge, size, + PSTR("memory"), type, nullptr); + } + + WritePromMetricInt32(PSTR("memory_free_bytes"), kPromMetricGauge, avail, + PSTR("memory"), type, nullptr); + + if (max_alloc != kPromMemoryUnknown) { + // The largest contiguous free memory block, useful for checking + // fragmentation. + WritePromMetricInt32(PSTR("memory_max_alloc_bytes"), kPromMetricGauge, max_alloc, + PSTR("memory"), type, nullptr); + } +} + void HandleMetrics(void) { if (!HttpCheckPriviledgedAccess()) { return; } @@ -211,25 +236,26 @@ void HandleMetrics(void) { nullptr); } - // Pseudo-metric providing metadata about the free memory. + WritePromMemoryMetrics(PSTR("heap"), #ifdef ESP32 - int32_t freeMaxMem = 100 - (int32_t)(ESP_getMaxAllocHeap() * 100 / ESP_getFreeHeap()); - - WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge, - ESP_getFreeHeap(), PSTR("memory"), PSTR("Ram"), nullptr); - - // FIXME: Always truncated to integer - WritePromMetricInt32(PSTR("memory_ratio"), kPromMetricGauge, - freeMaxMem / 100, PSTR("memory"), PSTR("Fragmentation"), nullptr); + ESP.getHeapSize(), +#else + kPromMemoryUnknown, +#endif + ESP_getFreeHeap(), +#ifdef ESP32 + ESP_getMaxAllocHeap() +#else + kPromMemoryUnknown +#endif + ); +#ifdef ESP32 if (UsePSRAM()) { - WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge, - ESP.getFreePsram(), PSTR("memory"), PSTR("Psram"), nullptr); + WritePromMemoryMetrics(PSTR("psram"), ESP.getPsramSize(), + ESP.getFreePsram(), ESP.getMaxAllocPsram()); } -#else // ESP32 - WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge, - ESP_getFreeHeap(), PSTR("memory"), PSTR("ram"), nullptr); -#endif // ESP32 +#endif #ifdef USE_ENERGY_SENSOR // TODO: Don't disable prefix on energy metrics