diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 2f0d179eba..be0a180a9d 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -4,7 +4,7 @@ from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.const import ( CONF_CSS_INCLUDE, CONF_CSS_URL, CONF_ID, CONF_JS_INCLUDE, CONF_JS_URL, CONF_PORT, - CONF_AUTH, CONF_USERNAME, CONF_PASSWORD) + CONF_AUTH, CONF_USERNAME, CONF_PASSWORD, CONF_PROMETHEUS) from esphome.core import coroutine_with_priority AUTO_LOAD = ['json', 'web_server_base'] @@ -19,6 +19,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_CSS_INCLUDE): cv.file_, cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string, cv.Optional(CONF_JS_INCLUDE): cv.file_, + cv.Optional(CONF_PROMETHEUS, default=False): cv.boolean, cv.Optional(CONF_AUTH): cv.Schema({ cv.Required(CONF_USERNAME): cv.string_strict, cv.Required(CONF_PASSWORD): cv.string_strict, @@ -49,3 +50,5 @@ def to_code(config): cg.add_define('WEBSERVER_JS_INCLUDE') with open(config[CONF_JS_INCLUDE], "r") as myfile: cg.add(var.set_js_include(myfile.read())) + if config[CONF_PROMETHEUS]: + cg.add_define('WEBSERVER_PROMETHEUS') diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 48a47080b2..7f0bb0f973 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -572,6 +572,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; +#ifdef WEBSERVER_PROMETHEUS + if (request->url() == "/metrics") + return true; +#endif + #ifdef WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") return true; @@ -632,6 +637,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } +#ifdef WEBSERVER_PROMETHEUS + if (request->url() == "/metrics") { + this->prometheus.handle_request(request); + return; + } +#endif + #ifdef WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") { this->handle_css_request(request); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index b3bf2ef7f7..77fcb1e88c 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -3,6 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/components/web_server_base/web_server_base.h" +#ifdef WEBSERVER_PROMETHEUS +#include "esphome/components/web_server/web_server_prometheus.h" +#endif #include @@ -170,6 +173,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { const char *css_include_{nullptr}; const char *js_url_{nullptr}; const char *js_include_{nullptr}; + +#ifdef WEBSERVER_PROMETHEUS + WebServerPrometheus prometheus; +#endif }; } // namespace web_server diff --git a/esphome/components/web_server/web_server_prometheus.cpp b/esphome/components/web_server/web_server_prometheus.cpp new file mode 100644 index 0000000000..4ba75c1673 --- /dev/null +++ b/esphome/components/web_server/web_server_prometheus.cpp @@ -0,0 +1,344 @@ +#include "web_server.h" +#include "web_server_prometheus.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/util.h" +#include "esphome/components/json/json_util.h" + +#include "StreamString.h" + +#include + +#ifdef USE_LOGGER +#include +#endif + +namespace esphome { +namespace web_server { + +void WebServerPrometheus::handle_request(AsyncWebServerRequest *request) { + AsyncResponseStream *stream = request->beginResponseStream("text/plain"); + +#ifdef USE_SENSOR + this->sensor_type_(stream); + for (auto *obj : App.get_sensors()) + this->sensor_row_(stream, obj); +#endif + +#ifdef USE_BINARY_SENSOR + this->binary_sensor_type_(stream); + for (auto *obj : App.get_binary_sensors()) + this->binary_sensor_row_(stream, obj); +#endif + +#ifdef USE_FAN + this->fan_type_(stream); + for (auto *obj : App.get_fans()) + this->fan_row_(stream, obj); +#endif + +#ifdef USE_LIGHT + this->light_type_(stream); + for (auto *obj : App.get_lights()) + this->light_row_(stream, obj); +#endif + +#ifdef USE_COVER + this->cover_type_(stream); + for (auto *obj : App.get_covers()) + this->cover_row_(stream, obj); +#endif + +#ifdef USE_SWITCH + this->switch_type_(stream); + for (auto *obj : App.get_switches()) + this->switch_row_(stream, obj); +#endif + + request->send(stream); +} + +// Type-specific implementation +#ifdef USE_SENSOR +void WebServerPrometheus::sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); +} +void WebServerPrometheus::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",unit=\"")); + stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(F("\"} ")); + stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_BINARY_SENSOR +void WebServerPrometheus::binary_sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_binary_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); +} +void WebServerPrometheus::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_FAN +void WebServerPrometheus::fan_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_fan_value GAUGE\n")); + stream->print(F("#TYPE esphome_fan_failed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_speed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); +} +void WebServerPrometheus::fan_row_(AsyncResponseStream *stream, fan::FanState *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_fan_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_fan_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + // Speed if available + if (obj->get_traits().supports_speed()) { + stream->print(F("esphome_fan_speed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->speed); + stream->print('\n'); + } + // Oscillation if available + if (obj->get_traits().supports_oscillation()) { + stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->oscillating); + stream->print('\n'); + } + } else { + // Invalid state + stream->print(F("esphome_fan_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_LIGHT +void WebServerPrometheus::light_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_light_state GAUGE\n")); + stream->print(F("#TYPE esphome_light_color GAUGE\n")); + stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); +} +void WebServerPrometheus::light_row_(AsyncResponseStream *stream, light::LightState *obj) { + if (obj->is_internal()) + return; + // State + stream->print(F("esphome_light_state{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->remote_values.is_on()); + stream->print(F("\n")); + // Brightness and RGBW + light::LightColorValues color = obj->current_values; + float brightness, r, g, b, w; + color.as_brightness(&brightness); + color.as_rgbw(&r, &g, &b, &w); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(F("\n")); + // Effect + std::string effect = obj->get_effect_name(); + if (effect == "None") { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"None\"} 0\n")); + } else { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"")); + stream->print(effect.c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_COVER +void WebServerPrometheus::cover_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_cover_value GAUGE\n")); + stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); +} +void WebServerPrometheus::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->position)) { + // We have a valid value, output this value + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_cover_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->position); + stream->print('\n'); + if (obj->get_traits().get_supports_tilt()) { + stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->tilt); + stream->print('\n'); + } + } else { + // Invalid state + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_SWITCH +void WebServerPrometheus::switch_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_switch_value GAUGE\n")); + stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); +} +void WebServerPrometheus::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_switch_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_switch_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_switch_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +} // namespace web_server +} // namespace esphome diff --git a/esphome/components/web_server/web_server_prometheus.h b/esphome/components/web_server/web_server_prometheus.h new file mode 100644 index 0000000000..93682c1ed5 --- /dev/null +++ b/esphome/components/web_server/web_server_prometheus.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/controller.h" +#include "esphome/components/web_server_base/web_server_base.h" + +namespace esphome { +namespace web_server { + +class WebServerPrometheus { + public: + WebServerPrometheus(){}; + /// Handle an prometheus metrics request under '/metrics'. + void handle_request(AsyncWebServerRequest *request); + + protected: +#ifdef USE_SENSOR + /// Return the type for prometheus + void sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj); +#endif + +#ifdef USE_BINARY_SENSOR + /// Return the type for prometheus + void binary_sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + /// Return the type for prometheus + void fan_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void fan_row_(AsyncResponseStream *stream, fan::FanState *obj); +#endif + +#ifdef USE_LIGHT + /// Return the type for prometheus + void light_type_(AsyncResponseStream *stream); + /// Return the Light Values state as prometheus data point + void light_row_(AsyncResponseStream *stream, light::LightState *obj); +#endif + +#ifdef USE_COVER + /// Return the type for prometheus + void cover_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void cover_row_(AsyncResponseStream *stream, cover::Cover *obj); +#endif + +#ifdef USE_SWITCH + /// Return the type for prometheus + void switch_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); +#endif +}; + +} // namespace web_server +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 95b2da36d7..5ac2b3c9ca 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -358,6 +358,7 @@ CONF_POWER_SAVE_MODE = 'power_save_mode' CONF_POWER_SUPPLY = 'power_supply' CONF_PRESSURE = 'pressure' CONF_PRIORITY = 'priority' +CONF_PROMETHEUS = 'prometheus' CONF_PROTOCOL = 'protocol' CONF_PULL_MODE = 'pull_mode' CONF_PULSE_LENGTH = 'pulse_length' diff --git a/tests/test1.yaml b/tests/test1.yaml index dae8cfdb21..d9563a470a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -153,6 +153,7 @@ web_server: port: 8080 css_url: https://esphome.io/_static/webserver-v1.min.css js_url: https://esphome.io/_static/webserver-v1.min.js + prometheus: true power_supply: id: 'atx_power_supply' diff --git a/tests/test3.yaml b/tests/test3.yaml index 0d8be590cf..62658c3c4b 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -197,6 +197,7 @@ logger: esp8266_store_log_strings_in_flash: false web_server: + prometheus: true deep_sleep: run_duration: 20s