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 5 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
66 changes: 66 additions & 0 deletions esphome/components/statsd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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,
CONF_UPDATE_INTERVAL,
)

CODEOWNERS = ["@Links2004"]

CONF_HOST = "host"
CONF_PREFIX = "prefix"

statsd_component_ns = cg.esphome_ns.namespace("statsd")
statsdComponent = statsd_component_ns.class_("StatsdComponent", cg.Component)
Links2004 marked this conversation as resolved.
Show resolved Hide resolved


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),
cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds,
}
).extend(cv.COMPONENT_SCHEMA)
Links2004 marked this conversation as resolved.
Show resolved Hide resolved


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))
159 changes: 159 additions & 0 deletions esphome/components/statsd/statsd.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#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_ARDUINO
#ifdef ESP8266
#error ESP8266 does not Support UDP socket
Links2004 marked this conversation as resolved.
Show resolved Hide resolved
#endif
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_ARDUINO
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 USE_ARDUINO
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
87 changes: 87 additions & 0 deletions esphome/components/statsd/statsd.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#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_ARDUINO
#include "WiFiUdp.h"
#include "IPAddress.h"
#endif
Links2004 marked this conversation as resolved.
Show resolved Hide resolved

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() : PollingComponent(10000){};
Links2004 marked this conversation as resolved.
Show resolved Hide resolved
~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_ARDUINO
WiFiUDP sock_;
#else
std::unique_ptr<esphome::socket::Socket> sock_;
struct sockaddr_in destination_;
#endif

void send_(std::string *out);
};

} // namespace statsd
} // namespace esphome
7 changes: 6 additions & 1 deletion script/clang-tidy
Links2004 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,12 @@ def main():
if args.fix and failed_files:
print("Applying fixes ...")
try:
subprocess.call(["clang-apply-replacements-14", tmpdir])
try:
subprocess.call(["clang-apply-replacements-14", tmpdir])
except FileNotFoundError:
subprocess.call(["clang-apply-replacements", tmpdir])
except FileNotFoundError:
print("Error please install clang-apply-replacements-14 or clang-apply-replacements.\n", file=sys.stderr)
except:
print("Error applying fixes.\n", file=sys.stderr)
raise
Expand Down
12 changes: 12 additions & 0 deletions tests/test11.5.yaml
Links2004 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ mqtt:
ESP_LOGD("Mqtt Test", "testing/sensor/testing_sensor/state=[%s]", x.c_str());
# yamllint enable rule:line-length

statsd:
host: "192.168.1.1"
port: 8125
prefix: esphome
update_interval: 60s
sensors:
id: adc_sensor_p32
name: adc.32
binary_sensors:
id: io0_button
name: gpio.0

vbus:
- uart_id: uart_2

Expand Down