From ff7d232ee6ed86b69e7abfa139a8d83af07c620b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:53:26 +1100 Subject: [PATCH] [logger] Add runtime level select (#8222) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/logger/__init__.py | 48 ++++++++++++++++--- esphome/components/logger/logger.cpp | 27 ++++++----- esphome/components/logger/logger.h | 18 ++++--- esphome/components/logger/select/__init__.py | 29 +++++++++++ .../logger/select/logger_level_select.cpp | 27 +++++++++++ .../logger/select/logger_level_select.h | 15 ++++++ esphome/core/defines.h | 3 ++ .../logger/common-default_uart.yaml | 12 ++++- 9 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 esphome/components/logger/select/__init__.py create mode 100644 esphome/components/logger/select/logger_level_select.cpp create mode 100644 esphome/components/logger/select/logger_level_select.h diff --git a/CODEOWNERS b/CODEOWNERS index eab02efffb..d4b3d7eff9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -242,6 +242,7 @@ esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core +esphome/components/logger/select/* @clydebarrow esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr501/* @latonita esphome/components/ltr_als_ps/* @latonita diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 6e92777058..a89bf95c77 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -35,7 +35,7 @@ from esphome.const import ( PLATFORM_RP2040, PLATFORM_RTL87XX, ) -from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority +from esphome.core import CORE, Lambda, coroutine_with_priority CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -77,6 +77,9 @@ USB_SERIAL_JTAG = "USB_SERIAL_JTAG" USB_CDC = "USB_CDC" DEFAULT = "DEFAULT" +CONF_INITIAL_LEVEL = "initial_level" +CONF_LOGGER_ID = "logger_id" + UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], VARIANT_ESP32S2: [UART0, UART1, USB_CDC], @@ -154,11 +157,11 @@ def uart_selection(value): def validate_local_no_higher_than_global(value): - global_level = value.get(CONF_LEVEL, "DEBUG") + global_level = LOG_LEVEL_SEVERITY.index(value[CONF_LEVEL]) for tag, level in value.get(CONF_LOGS, {}).items(): - if LOG_LEVEL_SEVERITY.index(level) > LOG_LEVEL_SEVERITY.index(global_level): - raise EsphomeError( - f"The local log level {level} for {tag} must be less severe than the global log level {global_level}." + if LOG_LEVEL_SEVERITY.index(level) > global_level: + raise cv.Invalid( + f"The configured log level for {tag} ({level}) must be no more severe than the global log level {value[CONF_LEVEL]}." ) return value @@ -209,6 +212,7 @@ CONFIG_SCHEMA = cv.All( cv.string: is_log_level, } ), + cv.Optional(CONF_INITIAL_LEVEL): is_log_level, cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), @@ -227,7 +231,14 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(90.0) async def to_code(config): baud_rate = config[CONF_BAUD_RATE] - log = cg.new_Pvariable(config[CONF_ID], baud_rate, config[CONF_TX_BUFFER_SIZE]) + level = config[CONF_LEVEL] + initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + config[CONF_TX_BUFFER_SIZE], + ) + cg.add(log.set_log_level(initial_level)) if CONF_HARDWARE_UART in config: cg.add( log.set_uart_selection( @@ -239,7 +250,6 @@ async def to_code(config): for tag, level in config[CONF_LOGS].items(): cg.add(log.set_log_level(tag, LOG_LEVELS[level])) - level = config[CONF_LEVEL] cg.add_define("USE_LOGGER") this_severity = LOG_LEVEL_SEVERITY.index(level) cg.add_build_flag(f"-DESPHOME_LOG_LEVEL={LOG_LEVELS[level]}") @@ -367,3 +377,27 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) return cg.new_Pvariable(action_id, template_arg, lambda_) + + +@automation.register_action( + "logger.set_level", + LambdaAction, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger), + cv.Required(CONF_LEVEL): is_log_level, + cv.Optional(CONF_TAG): cv.string, + }, + key=CONF_LEVEL, + ), +) +async def logger_set_level_to_code(config, action_id, template_arg, args): + level = LOG_LEVELS[config[CONF_LEVEL]] + logger = await cg.get_variable(config[CONF_LOGGER_ID]) + if tag := config.get(CONF_TAG): + text = str(cg.statement(logger.set_log_level(tag, level))) + else: + text = str(cg.statement(logger.set_log_level(level))) + + lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) + return cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 36934c7459..79fc4cf499 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -105,12 +105,9 @@ int HOT Logger::level_for(const char *tag) { // Uses std::vector<> for low memory footprint, though the vector // could be sorted to minimize lookup times. This feature isn't used that // much anyway so it doesn't matter too much. - for (auto &it : this->log_levels_) { - if (it.tag == tag) { - return it.level; - } - } - return ESPHOME_LOG_LEVEL; + if (this->log_levels_.count(tag) != 0) + return this->log_levels_[tag]; + return this->current_level_; } void HOT Logger::log_message_(int level, const char *tag, int offset) { @@ -167,9 +164,7 @@ void Logger::loop() { #endif void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, int log_level) { - this->log_levels_.push_back(LogLevelOverride{tag, log_level}); -} +void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_[tag] = log_level; } #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) UARTSelection Logger::get_uart() const { return this->uart_; } @@ -183,18 +178,28 @@ const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DE void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:"); - ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); + ESP_LOGCONFIG(TAG, " Max Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); + ESP_LOGCONFIG(TAG, " Initial Level: %s", LOG_LEVELS[this->current_level_]); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_); ESP_LOGCONFIG(TAG, " Hardware UART: %s", get_uart_selection_()); #endif for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_LEVELS[it.second]); } } void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); } +void Logger::set_log_level(int level) { + if (level > ESPHOME_LOG_LEVEL) { + level = ESPHOME_LOG_LEVEL; + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); + } + this->current_level_ = level; + this->level_callback_.call(level); +} + Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace logger diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index b55cfb0771..c4c873e020 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -1,11 +1,12 @@ #pragma once #include -#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #ifdef USE_ARDUINO #if defined(USE_ESP8266) || defined(USE_ESP32) @@ -74,8 +75,11 @@ class Logger : public Component { UARTSelection get_uart() const; #endif + /// Set the default log level for this logger. + void set_log_level(int level); /// Set the log level of the specified tag. void set_log_level(const std::string &tag, int log_level); + int get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -88,6 +92,9 @@ class Logger : public Component { /// Register a callback that will be called for every log message sent void add_on_log_callback(std::function &&callback); + // add a listener for log level changes + void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } + float get_setup_priority() const override; void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT @@ -159,17 +166,14 @@ class Logger : public Component { #ifdef USE_ESP_IDF uart_port_t uart_num_; #endif - struct LogLevelOverride { - std::string tag; - int level; - }; - std::vector log_levels_; + std::map log_levels_{}; CallbackManager log_callback_{}; + int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; /// Prevents recursive log calls, if true a log message is already being processed. bool recursion_guard_ = false; void *main_task_ = nullptr; + CallbackManager level_callback_{}; }; - extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) class LoggerMessageTrigger : public Trigger { diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py new file mode 100644 index 0000000000..b1fc881537 --- /dev/null +++ b/esphome/components/logger/select/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_BUG +from esphome.core import CORE +from esphome.cpp_helpers import register_component, register_parented + +from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns + +CODEOWNERS = ["@clydebarrow"] + +LoggerLevelSelect = logger_ns.class_("LoggerLevelSelect", select.Select, cg.Component) + +CONFIG_SCHEMA = select.select_schema( + LoggerLevelSelect, icon=ICON_BUG, entity_category=ENTITY_CATEGORY_CONFIG +).extend( + { + cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger), + } +) + + +async def to_code(config): + levels = LOG_LEVEL_SEVERITY + index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL]) + levels = levels[: index + 1] + var = await select.new_select(config, options=levels) + await register_parented(var, config[CONF_LOGGER_ID]) + await register_component(var, config) diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp new file mode 100644 index 0000000000..b71a6e02a2 --- /dev/null +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -0,0 +1,27 @@ +#include "logger_level_select.h" + +namespace esphome { +namespace logger { + +void LoggerLevelSelect::publish_state(int level) { + auto value = this->at(level); + if (!value) { + return; + } + Select::publish_state(value.value()); +} + +void LoggerLevelSelect::setup() { + this->parent_->add_listener([this](int level) { this->publish_state(level); }); + this->publish_state(this->parent_->get_log_level()); +} + +void LoggerLevelSelect::control(const std::string &value) { + auto level = this->index_of(value); + if (!level) + return; + this->parent_->set_log_level(level.value()); +} + +} // namespace logger +} // namespace esphome diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h new file mode 100644 index 0000000000..2c92c84d13 --- /dev/null +++ b/esphome/components/logger/select/logger_level_select.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "esphome/core/component.h" +#include "esphome/components/logger/logger.h" +namespace esphome { +namespace logger { +class LoggerLevelSelect : public Component, public select::Select, public Parented { + public: + void publish_state(int level); + void setup() override; + void control(const std::string &value) override; +}; +} // namespace logger +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8407391bce..dc0ac3c1e8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -14,6 +14,9 @@ #define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" +// logger +#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE + // Feature flags #define USE_ALARM_CONTROL_PANEL #define USE_AUDIO_FLAC_SUPPORT diff --git a/tests/components/logger/common-default_uart.yaml b/tests/components/logger/common-default_uart.yaml index 70b485daac..e8b56043eb 100644 --- a/tests/components/logger/common-default_uart.yaml +++ b/tests/components/logger/common-default_uart.yaml @@ -1,7 +1,17 @@ esphome: on_boot: then: - - logger.log: Hello world + - logger.log: + level: warn + format: "Warning: Logger level is %d" + args: [id(logger_id).get_log_level()] + - logger.set_level: WARN logger: + id: logger_id level: DEBUG + initial_level: INFO + +select: + - platform: logger + name: Logger Level