From ed0751246ad4468bd345110f1b23c936808e8815 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:28:02 -0600 Subject: [PATCH] [logger] Conditionally compile log level change listener --- esphome/components/logger/__init__.py | 22 +++++++++++++++++++ esphome/components/logger/logger.cpp | 5 ++++- esphome/components/logger/logger.h | 22 ++++++++++++++++--- esphome/components/logger/select/__init__.py | 9 +++++++- .../logger/select/logger_level_select.cpp | 6 ++--- .../logger/select/logger_level_select.h | 9 ++++++-- esphome/core/defines.h | 1 + 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 39877030e9..bd9885facf 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -406,6 +406,8 @@ async def to_code(config): conf, ) + CORE.add_job(final_step) + def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python @@ -506,3 +508,23 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, } ) + +# Key for CORE.data to track if level listeners are requested +LOGGER_LEVEL_LISTENERS_KEY = "logger_level_listeners" + + +def request_logger_level_listeners() -> None: + """Request that logger level listeners be compiled in. + + Components that need to be notified about log level changes should call this + function during their code generation. This enables the add_level_listener() + method and compiles in the listener vector. + """ + CORE.data[LOGGER_LEVEL_LISTENERS_KEY] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional logger features.""" + if CORE.data.get(LOGGER_LEVEL_LISTENERS_KEY, False): + cg.add_define("USE_LOGGER_LEVEL_LISTENERS") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index f925e85e11..21e2b44808 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -288,7 +288,10 @@ void Logger::set_log_level(uint8_t level) { ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; - this->level_callback_.call(level); +#ifdef USE_LOGGER_LEVEL_LISTENERS + for (auto *listener : this->level_listeners_) + listener->on_log_level_change(level); +#endif } Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index a0024411d7..4cf3d1f423 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -58,6 +58,18 @@ class LogListener { virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; }; +#ifdef USE_LOGGER_LEVEL_LISTENERS +/** Interface for receiving log level changes without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + */ +class LoggerLevelListener { + public: + virtual void on_log_level_change(uint8_t level) = 0; +}; +#endif + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -193,8 +205,10 @@ class Logger : public Component { /// Register a log listener to receive log messages void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } - // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } +#ifdef USE_LOGGER_LEVEL_LISTENERS + /// Register a listener for log level changes + void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } +#endif float get_setup_priority() const override; @@ -325,7 +339,9 @@ class Logger : public Component { std::map log_levels_{}; #endif std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) - CallbackManager level_callback_{}; +#ifdef USE_LOGGER_LEVEL_LISTENERS + std::vector level_listeners_; // Log level change listeners +#endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer #endif diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 2e83599eb4..6ce663978e 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -5,7 +5,13 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_ from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented -from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns +from .. import ( + CONF_LOGGER_ID, + LOG_LEVELS, + Logger, + logger_ns, + request_logger_level_listeners, +) CODEOWNERS = ["@clydebarrow"] @@ -21,6 +27,7 @@ CONFIG_SCHEMA = select.select_schema( async def to_code(config): + request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL]) diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index e2ec28a390..3091ca1851 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -2,7 +2,7 @@ namespace esphome::logger { -void LoggerLevelSelect::publish_state(int level) { +void LoggerLevelSelect::on_log_level_change(uint8_t level) { auto index = level_to_index(level); if (!this->has_index(index)) return; @@ -10,8 +10,8 @@ void LoggerLevelSelect::publish_state(int level) { } void LoggerLevelSelect::setup() { - this->parent_->add_listener([this](int level) { this->publish_state(level); }); - this->publish_state(this->parent_->get_log_level()); + this->parent_->add_level_listener(this); + this->on_log_level_change(this->parent_->get_log_level()); } void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 950edd29ac..6482114943 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -5,12 +5,17 @@ #include "esphome/components/logger/logger.h" namespace esphome::logger { -class LoggerLevelSelect : public Component, public select::Select, public Parented { +class LoggerLevelSelect final : public Component, + public select::Select, + public Parented, + public LoggerLevelListener { public: - void publish_state(int level); void setup() override; void control(size_t index) override; + // LoggerLevelListener interface + void on_log_level_change(uint8_t level) override; + protected: // Convert log level to option index (skip CONFIG at level 4) static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f4026aad96..538d4e3d6e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_LEVEL_LISTENERS #define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG