diff --git a/btproxy.yaml b/btproxy.yaml index 59308f5..e5b6460 100644 --- a/btproxy.yaml +++ b/btproxy.yaml @@ -13,3 +13,5 @@ packages: btproxy: !include templates/btproxy.yaml # Allow HA to use our proxy. api: !include templates/api.yaml + # With idf framework, we can use syslog. + syslog: !include templates/syslog.yaml diff --git a/components/syslog/__init__.py b/components/syslog/__init__.py new file mode 100644 index 0000000..3e392f6 --- /dev/null +++ b/components/syslog/__init__.py @@ -0,0 +1,59 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.const import CONF_ID, CONF_IP_ADDRESS, CONF_PORT, CONF_CLIENT_ID, CONF_LEVEL, CONF_PAYLOAD, CONF_TAG +from esphome.components.logger import LOG_LEVELS, is_log_level + +CONF_STRIP_COLOR_CODES = "strip_color_codes" +CONF_FORWARD_LOGGER = "forward_logger" +CONF_MIN_LOG_LEVEL = "min_log_level" + +DEPENDENCIES = ['logger', 'network'] + +syslog_ns = cg.esphome_ns.namespace('syslog') + +Syslog = syslog_ns.class_('Syslog', cg.Component) +SyslogLogAction = syslog_ns.class_('SyslogLogAction', automation.Action) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Syslog), + cv.Optional(CONF_IP_ADDRESS, default="255.255.255.255"): cv.string_strict, + cv.Optional(CONF_PORT, default=514): cv.port, + cv.Optional(CONF_FORWARD_LOGGER, default=True): cv.boolean, + cv.Optional(CONF_STRIP_COLOR_CODES, default=True): cv.boolean, + cv.Optional(CONF_MIN_LOG_LEVEL, default="DEBUG"): is_log_level, + cv.Optional(CONF_CLIENT_ID): cv.string_strict, +}) + +SYSLOG_LOG_ACTION_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(Syslog), + cv.Required(CONF_LEVEL): cv.templatable(cv.int_range(min=0, max=7)), + cv.Required(CONF_TAG): cv.templatable(cv.string), + cv.Required(CONF_PAYLOAD): cv.templatable(cv.string), +}) + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + cg.add(var.set_forward_logger(config[CONF_FORWARD_LOGGER])) + cg.add(var.set_strip_color_codes(config[CONF_STRIP_COLOR_CODES])) + cg.add(var.set_server_ip_address(config[CONF_IP_ADDRESS])) + cg.add(var.set_server_port(config[CONF_PORT])) + cg.add(var.set_min_log_level(LOG_LEVELS[config[CONF_MIN_LOG_LEVEL]])) + if CONF_CLIENT_ID in config: + cg.add(var.set_hostname(config[CONF_CLIENT_ID])) + +@automation.register_action('syslog.log', SyslogLogAction, SYSLOG_LOG_ACTION_SCHEMA) +def syslog_log_action_to_code(config, action_id, template_arg, args): + parent = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + template_ = yield cg.templatable(config[CONF_LEVEL], args, cg.int) + cg.add(var.set_level(template_)) + template_ = yield cg.templatable(config[CONF_TAG], args, cg.std_string) + cg.add(var.set_tag(template_)) + template_ = yield cg.templatable(config[CONF_PAYLOAD], args, cg.std_string) + cg.add(var.set_payload(template_)) + + yield var diff --git a/components/syslog/syslog.cpp b/components/syslog/syslog.cpp new file mode 100644 index 0000000..f777b65 --- /dev/null +++ b/components/syslog/syslog.cpp @@ -0,0 +1,147 @@ +#include "syslog.h" + +#include + +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +#include "esphome/components/network/util.h" +/* +#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" +#include "esphome/core/version.h" +*/ + +namespace esphome { + +namespace syslog { + +std::string remove_ansi_colors(const std::string& input) { + std::string result; + bool escape_sequence = false; + + for (char c : input) { + if (c == '\033') { // Start of an escape sequence + escape_sequence = true; + } else if (escape_sequence && c == 'm') { // End of an escape sequence + escape_sequence = false; + } else if (!escape_sequence) { // Normal character + result += c; + } + } + + return result; +} + +enum Priority { + EMERG = 0, /* System is unusable */ + ALERT = 1, /* Action must be taken immediately */ + CRIT = 2, /* Critical conditions */ + ERR = 3, /* Error conditions */ + WARNING = 4, /* Warning conditions */ + NOTICE = 5, /* Normal but significant conditions */ + INFO = 6, /* Informational messages */ + DEBUG = 7, /* Debug-level messages */ +}; + +static const uint8_t esphome_to_syslog_log_levels[] = { + /* ESPHOME_LOG_LEVEL_NONE */ Priority::DEBUG, + /* ESPHOME_LOG_LEVEL_ERROR */ Priority::ERR, + /* ESPHOME_LOG_LEVEL_WARN */ Priority::WARNING, + /* ESPHOME_LOG_LEVEL_INFO */ Priority::INFO, + /* ESPHOME_LOG_LEVEL_CONFIG */ Priority::NOTICE, + /* ESPHOME_LOG_LEVEL_DEBUG */ Priority::DEBUG, + /* ESPHOME_LOG_LEVEL_VERBOSE */ Priority::DEBUG, + /* ESPHOME_LOG_LEVEL_VERY_VERBOSE */ Priority::DEBUG, +}; + +static const uint8_t ESPHOME_LOG_LEVELS = 8; + +static const char *TAG = "syslog"; + +Syslog::Syslog() { + this->hostname_ = ::esphome::network::get_use_address(); + this->errors_encountered_ = 0; +} + +void Syslog::setup() { +#ifdef USE_LOGGER + if (logger::global_logger != nullptr && this->forward_logger_) { + logger::global_logger->add_on_log_callback( + [this](int level, const char *tag, const char *message) { + if (level > this->min_log_level_) + return; + + if (this->strip_color_codes_) { + std::string clean_message = remove_ansi_colors(message); + this->log(level, tag, clean_message); + } else { + this->log(level, tag, message); + } + }); + } +#endif + + this->socket_fd_ = ::socket(AF_INET, SOCK_DGRAM, 0); + + memset(&this->destination_, 0, sizeof(this->destination_)); + this->destination_.sin_family = AF_INET; + this->destination_.sin_port = htons(this->server_port_); + this->destination_.sin_addr.s_addr = inet_addr(this->server_ip_address_.c_str()); +} + +void Syslog::loop() { + if (!this->latest_error_message_.empty() && this->errors_encountered_ >= 10) { + ESP_LOGW(TAG, "Failed to send log: %s", latest_error_message_.c_str()); + this->errors_encountered_ = 0; + latest_error_message_.clear(); + } +} + +void Syslog::dump_config() { + ESP_LOGCONFIG(TAG, "Syslog:"); + ESP_LOGCONFIG(TAG, " Server IP address: %s", this->server_ip_address_.c_str()); + ESP_LOGCONFIG(TAG, " Server port: %d", this->server_port_); + ESP_LOGCONFIG(TAG, " Client ID (syslog hostname): %s", this->hostname_.c_str()); +#ifdef USE_LOGGER + ESP_LOGCONFIG(TAG, " Min log level: %d", this->min_log_level_); + ESP_LOGCONFIG(TAG, " Forward logger messages: %s", this->forward_logger_ ? "yes" : "no"); + ESP_LOGCONFIG(TAG, " Strip color codes: %s", this->strip_color_codes_ ? "yes" : "no"); +#endif +} + +void Syslog::log(int level, const std::string &tag, const std::string &msg) { + if (level >= ESPHOME_LOG_LEVELS) { + level = ESPHOME_LOG_LEVELS - 1; + } else if (level < 0) { + level = 0; + } + + std::stringstream payload_stream; + // rsyslog compatible protocol: + // VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID SP [SD-ID]s SP MSG + payload_stream + << "<" << static_cast(esphome_to_syslog_log_levels[level]) << ">1 - " + << this->hostname_ << " " + << App.get_name() << " - - - " << msg; + + std::string payload = payload_stream.str(); + ssize_t bytes_sent = ::sendto( + this->socket_fd_, + payload.c_str(), payload.length(), + 0, + reinterpret_cast(&this->destination_), sizeof(this->destination_)); + + if (bytes_sent != payload.length()) { + // Can't log here, but loop() will pick it up. + this->latest_error_message_ = strerror(errno); + ++this->errors_encountered_; + } +} + +} // namespace syslog +} // namespace esphome diff --git a/components/syslog/syslog.h b/components/syslog/syslog.h new file mode 100644 index 0000000..8850f64 --- /dev/null +++ b/components/syslog/syslog.h @@ -0,0 +1,78 @@ +#pragma once + +/* Potential future improvements: + + - hostname instead of IP address + - custom mapping of log leves to syslog levels + - set custom facility + - protocol support: Structured Data, BSD, IPv6 + - allow specifying time component to get timestamp + - code optimization (e.g. marking methods as HOT or using fewer complex ops) + */ + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/automation.h" +#include "esphome/core/log.h" +#include "esphome/components/socket/socket.h" + +namespace esphome { + +namespace syslog { + +class Syslog : public Component { + public: + explicit Syslog(); + + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void setup() override; + void loop() override; + void dump_config() override; + + void set_server_ip_address(const std::string &address) { this->server_ip_address_ = address; } + void set_server_port(uint16_t port) { this->server_port_ = port; } + void set_hostname(const std::string &hostname) { this->hostname_ = hostname; } + void set_min_log_level(int log_level) { this->min_log_level_ = log_level; } + void set_forward_logger(bool forward) { this->forward_logger_ = forward; } + void set_strip_color_codes(bool strip) { this->strip_color_codes_ = strip; } + + void log(int level, const std::string &tag, const std::string &msg); + + protected: + std::string server_ip_address_; + uint16_t server_port_; + std::string hostname_; + int min_log_level_; + bool forward_logger_; + bool strip_color_codes_; + + int socket_fd_; + struct sockaddr_in destination_; + + int errors_encountered_; + std::string latest_error_message_; + + template + class SyslogLogAction : public Action { + public: + SyslogLogAction(Syslog *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(int, level) + TEMPLATABLE_VALUE(std::string, tag) + TEMPLATABLE_VALUE(std::string, payload) + + void play(Ts... x) override { + this->parent_->log( + this->level_.value(x...), + this->tag_.value(x...), + this->payload_.value(x...)); + } + + protected: + Syslog *parent_; + }; +}; + +} // namespace syslog + +} // namespace esphome diff --git a/templates/mystrom.yaml b/templates/mystrom.yaml index 4da799e..8c1cbd0 100644 --- a/templates/mystrom.yaml +++ b/templates/mystrom.yaml @@ -23,6 +23,7 @@ packages: wifi: !include wifi.yaml uptime: !include uptime.yaml restart: !include restart.yaml + syslog: !include syslog.yaml logger: diff --git a/templates/secrets.yaml.sample b/templates/secrets.yaml.sample index 3a64de6..784c332 100644 --- a/templates/secrets.yaml.sample +++ b/templates/secrets.yaml.sample @@ -9,3 +9,4 @@ wifi_ap_lookup: api_encryption_key: "K3g+SB5OYgLxKtEXhWJd1Mryfy9hGol+mAq+nq/JDsY=" sntp_servers: - pool.ntp.org +syslog_server: 1.2.3.4 diff --git a/templates/syslog.yaml b/templates/syslog.yaml new file mode 100644 index 0000000..832f8c5 --- /dev/null +++ b/templates/syslog.yaml @@ -0,0 +1,14 @@ +external_components: + # Path is relative to the main YAML file, not this device_base template! + - source: components + +syslog: + ip_address: !secret syslog_server + +mqtt: + # Disable logging to save bandwidth. + log_topic: null + +logger: + # Disable logging to save cycles. + baud_rate: 0