mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00:00 
			
		
		
		
	Add StatsD component (#6642)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -385,6 +385,7 @@ esphome/components/st7701s/* @clydebarrow | |||||||
| esphome/components/st7735/* @SenexCrenshaw | esphome/components/st7735/* @SenexCrenshaw | ||||||
| esphome/components/st7789v/* @kbx81 | esphome/components/st7789v/* @kbx81 | ||||||
| esphome/components/st7920/* @marsjan155 | esphome/components/st7920/* @marsjan155 | ||||||
|  | esphome/components/statsd/* @Links2004 | ||||||
| esphome/components/substitutions/* @esphome/core | esphome/components/substitutions/* @esphome/core | ||||||
| esphome/components/sun/* @OttoWinter | esphome/components/sun/* @OttoWinter | ||||||
| esphome/components/sun_gtil2/* @Mat931 | esphome/components/sun_gtil2/* @Mat931 | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								esphome/components/statsd/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								esphome/components/statsd/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import sensor, binary_sensor | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_PORT, | ||||||
|  |     CONF_NAME, | ||||||
|  |     CONF_SENSORS, | ||||||
|  |     CONF_BINARY_SENSORS, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["socket"] | ||||||
|  | CODEOWNERS = ["@Links2004"] | ||||||
|  | DEPENDENCIES = ["network"] | ||||||
|  |  | ||||||
|  | CONF_HOST = "host" | ||||||
|  | CONF_PREFIX = "prefix" | ||||||
|  |  | ||||||
|  | statsd_component_ns = cg.esphome_ns.namespace("statsd") | ||||||
|  | StatsdComponent = statsd_component_ns.class_("StatsdComponent", cg.PollingComponent) | ||||||
|  |  | ||||||
|  | CONFIG_SENSORS_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(sensor.Sensor), | ||||||
|  |         cv.Required(CONF_NAME): cv.string_strict, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_BINARY_SENSORS_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor), | ||||||
|  |         cv.Required(CONF_NAME): cv.string_strict, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(StatsdComponent), | ||||||
|  |         cv.Required(CONF_HOST): cv.string_strict, | ||||||
|  |         cv.Optional(CONF_PORT, default=8125): cv.port, | ||||||
|  |         cv.Optional(CONF_PREFIX, default=""): cv.string_strict, | ||||||
|  |         cv.Optional(CONF_SENSORS): cv.ensure_list(CONFIG_SENSORS_SCHEMA), | ||||||
|  |         cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list(CONFIG_BINARY_SENSORS_SCHEMA), | ||||||
|  |     } | ||||||
|  | ).extend(cv.polling_component_schema("10s")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     cg.add( | ||||||
|  |         var.configure( | ||||||
|  |             config.get(CONF_HOST), | ||||||
|  |             config.get(CONF_PORT), | ||||||
|  |             config.get(CONF_PREFIX), | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     for sensor_cfg in config.get(CONF_SENSORS, []): | ||||||
|  |         s = await cg.get_variable(sensor_cfg[CONF_ID]) | ||||||
|  |         cg.add(var.register_sensor(sensor_cfg[CONF_NAME], s)) | ||||||
|  |  | ||||||
|  |     for sensor_cfg in config.get(CONF_BINARY_SENSORS, []): | ||||||
|  |         s = await cg.get_variable(sensor_cfg[CONF_ID]) | ||||||
|  |         cg.add(var.register_binary_sensor(sensor_cfg[CONF_NAME], s)) | ||||||
							
								
								
									
										156
									
								
								esphome/components/statsd/statsd.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								esphome/components/statsd/statsd.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | |||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | #include "statsd.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace statsd { | ||||||
|  |  | ||||||
|  | // send UDP packet if we reach 1Kb packed size | ||||||
|  | // this is needed since statsD does not support fragmented UDP packets | ||||||
|  | static const uint16_t SEND_THRESHOLD = 1024; | ||||||
|  |  | ||||||
|  | static const char *const TAG = "statsD"; | ||||||
|  |  | ||||||
|  | void StatsdComponent::setup() { | ||||||
|  | #ifndef USE_ESP8266 | ||||||
|  |   this->sock_ = esphome::socket::socket(AF_INET, SOCK_DGRAM, 0); | ||||||
|  |  | ||||||
|  |   struct sockaddr_in source; | ||||||
|  |   source.sin_family = AF_INET; | ||||||
|  |   source.sin_addr.s_addr = htonl(INADDR_ANY); | ||||||
|  |   source.sin_port = htons(this->port_); | ||||||
|  |   this->sock_->bind((struct sockaddr *) &source, sizeof(source)); | ||||||
|  |  | ||||||
|  |   this->destination_.sin_family = AF_INET; | ||||||
|  |   this->destination_.sin_port = htons(this->port_); | ||||||
|  |   this->destination_.sin_addr.s_addr = inet_addr(this->host_); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | StatsdComponent::~StatsdComponent() { | ||||||
|  | #ifndef USE_ESP8266 | ||||||
|  |   if (!this->sock_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->sock_->close(); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void StatsdComponent::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "statsD:"); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  host: %s", this->host_); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  port: %d", this->port_); | ||||||
|  |   if (this->prefix_) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  prefix: %s", this->prefix_); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(TAG, "  metrics:"); | ||||||
|  |   for (sensors_t s : this->sensors_) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "    - name: %s", s.name); | ||||||
|  |     ESP_LOGCONFIG(TAG, "      type: %d", s.type); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float StatsdComponent::get_setup_priority() const { return esphome::setup_priority::AFTER_WIFI; } | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  | void StatsdComponent::register_sensor(const char *name, esphome::sensor::Sensor *sensor) { | ||||||
|  |   sensors_t s; | ||||||
|  |   s.name = name; | ||||||
|  |   s.sensor = sensor; | ||||||
|  |   s.type = TYPE_SENSOR; | ||||||
|  |   this->sensors_.push_back(s); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  | void StatsdComponent::register_binary_sensor(const char *name, esphome::binary_sensor::BinarySensor *binary_sensor) { | ||||||
|  |   sensors_t s; | ||||||
|  |   s.name = name; | ||||||
|  |   s.binary_sensor = binary_sensor; | ||||||
|  |   s.type = TYPE_BINARY_SENSOR; | ||||||
|  |   this->sensors_.push_back(s); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | void StatsdComponent::update() { | ||||||
|  |   std::string out; | ||||||
|  |   out.reserve(SEND_THRESHOLD); | ||||||
|  |  | ||||||
|  |   for (sensors_t s : this->sensors_) { | ||||||
|  |     double val = 0; | ||||||
|  |     switch (s.type) { | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |       case TYPE_SENSOR: | ||||||
|  |         if (!s.sensor->has_state()) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         val = s.sensor->state; | ||||||
|  |         break; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |       case TYPE_BINARY_SENSOR: | ||||||
|  |         if (!s.binary_sensor->has_state()) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         // map bool to double | ||||||
|  |         if (s.binary_sensor->state) { | ||||||
|  |           val = 1; | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  | #endif | ||||||
|  |       default: | ||||||
|  |         ESP_LOGE(TAG, "type not known, name: %s type: %d", s.name, s.type); | ||||||
|  |         continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // statsD gauge: | ||||||
|  |     // https://github.com/statsd/statsd/blob/master/docs/metric_types.md | ||||||
|  |     // This implies you can't explicitly set a gauge to a negative number without first setting it to zero. | ||||||
|  |     if (val < 0) { | ||||||
|  |       if (this->prefix_) { | ||||||
|  |         out.append(str_sprintf("%s.", this->prefix_)); | ||||||
|  |       } | ||||||
|  |       out.append(str_sprintf("%s:0|g\n", s.name)); | ||||||
|  |     } | ||||||
|  |     if (this->prefix_) { | ||||||
|  |       out.append(str_sprintf("%s.", this->prefix_)); | ||||||
|  |     } | ||||||
|  |     out.append(str_sprintf("%s:%f|g\n", s.name, val)); | ||||||
|  |  | ||||||
|  |     if (out.length() > SEND_THRESHOLD) { | ||||||
|  |       this->send_(&out); | ||||||
|  |       out.clear(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->send_(&out); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void StatsdComponent::send_(std::string *out) { | ||||||
|  |   if (out->empty()) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |   IPAddress ip; | ||||||
|  |   ip.fromString(this->host_); | ||||||
|  |  | ||||||
|  |   this->sock_.beginPacket(ip, this->port_); | ||||||
|  |   this->sock_.write((const uint8_t *) out->c_str(), out->length()); | ||||||
|  |   this->sock_.endPacket(); | ||||||
|  |  | ||||||
|  | #else | ||||||
|  |   if (!this->sock_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   int n_bytes = this->sock_->sendto(out->c_str(), out->length(), 0, reinterpret_cast<sockaddr *>(&this->destination_), | ||||||
|  |                                     sizeof(this->destination_)); | ||||||
|  |   if (n_bytes != out->length()) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to send UDP packed (%d of %d)", n_bytes, out->length()); | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace statsd | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										86
									
								
								esphome/components/statsd/statsd.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								esphome/components/statsd/statsd.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/socket/socket.h" | ||||||
|  | #include "esphome/components/network/ip_address.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  | #include "esphome/components/binary_sensor/binary_sensor.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_LOGGER | ||||||
|  | #include "esphome/components/logger/logger.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  | #include "WiFiUdp.h" | ||||||
|  | #include "IPAddress.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace statsd { | ||||||
|  |  | ||||||
|  | using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR }; | ||||||
|  |  | ||||||
|  | using sensors_t = struct { | ||||||
|  |   const char *name; | ||||||
|  |   sensor_type_t type; | ||||||
|  |   union { | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |     esphome::sensor::Sensor *sensor; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |     esphome::binary_sensor::BinarySensor *binary_sensor; | ||||||
|  | #endif | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class StatsdComponent : public PollingComponent { | ||||||
|  |  public: | ||||||
|  |   ~StatsdComponent(); | ||||||
|  |  | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   void update() override; | ||||||
|  |   float get_setup_priority() const override; | ||||||
|  |  | ||||||
|  |   void configure(const char *host, uint16_t port, const char *prefix) { | ||||||
|  |     this->host_ = host; | ||||||
|  |     this->port_ = port; | ||||||
|  |     this->prefix_ = prefix; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |   void register_sensor(const char *name, esphome::sensor::Sensor *sensor); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |   void register_binary_sensor(const char *name, esphome::binary_sensor::BinarySensor *binary_sensor); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   const char *host_; | ||||||
|  |   const char *prefix_; | ||||||
|  |   uint16_t port_; | ||||||
|  |  | ||||||
|  |   std::vector<sensors_t> sensors_; | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |   WiFiUDP sock_; | ||||||
|  | #else | ||||||
|  |   std::unique_ptr<esphome::socket::Socket> sock_; | ||||||
|  |   struct sockaddr_in destination_; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   void send_(std::string *out); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace statsd | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										29
									
								
								tests/components/statsD/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								tests/components/statsD/common.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | wifi: | ||||||
|  |   ssid: MySSID | ||||||
|  |   password: password1 | ||||||
|  |  | ||||||
|  | statsd: | ||||||
|  |   host: "192.168.1.1" | ||||||
|  |   port: 8125 | ||||||
|  |   prefix: esphome | ||||||
|  |   update_interval: 60s | ||||||
|  |   sensors: | ||||||
|  |     id: s | ||||||
|  |     name: sensors | ||||||
|  |   binary_sensors: | ||||||
|  |     id: bs | ||||||
|  |     name: binary_sensors | ||||||
|  |  | ||||||
|  | sensor: | ||||||
|  |   - platform: template | ||||||
|  |     id: s | ||||||
|  |     name: "42.1" | ||||||
|  |     lambda: |- | ||||||
|  |         return 42.1f; | ||||||
|  |  | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: template | ||||||
|  |     id: bs | ||||||
|  |     name: "On" | ||||||
|  |     lambda: |- | ||||||
|  |         return true; | ||||||
							
								
								
									
										2
									
								
								tests/components/statsD/test.bk72xx-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/statsD/test.bk72xx-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
							
								
								
									
										2
									
								
								tests/components/statsD/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/statsD/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
							
								
								
									
										2
									
								
								tests/components/statsD/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/statsD/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
							
								
								
									
										2
									
								
								tests/components/statsD/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/statsD/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
							
								
								
									
										2
									
								
								tests/components/statsD/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/statsD/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
							
								
								
									
										2
									
								
								tests/components/statsD/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/statsD/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
							
								
								
									
										2
									
								
								tests/components/statsD/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/statsD/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
		Reference in New Issue
	
	Block a user