/* xsns_75_prometheus.ino - Web based information for Tasmota Copyright (C) 2021 Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_PROMETHEUS /*********************************************************************************************\ * Prometheus support * * The text format for metrics, labels and values is documented at [1]. Only * 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 * \*********************************************************************************************/ #define XSNS_75 75 // Find appropriate unit for measurement type. const char *UnitfromType(const char *type) { if (strcmp(type, "time") == 0) { return "seconds"; } if (strcmp(type, "temperature") == 0 || strcmp(type, "dewpoint") == 0) { return "celsius"; } if (strcmp(type, "pressure") == 0) { return "hpa"; } if (strcmp(type, "voltage") == 0) { return "volts"; } if (strcmp(type, "current") == 0) { return "amperes"; } if (strcmp(type, "mass") == 0) { return "grams"; } if (strcmp(type, "carbondioxide") == 0) { return "ppm"; } if (strcmp(type, "humidity") == 0) { return "percentage"; } if (strcmp(type, "id") == 0) { return "untyped"; } return ""; } // Replace spaces and periods in metric name to match Prometheus metrics // convention. String FormatMetricName(const char *metric) { String formatted = metric; formatted.toLowerCase(); formatted.replace(" ", "_"); formatted.replace(".", "_"); 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); } // Sentinel value for unknown 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; } AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP "Prometheus")); WSContentBegin(200, CT_PLAIN); char namebuf[64]; // Pseudo-metric providing metadata about the running firmware version. 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. WritePromMetricInt32(PSTR("wifi_station_info"), kPromMetricGauge, 1, PSTR("bssid"), WiFi.BSSIDstr().c_str(), PSTR("ssid"), WiFi.SSID().c_str(), nullptr); // Wi-Fi Signal strength WritePromMetricInt32(PSTR("wifi_station_signal_dbm"), kPromMetricGauge, WiFi.RSSI(), PSTR("mac_address"), WiFi.BSSIDstr().c_str(), nullptr); if (!isnan(TasmotaGlobal.temperature_celsius)) { WritePromMetricDec(PSTR("global_temperature_celsius"), kPromMetricGauge, TasmotaGlobal.temperature_celsius, Settings->flag2.temperature_resolution, nullptr); } if (TasmotaGlobal.humidity != 0) { WritePromMetricDec(PSTR("global_humidity_percentage"), kPromMetricGauge, TasmotaGlobal.humidity, Settings->flag2.humidity_resolution, nullptr); } if (TasmotaGlobal.pressure_hpa != 0) { WritePromMetricDec(PSTR("global_pressure_hpa"), kPromMetricGauge, TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution, nullptr); } WritePromMemoryMetrics(PSTR("heap"), #ifdef ESP32 ESP.getHeapSize(), #else kPromMemoryUnknown, #endif ESP_getFreeHeap(), #ifdef ESP32 ESP_getMaxAllocHeap() #else kPromMemoryUnknown #endif ); #ifdef ESP32 if (UsePSRAM()) { WritePromMemoryMetrics(PSTR("psram"), ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMaxAllocPsram()); } #endif #ifdef USE_ENERGY_SENSOR // 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; // 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(); MqttShowSensor(); //Pull sensor data String jsonStr = TasmotaGlobal.mqtt_data; JsonParser parser((char *)jsonStr.c_str()); JsonParserObject root = parser.getRootObject(); if (root) { // did JSON parsing succeed? for (auto key1 : root) { JsonParserToken value1 = key1.getValue(); if (value1.isObject()) { JsonParserObject Object2 = value1.getObject(); for (auto key2 : Object2) { JsonParserToken value2 = key2.getValue(); if (value2.isObject()) { JsonParserObject Object3 = value2.getObject(); for (auto key3 : Object3) { const char *value = key3.getValue().getStr(nullptr); if (value != nullptr && isdigit(value[0])) { String sensor = FormatMetricName(key2.getStr()); String type = FormatMetricName(key3.getStr()); 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 { const char *value = value2.getStr(nullptr); if (value != nullptr && isdigit(value[0])) { String sensor = FormatMetricName(key1.getStr()); String type = FormatMetricName(key2.getStr()); 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 WritePromMetricInt32(namebuf, kPromMetricGauge, 1, PSTR("sensor"), sensor.c_str(), PSTR("id"), value, nullptr); } else { WritePromMetricStr(namebuf, kPromMetricGauge, value, PSTR("sensor"), sensor.c_str(), nullptr); } } } } } } 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 WritePromMetricStr(PSTR("sensors"), kPromMetricGauge, value, PSTR("sensor"), sensor.c_str(), nullptr); } } } } WSContentEnd(); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xsns75(uint8_t function) { bool result = false; switch (function) { case FUNC_WEB_ADD_HANDLER: WebServer_on(PSTR("/metrics"), HandleMetrics); break; } return result; } #endif // USE_PROMETHEUS