Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StatsD component #6642

Open
wants to merge 23 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ esphome/components/st7701s/* @clydebarrow
esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931
Expand Down
63 changes: 63 additions & 0 deletions esphome/components/statsd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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,
)

CODEOWNERS = ["@Links2004"]

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 changes: 156 additions & 0 deletions esphome/components/statsd/statsd.cpp
Original file line number Diff line number Diff line change
@@ -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 ESP8266
Links2004 marked this conversation as resolved.
Show resolved Hide resolved
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 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::BEFORE_CONNECTION; }

#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() {
jesserockz marked this conversation as resolved.
Show resolved Hide resolved
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 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 changes: 86 additions & 0 deletions esphome/components/statsd/statsd.h
Original file line number Diff line number Diff line change
@@ -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 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 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