From cceab26bfb40a32858b2b31b7b6383c32f8e0b49 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:43:40 +1000 Subject: [PATCH 01/40] [lvgl] Fix dangling pointer issue with qrcode (#9190) --- esphome/components/lvgl/widgets/qrcode.py | 10 ++++------ tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 742b538938..7d8d13d8c4 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass -from ..defines import CONF_MAIN, literal +from ..defines import CONF_MAIN from ..lv_validation import color, color_retmapper, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA @@ -34,7 +34,7 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img") + return ("canvas", "img", "label") def obj_creator(self, parent: MockObjClass, config: dict): dark_color = color_retmapper(config[CONF_DARK_COLOR]) @@ -45,10 +45,8 @@ class QrCodeType(WidgetType): async def to_code(self, w: Widget, config): if (value := config.get(CONF_TEXT)) is not None: value = await lv_text.process(value) - with LocalVariable( - "qr_text", cg.const_char_ptr, value, modifier="" - ) as str_obj: - lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})")) + with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: + lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) qr_code_spec = QrCodeType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d8452bdd2a..e3391a5bac 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -646,7 +646,9 @@ lvgl: on_click: lvgl.qrcode.update: id: lv_qr - text: homeassistant.io + text: + format: "A string with a number %d" + args: ['(int)(random_uint32() % 1000)'] - slider: min_value: 0 From e146c0796abb32ff605ec68f23be74248c579039 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:11:50 +0100 Subject: [PATCH 02/40] [audio] Bugfix: improve timeout handling (#9221) --- esphome/components/audio/audio_reader.cpp | 62 +++++++++++++++-------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index b82c6db9ee..6966c95db7 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #include "esp_crt_bundle.h" @@ -16,13 +17,13 @@ namespace audio { static const uint32_t READ_WRITE_TIMEOUT_MS = 20; static const uint32_t CONNECTION_TIMEOUT_MS = 5000; - -// The number of times the http read times out with no data before throwing an error -static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; +static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; -static const uint8_t MAX_REDIRECTION = 5; +static const uint8_t MAX_REDIRECTIONS = 5; + +static const char *const TAG = "audio_reader"; // Some common HTTP status codes - borrowed from http_request component accessed 20241224 enum HttpStatus { @@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { client_config.url = uri.c_str(); client_config.cert_pem = nullptr; client_config.disable_auto_redirect = false; - client_config.max_redirection_count = 10; + client_config.max_redirection_count = MAX_REDIRECTIONS; client_config.event_handler = http_event_handler; client_config.user_data = this; client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; @@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { esp_err_t err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open URL"); this->cleanup_connection_(); return err; } int64_t header_length = esp_http_client_fetch_headers(this->client_); + uint8_t reattempt_count = 0; + while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) { + this->cleanup_connection_(); + if (header_length != -ESP_ERR_HTTP_EAGAIN) { + // Serious error, no recovery + return ESP_FAIL; + } else { + // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available + this->client_ = esp_http_client_init(&client_config); + esp_http_client_open(this->client_, 0); + header_length = esp_http_client_fetch_headers(this->client_); + ++reattempt_count; + } + } + if (header_length < 0) { + ESP_LOGE(TAG, "Failed to fetch headers"); this->cleanup_connection_(); return ESP_FAIL; } @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { ssize_t redirect_count = 0; - while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { + while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) { err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { this->cleanup_connection_(); @@ -267,27 +285,29 @@ AudioReaderState AudioReader::http_read_() { return AudioReaderState::FINISHED; } } else if (this->output_transfer_buffer_->free() > 0) { - size_t bytes_to_read = this->output_transfer_buffer_->free(); - int received_len = - esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); + int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free()); if (received_len > 0) { this->output_transfer_buffer_->increase_buffer_length(received_len); this->last_data_read_ms_ = millis(); - } else if (received_len < 0) { + return AudioReaderState::READING; + } else if (received_len <= 0) { // HTTP read error - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } else { - if (bytes_to_read > 0) { - // Read timed out - if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } - - delay(READ_WRITE_TIMEOUT_MS); + if (received_len == -1) { + // A true connection error occured, no chance at recovery + this->cleanup_connection_(); + return AudioReaderState::FAILED; } + + // Read timed out, manually verify if it has been too long since the last successful read + if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timed out"); + this->cleanup_connection_(); + return AudioReaderState::FAILED; + } + + delay(READ_WRITE_TIMEOUT_MS); } } From 068aa0ff1eadedf6895ffe31ba1b817e320fcbb0 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:12:58 +0100 Subject: [PATCH 03/40] [speaker] bugfix: continue to block tasks if stop flag is set (#9222) --- .../speaker/media_player/audio_pipeline.cpp | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index ac122b6e0c..333a076bec 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -343,13 +343,12 @@ void AudioPipeline::read_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); // Wait until the pipeline notifies us the source of the media file - EventBits_t event_bits = - xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = xEventGroupWaitBits( + this_pipeline->event_group_, + EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED | @@ -434,12 +433,12 @@ void AudioPipeline::decode_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); // Wait until the reader notifies us that the media type is available - EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = + xEventGroupWaitBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); From 9f3f4ead4f71a5af9860a7527ac5d96bd3d3d24e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:18:51 +0100 Subject: [PATCH 04/40] [voice_assistant] Support streaming TTS responses and fixes crash for long responses (#9224) --- CODEOWNERS | 2 +- .../components/voice_assistant/__init__.py | 13 ++++++- .../voice_assistant/voice_assistant.cpp | 38 ++++++++++++++++++- .../voice_assistant/voice_assistant.h | 4 ++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 66ea80f8d6..a0812c9cd6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -490,7 +490,7 @@ esphome/components/vbus/* @ssieb esphome/components/veml3235/* @kbx81 esphome/components/veml7700/* @latonita esphome/components/version/* @esphome/core -esphome/components/voice_assistant/* @jesserockz +esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index b9309ab422..59c7ec8383 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -17,10 +17,11 @@ from esphome.const import ( AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] CONF_ON_END = "on_end" CONF_ON_INTENT_END = "on_intent_end" +CONF_ON_INTENT_PROGRESS = "on_intent_progress" CONF_ON_INTENT_START = "on_intent_start" CONF_ON_LISTENING = "on_listening" CONF_ON_START = "on_start" @@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation( + single=True + ), cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( single=True ), @@ -282,6 +286,13 @@ async def to_code(config): config[CONF_ON_INTENT_START], ) + if CONF_ON_INTENT_PROGRESS in config: + await automation.build_automation( + var.get_intent_progress_trigger(), + [(cg.std_string, "x")], + config[CONF_ON_INTENT_PROGRESS], + ) + if CONF_ON_INTENT_END in config: await automation.build_automation( var.get_intent_end_trigger(), diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index a692a7556e..879d9492f0 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: this->signal_stop_(); - break; + // Fallthrough intended to stop a streaming TTS announcement that has potentially started case State::STREAMING_RESPONSE: #ifdef USE_MEDIA_PLAYER // Stop any ongoing media player announcement @@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); +#ifdef USE_MEDIA_PLAYER + this->started_streaming_tts_ = false; + for (auto arg : msg.data) { + if (arg.name == "url") { + this->tts_response_url_ = std::move(arg.value); + } + } +#endif this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: @@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (text.empty()) { ESP_LOGW(TAG, "No text in STT_END event"); return; + } else if (text.length() > 500) { + text = text.substr(0, 497) + "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Intent started"); this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; + case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { + ESP_LOGD(TAG, "Intent progress"); + std::string tts_url_for_trigger = ""; +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + for (const auto &arg : msg.data) { + if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { + this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); + + this->media_player_wait_for_announcement_start_ = true; + this->media_player_wait_for_announcement_end_ = false; + this->started_streaming_tts_ = true; + tts_url_for_trigger = this->tts_response_url_; + this->tts_response_url_.clear(); // Reset streaming URL + } + } + } +#endif + this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); + break; + } case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { @@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in TTS_START event"); return; } + if (text.length() > 500) { + text = text.substr(0, 497) + "..."; + } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { this->tts_start_trigger_->trigger(text); @@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { + if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); this->media_player_wait_for_announcement_start_ = true; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 865731522f..2424ea6052 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -177,6 +177,7 @@ class VoiceAssistant : public Component { Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } + Trigger *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } @@ -233,6 +234,7 @@ class VoiceAssistant : public Component { Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); #endif + Trigger *intent_progress_trigger_ = new Trigger(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); @@ -268,6 +270,8 @@ class VoiceAssistant : public Component { #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; + std::string tts_response_url_{""}; + bool started_streaming_tts_{false}; bool media_player_wait_for_announcement_start_{false}; bool media_player_wait_for_announcement_end_{false}; #endif From 9e993ac603a64b2f48c92e8910ef77eeee9aafc3 Mon Sep 17 00:00:00 2001 From: scaiper Date: Fri, 27 Jun 2025 13:43:18 +0300 Subject: [PATCH 05/40] [esp32] Change ``enable_lwip_mdns_queries`` default to ``True`` (#9188) --- esphome/components/esp32/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 157fd9db11..ba9a4894c2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -605,7 +605,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False ): cv.boolean, cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=False + CONF_ENABLE_LWIP_MDNS_QUERIES, default=True ): cv.boolean, cv.Optional( CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False @@ -760,7 +760,7 @@ async def to_code(config): and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False): + if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) From 948aa13fb9b0752132eabdcf0adb670585e57cac Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 27 Jun 2025 23:16:13 +1200 Subject: [PATCH 06/40] Bump version to 2025.6.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a4dd65bf45..edbb5b1206 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.6.1 +PROJECT_NUMBER = 2025.6.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 3ef82aff5b..956edfd6be 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.6.1" +__version__ = "2025.6.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 156a9160ba9ac191543ae7c6c844ba259d0cd59b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 27 Jun 2025 18:31:23 -0700 Subject: [PATCH 07/40] [mcp23xxx_base] fix pin interrupts (#9244) Co-authored-by: Samuel Sieb --- esphome/components/mcp23xxx_base/mcp23xxx_base.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 14a703fb9f..fc49f216ee 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -6,7 +6,11 @@ namespace mcp23xxx_base { float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } -void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } +void MCP23XXXGPIOPin::setup() { + pin_mode(flags_); + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); +} + void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } From 52ca8deb1099eca9be1ef9a4fa5c30f2edf67343 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:32:18 -0400 Subject: [PATCH 08/40] [i2c] Disable i2c scan on certain idf versions (#9237) --- esphome/components/i2c/__init__.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index d56bb2d07c..fae1fa1d22 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,5 +1,8 @@ +import logging + from esphome import pins import esphome.codegen as cg +from esphome.components import esp32 import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -12,6 +15,8 @@ from esphome.const import ( CONF_SCL, CONF_SDA, CONF_TIMEOUT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -19,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CBus = i2c_ns.class_("I2CBus") @@ -41,6 +47,32 @@ def _bus_declare_type(value): raise NotImplementedError +def validate_config(config): + if ( + config[CONF_SCAN] + and CORE.is_esp32 + and CORE.using_esp_idf + and esp32.get_esp32_variant() + in [ + esp32.const.VARIANT_ESP32C5, + esp32.const.VARIANT_ESP32C6, + esp32.const.VARIANT_ESP32P4, + ] + ): + version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if version.major == 5 and ( + (version.minor == 3 and version.patch <= 3) + or (version.minor == 4 and version.patch <= 1) + ): + LOGGER.warning( + "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " + "has been disabled, see https://github.com/esphome/issues/issues/7128", + str(version), + ) + config[CONF_SCAN] = False + return config + + pin_with_input_and_output_support = pins.internal_gpio_pin_number( {CONF_OUTPUT: True, CONF_INPUT: True} ) @@ -66,6 +98,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + validate_config, ) From 5010a0f5e706d1067e13bb6a76661d1b78ce29c4 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 27 Jun 2025 18:31:23 -0700 Subject: [PATCH 09/40] [mcp23xxx_base] fix pin interrupts (#9244) Co-authored-by: Samuel Sieb --- esphome/components/mcp23xxx_base/mcp23xxx_base.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 14a703fb9f..fc49f216ee 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -6,7 +6,11 @@ namespace mcp23xxx_base { float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } -void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } +void MCP23XXXGPIOPin::setup() { + pin_mode(flags_); + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); +} + void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } From 61b3379f48b2e4895da96ab0c72239e9516a3299 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:32:18 -0400 Subject: [PATCH 10/40] [i2c] Disable i2c scan on certain idf versions (#9237) --- esphome/components/i2c/__init__.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index e47dec650d..e20b7febeb 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,5 +1,8 @@ +import logging + from esphome import pins import esphome.codegen as cg +from esphome.components import esp32 import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -12,6 +15,8 @@ from esphome.const import ( CONF_SCL, CONF_SDA, CONF_TIMEOUT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -19,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CBus = i2c_ns.class_("I2CBus") @@ -40,6 +46,32 @@ def _bus_declare_type(value): raise NotImplementedError +def validate_config(config): + if ( + config[CONF_SCAN] + and CORE.is_esp32 + and CORE.using_esp_idf + and esp32.get_esp32_variant() + in [ + esp32.const.VARIANT_ESP32C5, + esp32.const.VARIANT_ESP32C6, + esp32.const.VARIANT_ESP32P4, + ] + ): + version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if version.major == 5 and ( + (version.minor == 3 and version.patch <= 3) + or (version.minor == 4 and version.patch <= 1) + ): + LOGGER.warning( + "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " + "has been disabled, see https://github.com/esphome/issues/issues/7128", + str(version), + ) + config[CONF_SCAN] = False + return config + + pin_with_input_and_output_support = pins.internal_gpio_pin_number( {CONF_OUTPUT: True, CONF_INPUT: True} ) @@ -65,6 +97,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + validate_config, ) From f8d59b5aeb1268d52a47fe674b115e7245ceb143 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 22:53:40 -0500 Subject: [PATCH 11/40] Reduce libretiny logconfig messages (#9239) --- esphome/components/libretiny/lt_component.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index ec4b60eaeb..ffccd0ad7a 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -10,9 +10,11 @@ namespace libretiny { static const char *const TAG = "lt.component"; void LTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "LibreTiny:"); - ESP_LOGCONFIG(TAG, " Version: %s", LT_BANNER_STR + 10); - ESP_LOGCONFIG(TAG, " Loglevel: %u", LT_LOGLEVEL); + ESP_LOGCONFIG(TAG, + "LibreTiny:\n" + " Version: %s\n" + " Loglevel: %u", + LT_BANNER_STR + 10, LT_LOGLEVEL); #ifdef USE_TEXT_SENSOR if (this->version_ != nullptr) { From 094bf19ec4d86de89d57cc60a3870bdcc6cc4567 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 23:58:53 -0500 Subject: [PATCH 12/40] Disable dynamic log level control for ESP32 ESP-IDF builds (#9233) --- esphome/components/esp32/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7e1c71a7de..6425e57b13 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -758,6 +758,9 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + # Disable dynamic log level control to save memory + add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) + # Set default CPU frequency add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True) From 3f65cee17cb135895921f0309d01af404d96d61a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 23:59:52 -0500 Subject: [PATCH 13/40] Silence protobuf compatibility warnings when importing aioesphomeapi (#9236) --- esphome/components/api/client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 20136ef7b8..7f8e2529a5 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -4,9 +4,15 @@ import asyncio from datetime import datetime import logging from typing import TYPE_CHECKING, Any +import warnings -from aioesphomeapi import APIClient, parse_log_message -from aioesphomeapi.log_runner import async_run +# Suppress protobuf version warnings +with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=UserWarning, message=".*Protobuf gencode version.*" + ) + from aioesphomeapi import APIClient, parse_log_message + from aioesphomeapi.log_runner import async_run from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE From d37f5b87bd024a14eb4828076b0a32f166d6f903 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 28 Jun 2025 02:30:59 -0400 Subject: [PATCH 14/40] [esp32] Allow 5.4.2 (#9243) --- esphome/components/esp32/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6425e57b13..32323b7504 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -341,6 +341,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ # List based on https://github.com/pioarduino/esp-idf/releases SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ cv.Version(5, 5, 0), + cv.Version(5, 4, 2), cv.Version(5, 4, 1), cv.Version(5, 4, 0), cv.Version(5, 3, 3), From 58b7d0b412187aa4fb0c8ec03444b5e23454d39b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:21:53 +0000 Subject: [PATCH 15/40] Bump ruff from 0.12.0 to 0.12.1 (#9241) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96efee7020..1a0289a3ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.0 + rev: v0.12.1 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 89aba702b9..66b71c2225 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.0 # also change in .pre-commit-config.yaml when updating +ruff==0.12.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 30f61b26ff150e97b29adead8dae13e04d165510 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sat, 28 Jun 2025 21:56:12 +0200 Subject: [PATCH 16/40] Remove backports of `std` (#9246) --- esphome/core/datatypes.h | 4 +- esphome/core/helpers.cpp | 15 ------ esphome/core/helpers.h | 109 ++++++--------------------------------- 3 files changed, 18 insertions(+), 110 deletions(-) diff --git a/esphome/core/datatypes.h b/esphome/core/datatypes.h index 5356be6b52..4929518387 100644 --- a/esphome/core/datatypes.h +++ b/esphome/core/datatypes.h @@ -11,7 +11,7 @@ namespace internal { /// Wrapper class for memory using big endian data layout, transparently converting it to native order. template class BigEndianLayout { public: - constexpr14 operator T() { return convert_big_endian(val_); } + constexpr operator T() { return convert_big_endian(val_); } private: T val_; @@ -20,7 +20,7 @@ template class BigEndianLayout { /// Wrapper class for memory using big endian data layout, transparently converting it to native order. template class LittleEndianLayout { public: - constexpr14 operator T() { return convert_little_endian(val_); } + constexpr operator T() { return convert_little_endian(val_); } private: T val_; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 79dbb314c8..fc91d83972 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -76,23 +76,8 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0 0x9188, 0x83b9, 0xb5ea, 0xa7db, 0xd94c, 0xcb7d, 0xfd2e, 0xef1f}; #endif -// STL backports - -#if _GLIBCXX_RELEASE < 8 -std::string to_string(int value) { return str_snprintf("%d", 32, value); } // NOLINT -std::string to_string(long value) { return str_snprintf("%ld", 32, value); } // NOLINT -std::string to_string(long long value) { return str_snprintf("%lld", 32, value); } // NOLINT -std::string to_string(unsigned value) { return str_snprintf("%u", 32, value); } // NOLINT -std::string to_string(unsigned long value) { return str_snprintf("%lu", 32, value); } // NOLINT -std::string to_string(unsigned long long value) { return str_snprintf("%llu", 32, value); } // NOLINT -std::string to_string(float value) { return str_snprintf("%f", 32, value); } -std::string to_string(double value) { return str_snprintf("%f", 32, value); } -std::string to_string(long double value) { return str_snprintf("%Lf", 32, value); } -#endif - // Mathematics -float lerp(float completion, float start, float end) { return start + (end - start) * completion; } uint8_t crc8(const uint8_t *data, uint8_t len) { uint8_t crc = 0; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8bd5b813c7..7d5366f323 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -37,89 +37,18 @@ #define ESPHOME_ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) -// Various functions can be constexpr in C++14, but not in C++11 (because their body isn't just a return statement). -// Define a substitute constexpr keyword for those functions, until we can drop C++11 support. -#if __cplusplus >= 201402L -#define constexpr14 constexpr -#else -#define constexpr14 inline // constexpr implies inline -#endif - namespace esphome { /// @name STL backports ///@{ -// Backports for various STL features we like to use. Pull in the STL implementation wherever available, to avoid -// ambiguity and to provide a uniform API. - -// std::to_string() from C++11, available from libstdc++/g++ 8 -// See https://github.com/espressif/esp-idf/issues/1445 -#if _GLIBCXX_RELEASE >= 8 +// Keep "using" even after the removal of our backports, to avoid breaking existing code. using std::to_string; -#else -std::string to_string(int value); // NOLINT -std::string to_string(long value); // NOLINT -std::string to_string(long long value); // NOLINT -std::string to_string(unsigned value); // NOLINT -std::string to_string(unsigned long value); // NOLINT -std::string to_string(unsigned long long value); // NOLINT -std::string to_string(float value); -std::string to_string(double value); -std::string to_string(long double value); -#endif - -// std::is_trivially_copyable from C++11, implemented in libstdc++/g++ 5.1 (but minor releases can't be detected) -#if _GLIBCXX_RELEASE >= 6 using std::is_trivially_copyable; -#else -// Implementing this is impossible without compiler intrinsics, so don't bother. Invalid usage will be detected on -// other variants that use a newer compiler anyway. -// NOLINTNEXTLINE(readability-identifier-naming) -template struct is_trivially_copyable : public std::integral_constant {}; -#endif - -// std::make_unique() from C++14 -#if __cpp_lib_make_unique >= 201304 using std::make_unique; -#else -template std::unique_ptr make_unique(Args &&...args) { - return std::unique_ptr(new T(std::forward(args)...)); -} -#endif - -// std::enable_if_t from C++14 -#if __cplusplus >= 201402L using std::enable_if_t; -#else -template using enable_if_t = typename std::enable_if::type; -#endif - -// std::clamp from C++17 -#if __cpp_lib_clamp >= 201603 using std::clamp; -#else -template constexpr const T &clamp(const T &v, const T &lo, const T &hi, Compare comp) { - return comp(v, lo) ? lo : comp(hi, v) ? hi : v; -} -template constexpr const T &clamp(const T &v, const T &lo, const T &hi) { - return clamp(v, lo, hi, std::less{}); -} -#endif - -// std::is_invocable from C++17 -#if __cpp_lib_is_invocable >= 201703 using std::is_invocable; -#else -// https://stackoverflow.com/a/37161919/8924614 -template struct is_invocable { // NOLINT(readability-identifier-naming) - template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type()); - template static auto test(...) -> decltype(std::false_type()); - static constexpr auto value = decltype(test(nullptr))::value; // NOLINT -}; -#endif - -// std::bit_cast from C++20 #if __cpp_lib_bit_cast >= 201806 using std::bit_cast; #else @@ -134,31 +63,29 @@ To bit_cast(const From &src) { return dst; } #endif +using std::lerp; // std::byteswap from C++23 -template constexpr14 T byteswap(T n) { +template constexpr T byteswap(T n) { T m; for (size_t i = 0; i < sizeof(T); i++) reinterpret_cast(&m)[i] = reinterpret_cast(&n)[sizeof(T) - 1 - i]; return m; } -template<> constexpr14 uint8_t byteswap(uint8_t n) { return n; } -template<> constexpr14 uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } -template<> constexpr14 uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } -template<> constexpr14 uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } -template<> constexpr14 int8_t byteswap(int8_t n) { return n; } -template<> constexpr14 int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } -template<> constexpr14 int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } -template<> constexpr14 int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } +template<> constexpr uint8_t byteswap(uint8_t n) { return n; } +template<> constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } +template<> constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } +template<> constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } +template<> constexpr int8_t byteswap(int8_t n) { return n; } +template<> constexpr int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } +template<> constexpr int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } +template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } ///@} /// @name Mathematics ///@{ -/// Linearly interpolate between \p start and \p end by \p completion (between 0 and 1). -float lerp(float completion, float start, float end); - /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out). template T remap(U value, U min, U max, T min_out, T max_out) { return (value - min) * (max_out - min_out) / (max - min) + min_out; @@ -203,8 +130,7 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui } /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). -template::value, int> = 0> -constexpr14 T encode_value(const uint8_t *bytes) { +template::value, int> = 0> constexpr T encode_value(const uint8_t *bytes) { T val = 0; for (size_t i = 0; i < sizeof(T); i++) { val <<= 8; @@ -214,12 +140,12 @@ constexpr14 T encode_value(const uint8_t *bytes) { } /// Encode a value from its constituent bytes (from most to least significant) in an std::array with length sizeof(T). template::value, int> = 0> -constexpr14 T encode_value(const std::array bytes) { +constexpr T encode_value(const std::array bytes) { return encode_value(bytes.data()); } /// Decode a value into its constituent bytes (from most to least significant). template::value, int> = 0> -constexpr14 std::array decode_value(T val) { +constexpr std::array decode_value(T val) { std::array ret{}; for (size_t i = sizeof(T); i > 0; i--) { ret[i - 1] = val & 0xFF; @@ -246,7 +172,7 @@ inline uint32_t reverse_bits(uint32_t x) { } /// Convert a value between host byte order and big endian (most significant byte first) order. -template constexpr14 T convert_big_endian(T val) { +template constexpr T convert_big_endian(T val) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return byteswap(val); #else @@ -255,7 +181,7 @@ template constexpr14 T convert_big_endian(T val) { } /// Convert a value between host byte order and little endian (least significant byte first) order. -template constexpr14 T convert_little_endian(T val) { +template constexpr T convert_little_endian(T val) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return val; #else @@ -276,9 +202,6 @@ bool str_startswith(const std::string &str, const std::string &start); /// Check whether a string ends with a value. bool str_endswith(const std::string &str, const std::string &end); -/// Convert the value to a string (added as extra overload so that to_string() can be used on all stringifiable types). -inline std::string to_string(const std::string &val) { return val; } - /// Truncate a string to a specific length. std::string str_truncate(const std::string &str, size_t length); From 13d4823db6a29fe2ad09a0a08c3f20c18b6eff78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 15:04:42 -0500 Subject: [PATCH 17/40] Fix buffer corruption in API message encoding with very verbose logging (#9249) --- esphome/components/api/api_connection.cpp | 35 ++++++-- esphome/components/api/api_connection.h | 8 ++ .../integration/fixtures/api_vv_logging.yaml | 89 +++++++++++++++++++ tests/integration/test_api_vv_logging.py | 83 +++++++++++++++++ 4 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/api_vv_logging.yaml create mode 100644 tests/integration/test_api_vv_logging.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fdcce6088c..65588ad4d8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -94,6 +94,19 @@ APIConnection::~APIConnection() { #endif } +#ifdef HAS_PROTO_MESSAGE_DUMP +void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) { + // Set log-only mode + this->log_only_mode_ = true; + + // Call the creator - it will create the message and log it via encode_message_to_buffer + item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); + + // Clear log-only mode + this->log_only_mode_ = false; +} +#endif + void APIConnection::loop() { if (this->next_close_) { // requested a disconnect @@ -249,6 +262,14 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { // including header and footer overhead. Returns 0 if the message doesn't fit. uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // If in log-only mode, just log and return + if (conn->log_only_mode_) { + conn->log_send_message_(msg.message_name(), msg.dump()); + return 1; // Return non-zero to indicate "success" for logging + } +#endif + // Calculate size uint32_t calculated_size = 0; msg.calculate_size(calculated_size); @@ -276,11 +297,6 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes // Encode directly into buffer msg.encode(buffer); -#ifdef HAS_PROTO_MESSAGE_DUMP - // Log the message for VV debugging - conn->log_send_message_(msg.message_name(), msg.dump()); -#endif - // Calculate actual encoded size (not including header that was already added) size_t actual_payload_size = shared_buf.size() - size_before_encode; @@ -1891,6 +1907,15 @@ void APIConnection::process_batch_() { } } +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + for (size_t i = 0; i < items_processed; i++) { + const auto &item = this->deferred_batch_.items[i]; + this->log_batch_item_(item); + } +#endif + // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.items.size()) { // Remove processed items from the beginning diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index e872711e95..07e87ab39f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -470,6 +470,10 @@ class APIConnection : public APIServerConnection { bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; +#ifdef HAS_PROTO_MESSAGE_DUMP + // When true, encode_message_to_buffer will only log, not encode + bool log_only_mode_{false}; +#endif uint8_t ping_retries_{0}; // 8 bytes used, no padding needed @@ -627,6 +631,10 @@ class APIConnection : public APIServerConnection { // State for batch buffer allocation bool batch_first_message_{false}; +#ifdef HAS_PROTO_MESSAGE_DUMP + void log_batch_item_(const DeferredBatch::BatchItem &item); +#endif + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { this->deferred_batch_.add_item(entity, std::move(creator), message_type); diff --git a/tests/integration/fixtures/api_vv_logging.yaml b/tests/integration/fixtures/api_vv_logging.yaml new file mode 100644 index 0000000000..df1edc796a --- /dev/null +++ b/tests/integration/fixtures/api_vv_logging.yaml @@ -0,0 +1,89 @@ +esphome: + name: vv-logging-test + +host: + +api: + +logger: + level: VERY_VERBOSE + # Enable VV logging for API components where the issue occurs + logs: + api.connection: VERY_VERBOSE + api.service: VERY_VERBOSE + api.proto: VERY_VERBOSE + sensor: VERY_VERBOSE + +# Create many sensors that update frequently to generate API traffic +# This will cause many messages to be batched and sent, triggering the +# code path where VV logging could cause buffer corruption +sensor: + - platform: template + name: "Test Sensor 1" + lambda: 'return millis() / 1000.0;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 2" + lambda: 'return (millis() / 1000.0) + 10;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 3" + lambda: 'return (millis() / 1000.0) + 20;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 4" + lambda: 'return (millis() / 1000.0) + 30;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 5" + lambda: 'return (millis() / 1000.0) + 40;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 6" + lambda: 'return (millis() / 1000.0) + 50;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 7" + lambda: 'return (millis() / 1000.0) + 60;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 8" + lambda: 'return (millis() / 1000.0) + 70;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 9" + lambda: 'return (millis() / 1000.0) + 80;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 10" + lambda: 'return (millis() / 1000.0) + 90;' + update_interval: 50ms + unit_of_measurement: "s" + +# Add some binary sensors too for variety +binary_sensor: + - platform: template + name: "Test Binary 1" + lambda: 'return (millis() / 1000) % 2 == 0;' + + - platform: template + name: "Test Binary 2" + lambda: 'return (millis() / 1000) % 3 == 0;' diff --git a/tests/integration/test_api_vv_logging.py b/tests/integration/test_api_vv_logging.py new file mode 100644 index 0000000000..19aab2001c --- /dev/null +++ b/tests/integration/test_api_vv_logging.py @@ -0,0 +1,83 @@ +"""Integration test for API with VERY_VERBOSE logging to verify no buffer corruption.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from aioesphomeapi import LogLevel +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_vv_logging( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that VERY_VERBOSE logging doesn't cause buffer corruption with API messages.""" + + # Track that we're receiving VV log messages and sensor updates + vv_logs_received = 0 + sensor_updates_received = 0 + errors_detected = [] + + def on_log(msg: Any) -> None: + """Capture log messages.""" + nonlocal vv_logs_received + # msg is a SubscribeLogsResponse object with 'message' attribute + # The message field is always bytes + message_text = msg.message.decode("utf-8", errors="replace") + + # Only count VV logs specifically + if "[VV]" in message_text: + vv_logs_received += 1 + + # Check for assertion or error messages + if "assert" in message_text.lower() or "error" in message_text.lower(): + errors_detected.append(message_text) + + # Write, compile and run the ESPHome device + async with run_compiled(yaml_config), api_client_connected() as client: + # Subscribe to VERY_VERBOSE logs - this enables the code path that could cause corruption + client.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE) + + # Wait for device to be ready + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "vv-logging-test" + + # Subscribe to sensor states + states = {} + + def on_state(state): + nonlocal sensor_updates_received + sensor_updates_received += 1 + states[state.key] = state + + client.subscribe_states(on_state) + + # List entities to find our test sensors + entity_info, _ = await client.list_entities_services() + + # Count sensors + sensor_count = sum(1 for e in entity_info if hasattr(e, "unit_of_measurement")) + assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}" + + # Wait for sensor updates to flow with VV logging active + # The sensors update every 50ms, so we should get many updates + await asyncio.sleep(0.25) + + # Verify we received both VV logs and sensor updates + assert vv_logs_received > 0, "Expected to receive VERY_VERBOSE log messages" + assert sensor_updates_received > 10, ( + f"Expected many sensor updates, got {sensor_updates_received}" + ) + + # Check for any errors + if errors_detected: + pytest.fail(f"Errors detected during test: {errors_detected}") + + # The test passes if we didn't hit any assertions or buffer corruption From 3f1f99cf37cfc621a44ee69d339d2401a50f70a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 15:08:33 -0500 Subject: [PATCH 18/40] Extract lock-free queue and event pool to core helpers (#9238) --- esphome/components/esp32_ble/ble.cpp | 1 - esphome/components/esp32_ble/ble.h | 8 +- esphome/components/esp32_ble/ble_event.h | 12 +- esphome/components/esp32_ble/ble_event_pool.h | 72 ---------- esphome/components/esp32_ble/queue.h | 85 ----------- esphome/core/event_pool.h | 81 +++++++++++ esphome/core/lock_free_queue.h | 132 ++++++++++++++++++ 7 files changed, 223 insertions(+), 168 deletions(-) delete mode 100644 esphome/components/esp32_ble/ble_event_pool.h delete mode 100644 esphome/components/esp32_ble/queue.h create mode 100644 esphome/core/event_pool.h create mode 100644 esphome/core/lock_free_queue.h diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b10d1fe10a..8b0cf4da98 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,7 +1,6 @@ #ifdef USE_ESP32 #include "ble.h" -#include "ble_event_pool.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 9fe996086e..ce452d65c4 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,8 +12,8 @@ #include "esphome/core/helpers.h" #include "ble_event.h" -#include "ble_event_pool.h" -#include "queue.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" #ifdef USE_ESP32 @@ -148,8 +148,8 @@ class ESP32BLE : public Component { std::vector ble_status_event_handlers_; BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; - LockFreeQueue ble_events_; - BLEEventPool ble_event_pool_; + esphome::LockFreeQueue ble_events_; + esphome::EventPool ble_event_pool_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index dd3ec3da42..9268c710f3 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -134,13 +134,13 @@ class BLEEvent { } // Destructor to clean up heap allocations - ~BLEEvent() { this->cleanup_heap_data(); } + ~BLEEvent() { this->release(); } // Default constructor for pre-allocation in pool BLEEvent() : type_(GAP) {} - // Clean up any heap-allocated data - void cleanup_heap_data() { + // Invoked on return to EventPool - clean up any heap-allocated data + void release() { if (this->type_ == GAP) { return; } @@ -161,19 +161,19 @@ class BLEEvent { // Load new event data for reuse (replaces previous event data) void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GAP; this->init_gap_data_(e, p); } void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GATTC; this->init_gattc_data_(e, i, p); } void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h deleted file mode 100644 index ef123b1325..0000000000 --- a/esphome/components/esp32_ble/ble_event_pool.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include -#include "ble_event.h" -#include "queue.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace esp32_ble { - -// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation -// Events are allocated on first use and reused thereafter, growing to peak usage -template class BLEEventPool { - public: - BLEEventPool() : total_created_(0) {} - - ~BLEEventPool() { - // Clean up any remaining events in the free list - BLEEvent *event; - while ((event = this->free_list_.pop()) != nullptr) { - delete event; - } - } - - // Allocate an event from the pool - // Returns nullptr if pool is full - BLEEvent *allocate() { - // Try to get from free list first - BLEEvent *event = this->free_list_.pop(); - if (event != nullptr) - return event; - - // Need to create a new event - if (this->total_created_ >= SIZE) { - // Pool is at capacity - return nullptr; - } - - // Use internal RAM for better performance - RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); - event = allocator.allocate(1); - - if (event == nullptr) { - // Memory allocation failed - return nullptr; - } - - // Placement new to construct the object - new (event) BLEEvent(); - this->total_created_++; - return event; - } - - // Return an event to the pool for reuse - void release(BLEEvent *event) { - if (event != nullptr) { - this->free_list_.push(event); - } - } - - private: - LockFreeQueue free_list_; // Free events ready for reuse - uint8_t total_created_; // Total events created (high water mark) -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h deleted file mode 100644 index 75bf1eef25..0000000000 --- a/esphome/components/esp32_ble/queue.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than using mutex-based locking, this lock-free queue allows the BLE - * task to enqueue events without blocking. The main loop() then processes - * these events at a safer time. - * - * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. - * The BLE task is the only producer, and the main loop() is the only consumer. - */ - -namespace esphome { -namespace esp32_ble { - -template class LockFreeQueue { - public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} - - bool push(T *element) { - if (element == nullptr) - return false; - - uint8_t current_tail = tail_.load(std::memory_order_relaxed); - uint8_t next_tail = (current_tail + 1) % SIZE; - - if (next_tail == head_.load(std::memory_order_acquire)) { - // Buffer full - dropped_count_.fetch_add(1, std::memory_order_relaxed); - return false; - } - - buffer_[current_tail] = element; - tail_.store(next_tail, std::memory_order_release); - return true; - } - - T *pop() { - uint8_t current_head = head_.load(std::memory_order_relaxed); - - if (current_head == tail_.load(std::memory_order_acquire)) { - return nullptr; // Empty - } - - T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); - return element; - } - - size_t size() const { - uint8_t tail = tail_.load(std::memory_order_acquire); - uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; - } - - uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } - - void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } - - bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } - - bool full() const { - uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; - return next_tail == head_.load(std::memory_order_acquire); - } - - protected: - T *buffer_[SIZE]; - // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) - std::atomic dropped_count_; // 65535 max - more than enough for drop tracking - // Atomic: written by consumer (pop), read by producer (push) to check if full - std::atomic head_; - // Atomic: written by producer (push), read by consumer (pop) to check if empty - std::atomic tail_; -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h new file mode 100644 index 0000000000..69e03bafac --- /dev/null +++ b/esphome/core/event_pool.h @@ -0,0 +1,81 @@ +#pragma once + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include +#include +#include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" + +namespace esphome { + +// Event Pool - On-demand pool of objects to avoid heap fragmentation +// Events are allocated on first use and reused thereafter, growing to peak usage +// @tparam T The type of objects managed by the pool (must have a release() method) +// @tparam SIZE The maximum number of objects in the pool (1-255, limited by uint8_t) +template class EventPool { + public: + EventPool() : total_created_(0) {} + + ~EventPool() { + // Clean up any remaining events in the free list + // IMPORTANT: This destructor assumes no concurrent access. The EventPool must not + // be destroyed while any thread might still call allocate() or release(). + // In practice, this is typically ensured by destroying the pool only during + // component shutdown when all producer/consumer threads have been stopped. + T *event; + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + while ((event = this->free_list_.pop()) != nullptr) { + // Call destructor + event->~T(); + // Deallocate using RAMAllocator + allocator.deallocate(event, 1); + } + } + + // Allocate an event from the pool + // Returns nullptr if pool is full + T *allocate() { + // Try to get from free list first + T *event = this->free_list_.pop(); + if (event != nullptr) + return event; + + // Need to create a new event + if (this->total_created_ >= SIZE) { + // Pool is at capacity + return nullptr; + } + + // Use internal RAM for better performance + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + event = allocator.allocate(1); + + if (event == nullptr) { + // Memory allocation failed + return nullptr; + } + + // Placement new to construct the object + new (event) T(); + this->total_created_++; + return event; + } + + // Return an event to the pool for reuse + void release(T *event) { + if (event != nullptr) { + // Clean up the event's allocated memory + event->release(); + this->free_list_.push(event); + } + } + + private: + LockFreeQueue free_list_; // Free events ready for reuse + uint8_t total_created_; // Total events created (high water mark, max 255) +}; + +} // namespace esphome + +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h new file mode 100644 index 0000000000..5460be0fae --- /dev/null +++ b/esphome/core/lock_free_queue.h @@ -0,0 +1,132 @@ +#pragma once + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include +#include + +#if defined(USE_ESP32) +#include +#include +#elif defined(USE_LIBRETINY) +#include +#include +#endif + +/* + * Lock-free queue for single-producer single-consumer scenarios. + * This allows one thread to push items and another to pop them without + * blocking each other. + * + * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. + * Available on platforms with FreeRTOS support (ESP32, LibreTiny). + * + * Common use cases: + * - BLE events: BLE task produces, main loop consumes + * - MQTT messages: main task produces, MQTT thread consumes + * + * @tparam T The type of elements stored in the queue (must be a pointer type) + * @tparam SIZE The maximum number of elements (1-255, limited by uint8_t indices) + */ + +namespace esphome { + +template class LockFreeQueue { + public: + LockFreeQueue() : head_(0), tail_(0), dropped_count_(0), task_to_notify_(nullptr) {} + + bool push(T *element) { + if (element == nullptr) + return false; + + uint8_t current_tail = tail_.load(std::memory_order_relaxed); + uint8_t next_tail = (current_tail + 1) % SIZE; + + // Read head before incrementing tail + uint8_t head_before = head_.load(std::memory_order_acquire); + + if (next_tail == head_before) { + // Buffer full + dropped_count_.fetch_add(1, std::memory_order_relaxed); + return false; + } + + // Check if queue was empty before push + bool was_empty = (current_tail == head_before); + + buffer_[current_tail] = element; + tail_.store(next_tail, std::memory_order_release); + + // Notify optimization: only notify if we need to + if (task_to_notify_ != nullptr) { + if (was_empty) { + // Queue was empty - consumer might be going to sleep, must notify + xTaskNotifyGive(task_to_notify_); + } else { + // Queue wasn't empty - check if consumer has caught up to previous tail + uint8_t head_after = head_.load(std::memory_order_acquire); + if (head_after == current_tail) { + // Consumer just caught up to where tail was - might go to sleep, must notify + // Note: There's a benign race here - between reading head_after and calling + // xTaskNotifyGive(), the consumer could advance further. This would result + // in an unnecessary wake-up, but is harmless and extremely rare in practice. + xTaskNotifyGive(task_to_notify_); + } + // Otherwise: consumer is still behind, no need to notify + } + } + + return true; + } + + T *pop() { + uint8_t current_head = head_.load(std::memory_order_relaxed); + + if (current_head == tail_.load(std::memory_order_acquire)) { + return nullptr; // Empty + } + + T *element = buffer_[current_head]; + head_.store((current_head + 1) % SIZE, std::memory_order_release); + return element; + } + + size_t size() const { + uint8_t tail = tail_.load(std::memory_order_acquire); + uint8_t head = head_.load(std::memory_order_acquire); + return (tail - head + SIZE) % SIZE; + } + + uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + + void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } + + bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } + + bool full() const { + uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + return next_tail == head_.load(std::memory_order_acquire); + } + + // Set the FreeRTOS task handle to notify when items are pushed to the queue + // This enables efficient wake-up of a consumer task that's waiting for data + // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications + void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; } + + protected: + T *buffer_[SIZE]; + // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) + std::atomic dropped_count_; // 65535 max - more than enough for drop tracking + // Atomic: written by consumer (pop), read by producer (push) to check if full + // Using uint8_t limits queue size to 255 elements but saves memory and ensures + // atomic operations are efficient on all platforms + std::atomic head_; + // Atomic: written by producer (push), read by consumer (pop) to check if empty + std::atomic tail_; + // Task handle for notification (optional) + TaskHandle_t task_to_notify_; +}; + +} // namespace esphome + +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) From 86c0fb48a3dc1454cefc8cc31338300e2b971e51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 16:08:30 -0500 Subject: [PATCH 19/40] Replace ping retry timer with batch queue fallback (#9207) --- esphome/components/api/api_connection.cpp | 32 ++++++++++++----------- esphome/components/api/api_connection.h | 15 +++++++++-- esphome/components/api/api_server.cpp | 4 +-- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 65588ad4d8..f339a4b26f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -65,10 +65,6 @@ uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_ void APIConnection::start() { this->last_traffic_ = App.get_loop_component_start_time(); - // Set next_ping_retry_ to prevent immediate ping - // This ensures the first ping happens after the keepalive period - this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS; - APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); @@ -176,20 +172,15 @@ void APIConnection::loop() { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) { + } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { ESP_LOGVV(TAG, "Sending keepalive PING"); this->sent_ping_ = this->send_message(PingRequest()); if (!this->sent_ping_) { - this->next_ping_retry_ = now + PING_RETRY_INTERVAL; - this->ping_retries_++; - if (this->ping_retries_ >= MAX_PING_RETRIES) { - on_fatal_error(); - ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_); - } else if (this->ping_retries_ >= 10) { - ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); - } else { - ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); - } + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); + this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings } } @@ -1773,6 +1764,11 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c items.emplace_back(entity, std::move(creator), message_type); } +void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) { + // Insert at front for high priority messages (no deduplication check) + items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type)); +} + bool APIConnection::schedule_batch_() { if (!this->deferred_batch_.batch_scheduled) { this->deferred_batch_.batch_scheduled = true; @@ -1963,6 +1959,12 @@ uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConne return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); } +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single) { + PingRequest req; + return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); +} + uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) { // Use generated ESTIMATED_SIZE constants from each message type switch (message_type) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 07e87ab39f..4397462d8e 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -185,7 +185,6 @@ class APIConnection : public APIServerConnection { void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping - this->ping_retries_ = 0; this->sent_ping_ = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; @@ -441,13 +440,16 @@ class APIConnection : public APIServerConnection { // Helper function to get estimated message size for buffer pre-allocation static uint16_t get_estimated_message_size(uint16_t message_type); + // Batch message method for ping requests + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single); + // Pointers first (4 bytes each, naturally aligned) std::unique_ptr helper_; APIServer *parent_; // 4-byte aligned types uint32_t last_traffic_; - uint32_t next_ping_retry_{0}; int state_subs_at_ = -1; // Strings (12 bytes each on 32-bit) @@ -470,6 +472,7 @@ class APIConnection : public APIServerConnection { bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; + // 7 bytes used, 1 byte padding #ifdef HAS_PROTO_MESSAGE_DUMP // When true, encode_message_to_buffer will only log, not encode bool log_only_mode_{false}; @@ -602,6 +605,8 @@ class APIConnection : public APIServerConnection { // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); + // Add item to the front of the batch (for high priority messages like ping) + void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); void clear() { items.clear(); batch_scheduled = false; @@ -645,6 +650,12 @@ class APIConnection : public APIServerConnection { bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { return schedule_message_(entity, MessageCreator(function_ptr), message_type); } + + // Helper function to schedule a high priority message at the front of the batch + bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { + this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type); + return this->schedule_batch_(); + } }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 583837af82..a33623b15a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -526,8 +526,8 @@ void APIServer::on_shutdown() { for (auto &c : this->clients_) { if (!c->send_message(DisconnectRequest())) { // If we can't send the disconnect request directly (tx_buffer full), - // schedule it in the batch so it will be sent with the 5ms timer - c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); + // schedule it at the front of the batch so it will be sent with priority + c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); } } } From 0c249a700610564d96a34896abaffc913b8a48c0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:16:34 -0500 Subject: [PATCH 20/40] [thermostat] Memory optimizations (#9259) --- .../thermostat/thermostat_climate.cpp | 65 ++++++++------- .../thermostat/thermostat_climate.h | 82 ++++++++++--------- 2 files changed, 77 insertions(+), 70 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index fe6ed8b159..404e585aff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -997,7 +997,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { auto config = this->preset_config_.find(preset); if (config != this->preset_config_.end()) { - ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined @@ -1015,7 +1015,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->custom_preset.reset(); this->preset = preset; } else { - ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } } @@ -1023,7 +1023,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); + ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || this->custom_preset.value() != custom_preset) { // Fire any preset changed trigger if defined @@ -1041,7 +1041,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) this->preset.reset(); this->custom_preset = custom_preset; } else { - ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str()); + ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } } @@ -1298,7 +1298,7 @@ void ThermostatClimate::dump_config() { if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); } - ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); + ESP_LOGCONFIG(TAG, " Use Start-up Delay: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:\n" @@ -1353,44 +1353,47 @@ void ThermostatClimate::dump_config() { } ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, - " Supports AUTO: %s\n" - " Supports HEAT/COOL: %s\n" - " Supports COOL: %s\n" - " Supports DRY: %s\n" - " Supports FAN_ONLY: %s\n" - " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" - " Supports FAN_ONLY_COOLING: %s", - YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_cool_), - YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), + " Supported MODES:\n" + " AUTO: %s\n" + " HEAT/COOL: %s\n" + " HEAT: %s\n" + " COOL: %s\n" + " DRY: %s\n" + " FAN_ONLY: %s\n" + " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" + " FAN_ONLY_COOLING: %s", + YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), + YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); if (this->supports_cool_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); } if (this->supports_heat_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); } - ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, - " Supports FAN MODE ON: %s\n" - " Supports FAN MODE OFF: %s\n" - " Supports FAN MODE AUTO: %s\n" - " Supports FAN MODE LOW: %s\n" - " Supports FAN MODE MEDIUM: %s\n" - " Supports FAN MODE HIGH: %s\n" - " Supports FAN MODE MIDDLE: %s\n" - " Supports FAN MODE FOCUS: %s\n" - " Supports FAN MODE DIFFUSE: %s\n" - " Supports FAN MODE QUIET: %s", + " Supported FAN MODES:\n" + " ON: %s\n" + " OFF: %s\n" + " AUTO: %s\n" + " LOW: %s\n" + " MEDIUM: %s\n" + " HIGH: %s\n" + " MIDDLE: %s\n" + " FOCUS: %s\n" + " DIFFUSE: %s\n" + " QUIET: %s", YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_)); ESP_LOGCONFIG(TAG, - " Supports SWING MODE BOTH: %s\n" - " Supports SWING MODE OFF: %s\n" - " Supports SWING MODE HORIZONTAL: %s\n" - " Supports SWING MODE VERTICAL: %s\n" + " Supported SWING MODES:\n" + " BOTH: %s\n" + " OFF: %s\n" + " HORIZONTAL: %s\n" + " VERTICAL: %s\n" " Supports TWO SET POINTS: %s", YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_), YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_), diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 50510cf070..007d7297d5 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -13,7 +13,7 @@ namespace esphome { namespace thermostat { -enum ThermostatClimateTimerIndex : size_t { +enum ThermostatClimateTimerIndex : uint8_t { TIMER_COOLING_MAX_RUN_TIME = 0, TIMER_COOLING_OFF = 1, TIMER_COOLING_ON = 2, @@ -26,7 +26,11 @@ enum ThermostatClimateTimerIndex : size_t { TIMER_IDLE_ON = 9, }; -enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; +enum OnBootRestoreFrom : uint8_t { + MEMORY = 0, + DEFAULT_PRESET = 1, +}; + struct ThermostatClimateTimer { bool active; uint32_t time; @@ -65,7 +69,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_default_preset(const std::string &custom_preset); void set_default_preset(climate::ClimatePreset preset); - void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from); + void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); void set_cool_deadband(float deadband); void set_cool_overrun(float overrun); @@ -240,10 +244,8 @@ class ThermostatClimate : public climate::Climate, public Component { void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, bool is_default_preset); - /// The sensor used for getting the current temperature - sensor::Sensor *sensor_{nullptr}; - /// The sensor used for getting the current humidity - sensor::Sensor *humidity_sensor_{nullptr}; + /// Minimum allowable duration in seconds for action timers + const uint8_t min_timer_duration_{1}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// @@ -310,6 +312,31 @@ class ThermostatClimate : public climate::Climate, public Component { /// setup_complete_ blocks modifying/resetting the temps immediately after boot bool setup_complete_{false}; + /// Store previously-known temperatures + /// + /// These are used to determine when the temperature change trigger/action needs to be called + float prev_target_temperature_{NAN}; + float prev_target_temperature_low_{NAN}; + float prev_target_temperature_high_{NAN}; + + /// Minimum differential required between set points + float set_point_minimum_differential_{0}; + + /// Hysteresis values used for computing climate actions + float cooling_deadband_{0}; + float cooling_overrun_{0}; + float heating_deadband_{0}; + float heating_overrun_{0}; + + /// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions + float supplemental_cool_delta_{0}; + float supplemental_heat_delta_{0}; + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /// The trigger to call when the controller should switch to cooling action/mode. /// /// A null value for this attribute means that the controller has no cooling action @@ -399,7 +426,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the target temperature(s) change(es). Trigger<> *temperature_change_trigger_{nullptr}; - /// The triggr to call when the preset mode changes + /// The trigger to call when the preset mode changes Trigger<> *preset_change_trigger_{nullptr}; /// A reference to the trigger that was previously active. @@ -411,6 +438,10 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; + /// Store previously-known states /// /// These are used to determine when a trigger/action needs to be called @@ -419,28 +450,10 @@ class ThermostatClimate : public climate::Climate, public Component { climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; - /// Store previously-known temperatures - /// - /// These are used to determine when the temperature change trigger/action needs to be called - float prev_target_temperature_{NAN}; - float prev_target_temperature_low_{NAN}; - float prev_target_temperature_high_{NAN}; - - /// Minimum differential required between set points - float set_point_minimum_differential_{0}; - - /// Hysteresis values used for computing climate actions - float cooling_deadband_{0}; - float cooling_overrun_{0}; - float heating_deadband_{0}; - float heating_overrun_{0}; - - /// Maximum allowable temperature deltas before engauging supplemental cooling/heating actions - float supplemental_cool_delta_{0}; - float supplemental_heat_delta_{0}; - - /// Minimum allowable duration in seconds for action timers - const uint8_t min_timer_duration_{1}; + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + /// Default custom preset to use on start up + std::string default_custom_preset_{}; /// Climate action timers std::vector timer_{ @@ -460,15 +473,6 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; - - /// Default standard preset to use on start up - climate::ClimatePreset default_preset_{}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - - /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior - /// state will attempt to be restored if possible - thermostat::OnBootRestoreFrom on_boot_restore_from_{thermostat::OnBootRestoreFrom::MEMORY}; }; } // namespace thermostat From 53ab016098be0719f182c3586b65d47e0f54a85c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:17:53 -0500 Subject: [PATCH 21/40] [adc] Memory optimizations (#9247) --- esphome/components/adc/adc_sensor.h | 15 ++++++++---- esphome/components/adc/adc_sensor_common.cpp | 2 +- esphome/components/adc/adc_sensor_esp32.cpp | 24 +++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 62f2461245..9ffb6cf856 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -28,19 +28,24 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11; #endif #endif // USE_ESP32 -enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 }; +enum class SamplingMode : uint8_t { + AVG = 0, + MIN = 1, + MAX = 2, +}; + const LogString *sampling_mode_to_str(SamplingMode mode); class Aggregator { public: + Aggregator(SamplingMode mode); void add_sample(uint32_t value); uint32_t aggregate(); - Aggregator(SamplingMode mode); protected: - SamplingMode mode_{SamplingMode::AVG}; uint32_t aggr_{0}; uint32_t samples_{0}; + SamplingMode mode_{SamplingMode::AVG}; }; class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { @@ -81,9 +86,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #endif // USE_RP2040 protected: - InternalGPIOPin *pin_; - bool output_raw_{false}; uint8_t sample_count_{1}; + bool output_raw_{false}; + InternalGPIOPin *pin_; SamplingMode sampling_mode_{SamplingMode::AVG}; #ifdef USE_RP2040 diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index c7509c7c7a..797ab75045 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -61,7 +61,7 @@ uint32_t Aggregator::aggregate() { void ADCSensor::update() { float value_v = this->sample(); - ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); + ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v); this->publish_state(value_v); } diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index d6cf6e893b..ed1f3329ab 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -55,32 +55,40 @@ void ADCSensor::setup() { } void ADCSensor::dump_config() { + static const char *const ATTEN_AUTO_STR = "auto"; + static const char *const ATTEN_0DB_STR = "0 db"; + static const char *const ATTEN_2_5DB_STR = "2.5 db"; + static const char *const ATTEN_6DB_STR = "6 db"; + static const char *const ATTEN_12DB_STR = "12 db"; + const char *atten_str = ATTEN_AUTO_STR; + LOG_SENSOR("", "ADC Sensor", this); LOG_PIN(" Pin: ", this->pin_); - if (this->autorange_) { - ESP_LOGCONFIG(TAG, " Attenuation: auto"); - } else { + + if (!this->autorange_) { switch (this->attenuation_) { case ADC_ATTEN_DB_0: - ESP_LOGCONFIG(TAG, " Attenuation: 0db"); + atten_str = ATTEN_0DB_STR; break; case ADC_ATTEN_DB_2_5: - ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); + atten_str = ATTEN_2_5DB_STR; break; case ADC_ATTEN_DB_6: - ESP_LOGCONFIG(TAG, " Attenuation: 6db"); + atten_str = ATTEN_6DB_STR; break; case ADC_ATTEN_DB_12_COMPAT: - ESP_LOGCONFIG(TAG, " Attenuation: 12db"); + atten_str = ATTEN_12DB_STR; break; default: // This is to satisfy the unused ADC_ATTEN_MAX break; } } + ESP_LOGCONFIG(TAG, + " Attenuation: %s\n" " Samples: %i\n" " Sampling mode: %s", - this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); + atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } From 21e1f3d1034098e27f9d793a08436955486f7e40 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:28:51 -0500 Subject: [PATCH 22/40] [light] Memory optimizations (#9260) --- .../components/light/esp_color_correction.h | 2 +- esphome/components/light/light_call.cpp | 44 +++++++++---------- esphome/components/light/light_state.h | 9 ++-- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 979a1acb07..39ce5700c6 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -69,8 +69,8 @@ class ESPColorCorrection { protected: uint8_t gamma_table_[256]; uint8_t gamma_reverse_table_[256]; - Color max_brightness_; uint8_t local_brightness_{255}; + Color max_brightness_; }; } // namespace light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index c2600d05c2..78b0ac9feb 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -136,7 +136,7 @@ LightColorValues LightCall::validate_() { // Color mode check if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { - ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, + ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); this->color_mode_.reset(); } @@ -152,20 +152,20 @@ LightColorValues LightCall::validate_() { // Brightness exists check if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); + ESP_LOGW(TAG, "'%s': setting brightness not supported", name); this->brightness_.reset(); } // Transition length possible check if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } // Color brightness exists check if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB brightness!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); this->color_brightness_.reset(); } @@ -173,7 +173,7 @@ LightColorValues LightCall::validate_() { if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || (this->blue_.has_value() && *this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); this->red_.reset(); this->green_.reset(); this->blue_.reset(); @@ -183,14 +183,14 @@ LightColorValues LightCall::validate_() { // White value exists check if (this->white_.has_value() && *this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); this->white_.reset(); } // Color temperature exists check if (this->color_temperature_.has_value() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); this->color_temperature_.reset(); } @@ -198,7 +198,7 @@ LightColorValues LightCall::validate_() { if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); this->cold_white_.reset(); this->warm_white_.reset(); } @@ -208,7 +208,7 @@ LightColorValues LightCall::validate_() { if (name_##_.has_value()) { \ auto val = *name_##_; \ if (val < (min) || val > (max)) { \ - ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [%.1f - %.1f]!", name, LOG_STR_LITERAL(upper_name), val, \ + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ (min), (max)); \ name_##_ = clamp(val, (min), (max)); \ } \ @@ -270,7 +270,7 @@ LightColorValues LightCall::validate_() { // Flash length check if (this->has_flash_() && *this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); + ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); this->flash_length_.reset(); } @@ -284,18 +284,18 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); + ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_); this->effect_.reset(); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); + ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); this->transition_length_.reset(); this->flash_length_.reset(); } if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); + ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); this->transition_length_.reset(); } @@ -311,7 +311,7 @@ LightColorValues LightCall::validate_() { } if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } @@ -320,7 +320,7 @@ LightColorValues LightCall::validate_() { // Reason: When user turns off the light in frontend, the effect should also stop if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); + ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); this->effect_.reset(); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect @@ -348,7 +348,7 @@ void LightCall::transform_parameters_() { !(*this->color_mode_ & ColorCapability::WHITE) && // !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { - ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", + ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); if (this->color_temperature_.has_value()) { const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); @@ -388,8 +388,8 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the current mode is suitable. if (suitable_modes.count(current_mode) > 0) { - ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); + ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } @@ -398,7 +398,7 @@ ColorMode LightCall::compute_color_mode_() { if (supported_modes.count(mode) == 0) continue; - ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; } @@ -406,8 +406,8 @@ ColorMode LightCall::compute_color_mode_() { // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip // out whatever we don't support. auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); - ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(color_mode))); + ESP_LOGW(TAG, "'%s': no suitable color mode supported; defaulting to %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } std::set LightCall::get_suitable_color_modes_() { @@ -472,7 +472,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { } } if (!found) { - ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); + ESP_LOGW(TAG, "'%s': no such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); } return *this; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index b93823feac..f21fb8a06e 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -225,6 +225,11 @@ class LightState : public EntityBase, public Component { /// Gamma correction factor for the light. float gamma_correct_{}; + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_ = false; + /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; @@ -247,10 +252,6 @@ class LightState : public EntityBase, public Component { /// Restore mode of the light. LightRestoreMode restore_mode_; - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; - // for effects, true if a transformer (transition) is active. - bool is_transformer_active_ = false; }; } // namespace light From 921d0888cdce6e94a29c024c604812e54aa59d40 Mon Sep 17 00:00:00 2001 From: Rezoran <30475103+Rezoran@users.noreply.github.com> Date: Sun, 29 Jun 2025 17:05:23 +0200 Subject: [PATCH 23/40] [uart] fix: missing uart_config_t struct initialisation (#9235) --- esphome/components/uart/uart_component_esp_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 84bd48d530..8fae63a603 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -42,7 +42,7 @@ uart_config_t IDFUARTComponent::get_config_() { break; } - uart_config_t uart_config; + uart_config_t uart_config{}; uart_config.baud_rate = this->baud_rate_; uart_config.data_bits = data_bits; uart_config.parity = parity; From ddbcf8549c21c68a48aade8824a22f55c055f03e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 13:29:18 -0500 Subject: [PATCH 24/40] Reduce web_server code duplication by extracting detail parameter parsing (#9257) --- esphome/components/web_server/web_server.cpp | 126 ++++--------------- 1 file changed, 26 insertions(+), 100 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index becb5bc2c7..9f42253794 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -370,6 +370,12 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { set_json_value(root, obj, sensor, value, start_config); \ (root)["state"] = state; +// Helper to get request detail parameter +static JsonDetail get_request_detail(AsyncWebServerRequest *request) { + auto *param = request->getParam("detail"); + return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; +} + #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { if (this->events_.empty()) @@ -381,11 +387,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -435,11 +437,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -483,11 +481,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -534,11 +528,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->button_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "press") { @@ -584,11 +574,7 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -632,11 +618,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->fan_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -722,11 +704,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->light_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -847,11 +825,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->cover_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -937,11 +911,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->number_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1016,11 +986,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->date_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1084,11 +1050,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->time_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1151,11 +1113,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->datetime_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1220,11 +1178,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->text_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1290,11 +1244,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->select_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1358,11 +1308,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->climate_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1526,11 +1472,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "lock") { @@ -1583,11 +1525,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->valve_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1664,11 +1602,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; @@ -1740,11 +1674,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); return; @@ -1795,11 +1725,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->update_json(obj, detail); request->send(200, "application/json", data.c_str()); return; From ed9850c4a4937b0276cb0c2565a4ac81caf387b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 13:46:28 -0500 Subject: [PATCH 25/40] Remove redundant get_setup_priority() overrides returning default value (#9253) --- esphome/components/ade7880/ade7880.h | 2 -- esphome/components/ads1115/ads1115.h | 1 - esphome/components/ads1118/ads1118.h | 1 - esphome/components/ags10/ags10.h | 2 -- esphome/components/aic3204/aic3204.h | 1 - esphome/components/alpha3/alpha3.h | 1 - esphome/components/am43/cover/am43_cover.h | 1 - esphome/components/am43/sensor/am43_sensor.h | 1 - .../analog_threshold/analog_threshold_binary_sensor.h | 2 -- esphome/components/anova/anova.h | 1 - esphome/components/as5600/as5600.h | 1 - esphome/components/atc_mithermometer/atc_mithermometer.h | 1 - esphome/components/b_parasite/b_parasite.h | 1 - esphome/components/ble_client/output/ble_binary_output.h | 1 - esphome/components/ble_client/sensor/ble_rssi_sensor.h | 1 - esphome/components/ble_client/sensor/ble_sensor.h | 1 - esphome/components/ble_client/switch/ble_switch.h | 1 - esphome/components/ble_client/text_sensor/ble_text_sensor.h | 1 - esphome/components/ble_presence/ble_presence_device.h | 1 - esphome/components/ble_rssi/ble_rssi_sensor.h | 1 - esphome/components/ble_scanner/ble_scanner.h | 1 - esphome/components/bmp581/bmp581.h | 2 -- esphome/components/cap1188/cap1188.h | 1 - esphome/components/ccs811/ccs811.h | 2 -- esphome/components/copy/binary_sensor/copy_binary_sensor.h | 1 - esphome/components/copy/button/copy_button.h | 1 - esphome/components/copy/cover/copy_cover.h | 1 - esphome/components/copy/fan/copy_fan.h | 1 - esphome/components/copy/lock/copy_lock.h | 1 - esphome/components/copy/number/copy_number.h | 1 - esphome/components/copy/select/copy_select.h | 1 - esphome/components/copy/sensor/copy_sensor.h | 1 - esphome/components/copy/switch/copy_switch.h | 1 - esphome/components/copy/text/copy_text.h | 1 - esphome/components/copy/text_sensor/copy_text_sensor.h | 1 - esphome/components/cs5460a/cs5460a.h | 1 - esphome/components/duty_time/duty_time_sensor.h | 1 - esphome/components/ens160_base/ens160_base.h | 1 - esphome/components/es7210/es7210.h | 1 - esphome/components/es7243e/es7243e.h | 1 - esphome/components/es8156/es8156.h | 1 - esphome/components/es8311/es8311.h | 1 - esphome/components/es8388/es8388.h | 1 - esphome/components/esp32_touch/esp32_touch.h | 1 - esphome/components/ezo/ezo.h | 1 - esphome/components/ezo_pmp/ezo_pmp.h | 1 - esphome/components/feedback/feedback_cover.h | 1 - esphome/components/fs3000/fs3000.h | 1 - esphome/components/gcja5/gcja5.h | 1 - esphome/components/gp8403/gp8403.h | 1 - esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h | 2 -- esphome/components/he60r/he60r.h | 1 - esphome/components/honeywellabp2_i2c/honeywellabp2.h | 1 - esphome/components/i2c_device/i2c_device.h | 1 - esphome/components/iaqcore/iaqcore.h | 2 -- esphome/components/ina260/ina260.h | 2 -- esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h | 1 - esphome/components/integration/integration_sensor.h | 1 - esphome/components/interval/interval.h | 2 -- esphome/components/ltr390/ltr390.h | 1 - esphome/components/ltr501/ltr501.h | 1 - esphome/components/ltr_als_ps/ltr_als_ps.h | 1 - esphome/components/max9611/max9611.h | 1 - esphome/components/mcp9600/mcp9600.h | 2 -- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 1 - esphome/components/mopeka_std_check/mopeka_std_check.h | 1 - esphome/components/mpl3115a2/mpl3115a2.h | 2 -- esphome/components/ms8607/ms8607.h | 1 - esphome/components/pmsa003i/pmsa003i.h | 1 - esphome/components/pmsx003/pmsx003.h | 1 - esphome/components/pn7150/pn7150.h | 1 - esphome/components/pn7160/pn7160.h | 1 - esphome/components/pulse_counter/pulse_counter_sensor.h | 1 - esphome/components/pulse_width/pulse_width.h | 1 - esphome/components/pvvx_mithermometer/display/pvvx_display.h | 2 -- esphome/components/pvvx_mithermometer/pvvx_mithermometer.h | 1 - esphome/components/qwiic_pir/qwiic_pir.h | 1 - esphome/components/rc522/rc522.h | 1 - esphome/components/rdm6300/rdm6300.h | 2 -- esphome/components/remote_receiver/remote_receiver.h | 1 - esphome/components/resistance/resistance_sensor.h | 1 - esphome/components/ruuvitag/ruuvitag.h | 1 - esphome/components/scd30/scd30.h | 1 - esphome/components/scd4x/scd4x.h | 1 - esphome/components/script/script.h | 2 -- esphome/components/sen5x/sen5x.h | 1 - esphome/components/senseair/senseair.h | 1 - esphome/components/servo/servo.h | 1 - esphome/components/sfa30/sfa30.h | 1 - esphome/components/sgp30/sgp30.h | 1 - esphome/components/sgp4x/sgp4x.h | 1 - esphome/components/sht4x/sht4x.h | 1 - esphome/components/sm300d2/sm300d2.h | 2 -- esphome/components/sps30/sps30.h | 1 - esphome/components/status/status_binary_sensor.h | 2 -- esphome/components/switch/binary_sensor/switch_binary_sensor.h | 1 - esphome/components/tmp1075/tmp1075.h | 2 -- esphome/components/tof10120/tof10120_sensor.h | 1 - esphome/components/tormatic/tormatic_cover.h | 1 - esphome/components/total_daily_energy/total_daily_energy.h | 1 - esphome/components/ttp229_bsf/ttp229_bsf.h | 1 - esphome/components/ttp229_lsf/ttp229_lsf.h | 1 - esphome/components/vbus/vbus.h | 1 - esphome/components/veml3235/veml3235.h | 1 - esphome/components/veml7700/veml7700.h | 1 - esphome/components/vl53l0x/vl53l0x_sensor.h | 1 - esphome/components/xiaomi_cgd1/xiaomi_cgd1.h | 1 - esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h | 1 - esphome/components/xiaomi_cgg1/xiaomi_cgg1.h | 1 - esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h | 1 - esphome/components/xiaomi_gcls002/xiaomi_gcls002.h | 1 - esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h | 1 - esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h | 1 - esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h | 1 - esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h | 1 - esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h | 1 - esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h | 1 - esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h | 1 - esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h | 1 - esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h | 1 - esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h | 1 - esphome/components/xiaomi_miscale/xiaomi_miscale.h | 1 - esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h | 1 - esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h | 1 - esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h | 1 - esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h | 1 - esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h | 1 - esphome/components/zio_ultrasonic/zio_ultrasonic.h | 2 -- esphome/components/zyaura/zyaura.h | 1 - 129 files changed, 147 deletions(-) diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h index a565357dc5..40bc22e54a 100644 --- a/esphome/components/ade7880/ade7880.h +++ b/esphome/components/ade7880/ade7880.h @@ -85,8 +85,6 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: ADE7880Store store_{}; InternalGPIOPin *irq0_pin_{nullptr}; diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index e65835a386..e827a739d2 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -49,7 +49,6 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } /// Helper method to request a measurement from a sensor. diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h index 8b9aa15cd2..e96baab386 100644 --- a/esphome/components/ads1118/ads1118.h +++ b/esphome/components/ads1118/ads1118.h @@ -34,7 +34,6 @@ class ADS1118 : public Component, ADS1118() = default; void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } /// Helper method to request a measurement from a sensor. float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode); diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index f2201fe70c..3e184ae176 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -31,8 +31,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - /** * Modifies target address of AGS10. * diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h index 783a58a2b9..28006e33fc 100644 --- a/esphome/components/aic3204/aic3204.h +++ b/esphome/components/aic3204/aic3204.h @@ -66,7 +66,6 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool set_mute_off() override; bool set_mute_on() override; diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h index 325c70a538..7189ecbc33 100644 --- a/esphome/components/alpha3/alpha3.h +++ b/esphome/components/alpha3/alpha3.h @@ -41,7 +41,6 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h index f33f2d1734..d6d020e98c 100644 --- a/esphome/components/am43/cover/am43_cover.h +++ b/esphome/components/am43/cover/am43_cover.h @@ -22,7 +22,6 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; void set_pin(uint16_t pin) { this->pin_ = pin; } void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 8dfe83e3a3..91973d8e33 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -22,7 +22,6 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery(sensor::Sensor *battery) { battery_ = battery; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index efb8e3c90c..55d6b15c36 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -12,8 +12,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina void dump_config() override; void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_sensor(sensor::Sensor *analog_sensor); template void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; } template void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; } diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 3d1394980a..560d96baa7 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -26,7 +26,6 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h index fbfd18db40..914a4431bd 100644 --- a/esphome/components/as5600/as5600.h +++ b/esphome/components/as5600/as5600.h @@ -50,7 +50,6 @@ class AS5600Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } // configuration setters void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; } diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index 31fb77ac7f..d22e3f069b 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -25,7 +25,6 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h index 70ee4ab23c..7dd08968ec 100644 --- a/esphome/components/b_parasite/b_parasite.h +++ b/esphome/components/b_parasite/b_parasite.h @@ -16,7 +16,6 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 0a1e186b26..5e8bd6da62 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -16,7 +16,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi public: void dump_config() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 5dd3fc7af9..76cd8345a6 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -18,7 +18,6 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index b11a010ee4..24d1ed2fd2 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -24,7 +24,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 2e19c8aeef..9809f904e7 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -19,7 +19,6 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index cb34043b46..c75a4df952 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -20,7 +20,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 3ed60d1b49..70ecc67c32 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -105,7 +105,6 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->set_found_(false); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void set_found_(bool state) { diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 89e4f33aca..80245a1fe1 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -99,7 +99,6 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi return false; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index b330eff696..8bb51fcff2 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -29,7 +29,6 @@ class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESP return true; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } }; } // namespace ble_scanner diff --git a/esphome/components/bmp581/bmp581.h b/esphome/components/bmp581/bmp581.h index 7327be44ae..1d7e932fa1 100644 --- a/esphome/components/bmp581/bmp581.h +++ b/esphome/components/bmp581/bmp581.h @@ -61,8 +61,6 @@ enum IIRFilter { class BMP581Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; void setup() override; diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h index fa0ed622fa..baefd1c48f 100644 --- a/esphome/components/cap1188/cap1188.h +++ b/esphome/components/cap1188/cap1188.h @@ -46,7 +46,6 @@ class CAP1188Component : public Component, public i2c::I2CDevice { void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index 8a0d60d002..675ba7da97 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -25,8 +25,6 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: optional read_status_() { return this->read_byte(0x00); } bool status_has_error_() { return this->read_status_().value_or(1) & 1; } diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h index d62ed13c76..fc1e368b38 100644 --- a/esphome/components/copy/binary_sensor/copy_binary_sensor.h +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h @@ -11,7 +11,6 @@ class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { void set_source(binary_sensor::BinarySensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: binary_sensor::BinarySensor *source_; diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h index 9996ca0c65..79d5dbcf04 100644 --- a/esphome/components/copy/button/copy_button.h +++ b/esphome/components/copy/button/copy_button.h @@ -10,7 +10,6 @@ class CopyButton : public button::Button, public Component { public: void set_source(button::Button *source) { source_ = source; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void press_action() override; diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h index fb278523ff..ec27b6782a 100644 --- a/esphome/components/copy/cover/copy_cover.h +++ b/esphome/components/copy/cover/copy_cover.h @@ -11,7 +11,6 @@ class CopyCover : public cover::Cover, public Component { void set_source(cover::Cover *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index 1a69810510..b474975bc4 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -11,7 +11,6 @@ class CopyFan : public fan::Fan, public Component { void set_source(fan::Fan *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } fan::FanTraits get_traits() override; diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h index 0554013674..8799eebb4a 100644 --- a/esphome/components/copy/lock/copy_lock.h +++ b/esphome/components/copy/lock/copy_lock.h @@ -11,7 +11,6 @@ class CopyLock : public lock::Lock, public Component { void set_source(lock::Lock *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const lock::LockCall &call) override; diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h index 1ad956fec4..09b65e2cbf 100644 --- a/esphome/components/copy/number/copy_number.h +++ b/esphome/components/copy/number/copy_number.h @@ -11,7 +11,6 @@ class CopyNumber : public number::Number, public Component { void set_source(number::Number *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(float value) override; diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index c8666cd394..fb0aee86f6 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -11,7 +11,6 @@ class CopySelect : public select::Select, public Component { void set_source(select::Select *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h index 1ae790ada3..500e6872fe 100644 --- a/esphome/components/copy/sensor/copy_sensor.h +++ b/esphome/components/copy/sensor/copy_sensor.h @@ -11,7 +11,6 @@ class CopySensor : public sensor::Sensor, public Component { void set_source(sensor::Sensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: sensor::Sensor *source_; diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h index 26cb254ab3..80310af03f 100644 --- a/esphome/components/copy/switch/copy_switch.h +++ b/esphome/components/copy/switch/copy_switch.h @@ -11,7 +11,6 @@ class CopySwitch : public switch_::Switch, public Component { void set_source(switch_::Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/copy/text/copy_text.h b/esphome/components/copy/text/copy_text.h index beb8610dfe..9eaebae4be 100644 --- a/esphome/components/copy/text/copy_text.h +++ b/esphome/components/copy/text/copy_text.h @@ -11,7 +11,6 @@ class CopyText : public text::Text, public Component { void set_source(text::Text *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h index fe91fe948b..489986c59d 100644 --- a/esphome/components/copy/text_sensor/copy_text_sensor.h +++ b/esphome/components/copy/text_sensor/copy_text_sensor.h @@ -11,7 +11,6 @@ class CopyTextSensor : public text_sensor::TextSensor, public Component { void set_source(text_sensor::TextSensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: text_sensor::TextSensor *source_; diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 763ddc14fa..15ae04f3c6 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -77,7 +77,6 @@ class CS5460AComponent : public Component, void setup() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 38655f104a..18280f8e21 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -19,7 +19,6 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { void update() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void start(); void stop(); diff --git a/esphome/components/ens160_base/ens160_base.h b/esphome/components/ens160_base/ens160_base.h index 729225a5ae..ae850c8180 100644 --- a/esphome/components/ens160_base/ens160_base.h +++ b/esphome/components/ens160_base/ens160_base.h @@ -18,7 +18,6 @@ class ENS160Component : public PollingComponent, public sensor::Sensor { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h index 8f6d9d8136..7071a547ec 100644 --- a/esphome/components/es7210/es7210.h +++ b/esphome/components/es7210/es7210.h @@ -25,7 +25,6 @@ class ES7210 : public audio_adc::AudioAdc, public Component, public i2c::I2CDevi */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } diff --git a/esphome/components/es7243e/es7243e.h b/esphome/components/es7243e/es7243e.h index 41a8acac8d..f7c9d67371 100644 --- a/esphome/components/es7243e/es7243e.h +++ b/esphome/components/es7243e/es7243e.h @@ -14,7 +14,6 @@ class ES7243E : public audio_adc::AudioAdc, public Component, public i2c::I2CDev */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; bool set_mic_gain(float mic_gain) override; diff --git a/esphome/components/es8156/es8156.h b/esphome/components/es8156/es8156.h index e973599a7a..082514485c 100644 --- a/esphome/components/es8156/es8156.h +++ b/esphome/components/es8156/es8156.h @@ -14,7 +14,6 @@ class ES8156 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h index 840a07204c..5eccc48004 100644 --- a/esphome/components/es8311/es8311.h +++ b/esphome/components/es8311/es8311.h @@ -50,7 +50,6 @@ class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8388/es8388.h b/esphome/components/es8388/es8388.h index 45944f68bd..373f71b437 100644 --- a/esphome/components/es8388/es8388.h +++ b/esphome/components/es8388/es8388.h @@ -38,7 +38,6 @@ class ES8388 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 0eac590ce7..3fce8a7e18 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -52,7 +52,6 @@ class ESP32TouchComponent : public Component { void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index 28b46643e9..00dd98fc80 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -38,7 +38,6 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void loop() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; // I2C void set_address(uint8_t address); diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index b41710cd78..671e124810 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -23,7 +23,6 @@ namespace ezo_pmp { class EzoPMP : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; void update() override; diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 7e107aebcd..199d3b520a 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -16,7 +16,6 @@ class FeedbackCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; Trigger<> *get_open_trigger() const { return this->open_trigger_; } Trigger<> *get_close_trigger() const { return this->close_trigger_; } diff --git a/esphome/components/fs3000/fs3000.h b/esphome/components/fs3000/fs3000.h index be3680e7e1..e33c72215f 100644 --- a/esphome/components/fs3000/fs3000.h +++ b/esphome/components/fs3000/fs3000.h @@ -18,7 +18,6 @@ class FS3000Component : public PollingComponent, public i2c::I2CDevice, public s void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_model(FS3000Model model) { this->model_ = model; } diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h index ea1fb78bf0..30bc877169 100644 --- a/esphome/components/gcja5/gcja5.h +++ b/esphome/components/gcja5/gcja5.h @@ -12,7 +12,6 @@ class GCJA5Component : public Component, public uart::UARTDevice { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 65182ef301..9f493d39e3 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -15,7 +15,6 @@ class GP8403 : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h index 1987d33f37..aab881bd05 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h @@ -22,8 +22,6 @@ class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2C void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: enum ErrorCode { UNKNOWN, diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h index e41e2203c1..02a2b44e66 100644 --- a/esphome/components/he60r/he60r.h +++ b/esphome/components/he60r/he60r.h @@ -13,7 +13,6 @@ class HE60rCover : public cover::Cover, public Component, public uart::UARTDevic void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index bc81524ac2..274de847ac 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -18,7 +18,6 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }; void loop() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void dump_config() override; void read_sensor_data(); diff --git a/esphome/components/i2c_device/i2c_device.h b/esphome/components/i2c_device/i2c_device.h index ab118e3e89..9944ca9204 100644 --- a/esphome/components/i2c_device/i2c_device.h +++ b/esphome/components/i2c_device/i2c_device.h @@ -9,7 +9,6 @@ namespace i2c_device { class I2CDeviceComponent : public Component, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: }; diff --git a/esphome/components/iaqcore/iaqcore.h b/esphome/components/iaqcore/iaqcore.h index f343c2a705..bb0bfcc754 100644 --- a/esphome/components/iaqcore/iaqcore.h +++ b/esphome/components/iaqcore/iaqcore.h @@ -16,8 +16,6 @@ class IAQCore : public PollingComponent, public i2c::I2CDevice { void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *co2_{nullptr}; sensor::Sensor *tvoc_{nullptr}; diff --git a/esphome/components/ina260/ina260.h b/esphome/components/ina260/ina260.h index 8bad1cba6d..6cbc157cf3 100644 --- a/esphome/components/ina260/ina260.h +++ b/esphome/components/ina260/ina260.h @@ -13,8 +13,6 @@ class INA260Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { this->bus_voltage_sensor_ = bus_voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h index bdca2d0cac..cd2ea99717 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -16,7 +16,6 @@ class InkbirdIbstH1Mini : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_external_temperature(sensor::Sensor *external_temperature) { external_temperature_ = external_temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index e84d7a8ed1..d9f2f5e50f 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -27,7 +27,6 @@ class IntegrationSensor : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_time(IntegrationSensorTime time) { time_ = time; } void set_method(IntegrationMethod method) { method_ = method; } diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 5b8bc3081f..8f904b104d 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -23,8 +23,6 @@ class IntervalTrigger : public Trigger<>, public PollingComponent { void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: uint32_t startup_delay_{0}; bool started_{false}; diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7359cbd336..7db73d68ff 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -44,7 +44,6 @@ enum LTR390RESOLUTION { class LTR390Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 07b69fa0d0..849ff6bc23 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -25,7 +25,6 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 4cbbcea54c..2c768009ab 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -25,7 +25,6 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/max9611/max9611.h b/esphome/components/max9611/max9611.h index 017f56b1a7..1eb7542aee 100644 --- a/esphome/components/max9611/max9611.h +++ b/esphome/components/max9611/max9611.h @@ -38,7 +38,6 @@ class MAX9611Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void set_voltage_sensor(sensor::Sensor *vs) { voltage_sensor_ = vs; } void set_current_sensor(sensor::Sensor *cs) { current_sensor_ = cs; } diff --git a/esphome/components/mcp9600/mcp9600.h b/esphome/components/mcp9600/mcp9600.h index 92612cc26d..c414653ea6 100644 --- a/esphome/components/mcp9600/mcp9600.h +++ b/esphome/components/mcp9600/mcp9600.h @@ -24,8 +24,6 @@ class MCP9600Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_hot_junction(sensor::Sensor *hot_junction) { this->hot_junction_sensor_ = hot_junction; } void set_cold_junction(sensor::Sensor *cold_junction) { this->cold_junction_sensor_ = cold_junction; } void set_thermocouple_type(MCP9600ThermocoupleType thermocouple_type) { diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index c58406ac18..4cbe8f2afe 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -34,7 +34,6 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_min_signal_quality(SensorReadQuality min) { this->min_signal_quality_ = min; }; void set_level(sensor::Sensor *level) { level_ = level; }; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 2a1d9d2dfc..b92445df34 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -48,7 +48,6 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_level(sensor::Sensor *level) { this->level_ = level; }; void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }; diff --git a/esphome/components/mpl3115a2/mpl3115a2.h b/esphome/components/mpl3115a2/mpl3115a2.h index 00a6d90c52..05da71f830 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.h +++ b/esphome/components/mpl3115a2/mpl3115a2.h @@ -91,8 +91,6 @@ class MPL3115A2Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *altitude_{nullptr}; diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index 0bee7e97b7..67ce2817fa 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -37,7 +37,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h index 59f39a7314..cd106704a6 100644 --- a/esphome/components/pmsa003i/pmsa003i.h +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -32,7 +32,6 @@ class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_standard_units(bool standard_units) { this->standard_units_ = standard_units; } diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index e422d4165b..ba607b4487 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -31,7 +31,6 @@ enum PMSX003State { class PMSX003Component : public uart::UARTDevice, public Component { public: PMSX003Component() = default; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void loop() override; diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 87af7d629b..42cd7a6ef7 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -146,7 +146,6 @@ class PN7150 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index ff8a492b7b..fc00296a71 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -161,7 +161,6 @@ class PN7160 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index cea9fa7bf9..5ba59cca2a 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -76,7 +76,6 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { /// Unit of measurement is "pulses/min". void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/pulse_width/pulse_width.h b/esphome/components/pulse_width/pulse_width.h index 822688ec88..c6b896988d 100644 --- a/esphome/components/pulse_width/pulse_width.h +++ b/esphome/components/pulse_width/pulse_width.h @@ -32,7 +32,6 @@ class PulseWidthSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override { this->store_.setup(this->pin_); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; protected: diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index dfeb49c49d..9739362024 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -39,8 +39,6 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void update() override; void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index 99455a1663..9614a3c586 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -25,7 +25,6 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/qwiic_pir/qwiic_pir.h b/esphome/components/qwiic_pir/qwiic_pir.h index d58d67734f..797ded2cc6 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.h +++ b/esphome/components/qwiic_pir/qwiic_pir.h @@ -36,7 +36,6 @@ class QwiicPIRComponent : public Component, public i2c::I2CDevice, public binary void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_debounce_time(uint16_t debounce_time) { this->debounce_time_ = debounce_time; } void set_debounce_mode(DebounceMode mode) { this->debounce_mode_ = mode; } diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index c6c5e119f0..437cea808b 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -19,7 +19,6 @@ class RC522 : public PollingComponent { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index 1a1a0c0cd6..24a808b62c 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -21,8 +21,6 @@ class RDM6300Component : public Component, public uart::UARTDevice { void register_card(RDM6300BinarySensor *obj) { this->cards_.push_back(obj); } void register_trigger(RDM6300Trigger *trig) { this->triggers_.push_back(trig); } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: int8_t read_state_{-1}; uint8_t buffer_[6]{}; diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 9d844eee66..45e06e664a 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -59,7 +59,6 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_ESP32 void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index b57f90b59c..a3b6e92c59 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -24,7 +24,6 @@ class ResistanceSensor : public Component, public sensor::Sensor { this->process_(this->sensor_->state); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void process_(float value); diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h index 63029ebb4d..dfe393724c 100644 --- a/esphome/components/ruuvitag/ruuvitag.h +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -48,7 +48,6 @@ class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_pressure(sensor::Sensor *pressure) { pressure_ = pressure; } diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 40f075e673..ed3f5e7e9a 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -26,7 +26,6 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe void setup() override; void update(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: bool is_data_ready_(); diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 22055e78d0..f2efb28ac1 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -19,7 +19,6 @@ enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RH class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 165f90ed11..60175ec933 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -239,8 +239,6 @@ template class ScriptWaitAction : public Action, this->play_next_tuple_(this->var_); } - float get_setup_priority() const override { return setup_priority::DATA; } - void play(Ts... x) override { /* ignore - see play_complex */ } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 6d90636a89..0fa31605e6 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -48,7 +48,6 @@ struct TemperatureCompensation { class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index bcec638f79..9f939d5b07 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -10,7 +10,6 @@ namespace senseair { class SenseAirComponent : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void update() override; diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 92d18bf601..ff1708dc53 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -20,7 +20,6 @@ class Servo : public Component { void detach(); void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_min_level(float min_level) { min_level_ = min_level; } void set_idle_level(float idle_level) { idle_level_ = idle_level; } void set_max_level(float max_level) { max_level_ = max_level; } diff --git a/esphome/components/sfa30/sfa30.h b/esphome/components/sfa30/sfa30.h index fa2c59f624..2b744b8da4 100644 --- a/esphome/components/sfa30/sfa30.h +++ b/esphome/components/sfa30/sfa30.h @@ -11,7 +11,6 @@ class SFA30Component : public PollingComponent, public sensirion_common::Sensiri enum ErrorCode { DEVICE_MARKING_READ_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 9e882e6b05..e6429a7bfa 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -32,7 +32,6 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 45ee66af68..8b31bca28c 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -75,7 +75,6 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se void update() override; void take_sample(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 98e0629b50..accc7323be 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -17,7 +17,6 @@ enum SHT4XHEATERTIME : uint16_t { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h index 88c04e9813..4e97b54988 100644 --- a/esphome/components/sm300d2/sm300d2.h +++ b/esphome/components/sm300d2/sm300d2.h @@ -9,8 +9,6 @@ namespace sm300d2 { class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor) { formaldehyde_sensor_ = formaldehyde_sensor; } void set_tvoc_sensor(sensor::Sensor *tvoc_sensor) { tvoc_sensor_ = tvoc_sensor; } diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index cf2e7a7d4f..04189247e8 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -26,7 +26,6 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool start_fan_cleaning(); diff --git a/esphome/components/status/status_binary_sensor.h b/esphome/components/status/status_binary_sensor.h index 08aa0fb32f..feda8b6328 100644 --- a/esphome/components/status/status_binary_sensor.h +++ b/esphome/components/status/status_binary_sensor.h @@ -13,8 +13,6 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - bool is_status_binary_sensor() const override { return true; } }; diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h index 5a947c2fb4..53b07da903 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.h +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h @@ -12,7 +12,6 @@ class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component void set_source(Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: Switch *source_; diff --git a/esphome/components/tmp1075/tmp1075.h b/esphome/components/tmp1075/tmp1075.h index 84e2e8abe4..b5fd60c08e 100644 --- a/esphome/components/tmp1075/tmp1075.h +++ b/esphome/components/tmp1075/tmp1075.h @@ -58,8 +58,6 @@ class TMP1075Sensor : public PollingComponent, public sensor::Sensor, public i2c void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; // Call write_config() after calling any of these to send the new config to diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h index 90bad8ed07..d0cca19d4c 100644 --- a/esphome/components/tof10120/tof10120_sensor.h +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -12,7 +12,6 @@ class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; }; } // namespace tof10120 diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 33a2e1db8f..534d4bef14 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -16,7 +16,6 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1a9d5d1a49..1145f54f95 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -23,7 +23,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void publish_state_and_save(float state); diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.h b/esphome/components/ttp229_bsf/ttp229_bsf.h index 2663afcec9..fea4356b55 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.h +++ b/esphome/components/ttp229_bsf/ttp229_bsf.h @@ -25,7 +25,6 @@ class TTP229BSFComponent : public Component { void register_channel(TTP229BSFChannel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override { // check datavalid if sdo is high if (!this->sdo_pin_->digital_read()) { diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.h b/esphome/components/ttp229_lsf/ttp229_lsf.h index f8775a17f0..7cc4bfca89 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.h +++ b/esphome/components/ttp229_lsf/ttp229_lsf.h @@ -23,7 +23,6 @@ class TTP229LSFComponent : public Component, public i2c::I2CDevice { void register_channel(TTP229Channel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h index 7e97b5049a..0a253f1bdb 100644 --- a/esphome/components/vbus/vbus.h +++ b/esphome/components/vbus/vbus.h @@ -30,7 +30,6 @@ class VBus : public uart::UARTDevice, public Component { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void register_listener(VBusListener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h index 2b0d6b23ea..b57e1571f1 100644 --- a/esphome/components/veml3235/veml3235.h +++ b/esphome/components/veml3235/veml3235.h @@ -65,7 +65,6 @@ class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; void update() override { this->publish_state(this->read_lx_()); } - float get_setup_priority() const override { return setup_priority::DATA; } // Used by ESPHome framework. Does NOT actually set the value on the device. void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; } diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index 17fee6b851..b0d1451cf0 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -102,7 +102,6 @@ class VEML7700Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index dd76e8e0ab..2bf90015fe 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -30,7 +30,6 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void loop() override; diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h index d05cffc4d1..393795439b 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -17,7 +17,6 @@ class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h index 8fd9946537..1f5ef89869 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h @@ -17,7 +17,6 @@ class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListe bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 966c05ac79..52904fd75e 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -18,7 +18,6 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h index eff4b1c6fb..124f9411a1 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -21,7 +21,6 @@ class XiaomiCGPR1 : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h index 08e1bd7e54..83c8f15ace 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -17,7 +17,6 @@ class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index aa99cc004a..96ea9217fb 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -17,7 +17,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h index bc1e580ce4..bd4ad75c1d 100644 --- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h @@ -16,7 +16,6 @@ class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { this->moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { this->conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h index ce746b9ee0..0ec34b1871 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -17,7 +17,6 @@ class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h index ca1ad0f27e..e9c44800f2 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -17,7 +17,6 @@ class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; } diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index 641a02bd5a..772b389a92 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -17,7 +17,6 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h index 19092aa2a9..e1e0fcae40 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -18,7 +18,6 @@ class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h index 95710a1508..3c7907479a 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -17,7 +17,6 @@ class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index cbc76f9dd3..cf90db937f 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -17,7 +17,6 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h index d0304f7894..c3b8e7d68f 100644 --- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h +++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h @@ -17,7 +17,6 @@ class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h index 4ab882b2af..1acdaa88af 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -17,7 +17,6 @@ class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index 4523bbc82b..10d308ef6c 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -23,7 +23,6 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_weight(sensor::Sensor *weight) { weight_ = weight; } void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; } void set_clear_impedance(bool clear_impedance) { clear_impedance_ = clear_impedance; } diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index 34b1fe4af0..e1b4055696 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -21,7 +21,6 @@ class XiaomiMJYD02YLA : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h index 904c575ae6..f1da0705d0 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -19,7 +19,6 @@ class XiaomiMUE4094RT : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_time(uint16_t timeout) { timeout_ = timeout; } protected: diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h index a16c5209d9..ae00a28ac9 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -23,7 +23,6 @@ class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_BINARY_SENSOR void set_motion(binary_sensor::BinarySensor *motion) { this->motion_ = motion; } diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h index 297c7ab47d..081705fd50 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -20,7 +20,6 @@ class XiaomiWX08ZM : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_tablet(sensor::Sensor *tablet) { tablet_ = tablet; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h index 9ce02bb64e..ed0458ce49 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -18,7 +18,6 @@ class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h index 84c8d44c65..23057b2ab0 100644 --- a/esphome/components/zio_ultrasonic/zio_ultrasonic.h +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h @@ -11,8 +11,6 @@ namespace zio_ultrasonic { class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; void update() override; diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h index 85c31ec75a..3070aa90c5 100644 --- a/esphome/components/zyaura/zyaura.h +++ b/esphome/components/zyaura/zyaura.h @@ -69,7 +69,6 @@ class ZyAuraSensor : public PollingComponent { void setup() override { this->store_.setup(this->pin_clock_, this->pin_data_); } void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: ZaSensorStore store_; From a4cc6166a0c8f014b3e821910f81a52e15070fae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:20:52 -0500 Subject: [PATCH 26/40] Bump aioesphomeapi from 33.1.1 to 34.0.0 (#9265) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3f306fe4fa..12f3b84359 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==33.1.1 +aioesphomeapi==34.0.0 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From b743577ebe37063992506272d74431ad8a14e0ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Jun 2025 08:07:29 +1200 Subject: [PATCH 27/40] Fix api log client crashing when api encryption is dynamic (#9245) --- esphome/components/api/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 7f8e2529a5..2d4bc37c89 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -35,8 +35,8 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: str | None = None - if CONF_ENCRYPTION in conf: - noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] + if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): + noise_psk = key _LOGGER.info("Starting log output from %s using esphome API", address) cli = APIClient( address, From 971bbd088c9eaa0590d4ce5c51c840a246b99c3d Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Sun, 29 Jun 2025 21:34:59 +0100 Subject: [PATCH 28/40] Fix MQTT blocking main loop for multiple seconds at a time (#8325) Co-authored-by: patagona Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/mqtt/mqtt_backend_esp32.cpp | 118 +++++++++++++++++- esphome/components/mqtt/mqtt_backend_esp32.h | 110 +++++++++++++++- 2 files changed, 223 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 64dc27d84b..62b153e676 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -6,6 +6,7 @@ #include #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace mqtt { @@ -100,9 +101,24 @@ bool MQTTBackendESP32::initialize_() { handler_.reset(mqtt_client); is_initalized_ = true; esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this); +#if defined(USE_MQTT_IDF_ENQUEUE) + // Create the task only after MQTT client is initialized successfully + // Use larger stack size when TLS is enabled + size_t stack_size = this->ca_certificate_.has_value() ? TASK_STACK_SIZE_TLS : TASK_STACK_SIZE; + xTaskCreate(esphome_mqtt_task, "esphome_mqtt", stack_size, (void *) this, TASK_PRIORITY, &this->task_handle_); + if (this->task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create MQTT task"); + // Clean up MQTT client since we can't start the async task + handler_.reset(); + is_initalized_ = false; + return false; + } + // Set the task handle so the queue can notify it + this->mqtt_queue_.set_task_to_notify(this->task_handle_); +#endif return true; } else { - ESP_LOGE(TAG, "Failed to initialize IDF-MQTT"); + ESP_LOGE(TAG, "Failed to init client"); return false; } } @@ -115,6 +131,26 @@ void MQTTBackendESP32::loop() { mqtt_event_handler_(event); mqtt_events_.pop(); } + +#if defined(USE_MQTT_IDF_ENQUEUE) + // Periodically log dropped messages to avoid blocking during spikes. + // During high load, many messages can be dropped in quick succession. + // Logging each drop immediately would flood the logs and potentially + // cause more drops if MQTT logging is enabled (cascade effect). + // Instead, we accumulate the count and log a summary periodically. + // IMPORTANT: Don't move this to the scheduler - if drops are due to memory + // pressure, the scheduler's heap allocations would make things worse. + uint32_t now = App.get_loop_component_start_time(); + // Handle rollover: (now - last_time) works correctly with unsigned arithmetic + // even when now < last_time due to rollover + if ((now - this->last_dropped_log_time_) >= DROP_LOG_INTERVAL_MS) { + uint16_t dropped = this->mqtt_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u messages (%us)", dropped, DROP_LOG_INTERVAL_MS / 1000); + } + this->last_dropped_log_time_ = now; + } +#endif } void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { @@ -188,6 +224,86 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b } } +#if defined(USE_MQTT_IDF_ENQUEUE) +void MQTTBackendESP32::esphome_mqtt_task(void *params) { + MQTTBackendESP32 *this_mqtt = (MQTTBackendESP32 *) params; + + while (true) { + // Wait for notification indefinitely + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Process all queued items + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + if (this_mqtt->is_connected_) { + switch (elem->type) { + case MQTT_QUEUE_TYPE_SUBSCRIBE: + esp_mqtt_client_subscribe(this_mqtt->handler_.get(), elem->topic, elem->qos); + break; + + case MQTT_QUEUE_TYPE_UNSUBSCRIBE: + esp_mqtt_client_unsubscribe(this_mqtt->handler_.get(), elem->topic); + break; + + case MQTT_QUEUE_TYPE_PUBLISH: + esp_mqtt_client_publish(this_mqtt->handler_.get(), elem->topic, elem->payload, elem->payload_len, elem->qos, + elem->retain); + break; + + default: + ESP_LOGE(TAG, "Invalid operation type from MQTT queue"); + break; + } + } + this_mqtt->mqtt_event_pool_.release(elem); + } + } + + // Clean up any remaining items in the queue + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + this_mqtt->mqtt_event_pool_.release(elem); + } + + // Note: EventPool destructor will clean up the pool itself + // Task will delete itself + vTaskDelete(nullptr); +} + +bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, + size_t len) { + auto *elem = this->mqtt_event_pool_.allocate(); + + if (!elem) { + // Queue is full - increment counter but don't log immediately. + // Logging here can cause a cascade effect: if MQTT logging is enabled, + // each dropped message would generate a log message, which could itself + // be sent via MQTT, causing more drops and more logs in a feedback loop + // that eventually triggers a watchdog reset. Instead, we log periodically + // in loop() to prevent blocking the event loop during spikes. + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + elem->type = type; + elem->qos = qos; + elem->retain = retain; + + // Use the helper to allocate and copy data + if (!elem->set_data(topic, payload, len)) { + // Allocation failed, return elem to pool + this->mqtt_event_pool_.release(elem); + // Increment counter without logging to avoid cascade effect during memory pressure + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + // Push to queue - always succeeds since we allocated from the pool + this->mqtt_queue_.push(elem); + return true; +} +#endif // USE_MQTT_IDF_ENQUEUE + } // namespace mqtt } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 9054702115..57286a24b2 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -6,9 +6,14 @@ #include #include +#include #include +#include +#include #include "esphome/components/network/ip_address.h" #include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" namespace esphome { namespace mqtt { @@ -42,9 +47,79 @@ struct Event { error_handle(*event.error_handle) {} }; +enum MqttQueueTypeT : uint8_t { + MQTT_QUEUE_TYPE_NONE = 0, + MQTT_QUEUE_TYPE_SUBSCRIBE, + MQTT_QUEUE_TYPE_UNSUBSCRIBE, + MQTT_QUEUE_TYPE_PUBLISH, +}; + +struct QueueElement { + char *topic; + char *payload; + uint16_t payload_len; // MQTT max payload is 64KiB + uint8_t type : 2; + uint8_t qos : 2; // QoS only needs values 0-2 + uint8_t retain : 1; + uint8_t reserved : 3; // Reserved for future use + + QueueElement() : topic(nullptr), payload(nullptr), payload_len(0), qos(0), retain(0), reserved(0) {} + + // Helper to set topic/payload (uses RAMAllocator) + bool set_data(const char *topic_str, const char *payload_data, size_t len) { + // Check payload size limit (MQTT max is 64KiB) + if (len > std::numeric_limits::max()) { + return false; + } + + // Use RAMAllocator with default flags (tries external RAM first, falls back to internal) + RAMAllocator allocator; + + // Allocate and copy topic + size_t topic_len = strlen(topic_str) + 1; + topic = allocator.allocate(topic_len); + if (!topic) + return false; + memcpy(topic, topic_str, topic_len); + + if (payload_data && len) { + payload = allocator.allocate(len); + if (!payload) { + allocator.deallocate(topic, topic_len); + topic = nullptr; + return false; + } + memcpy(payload, payload_data, len); + payload_len = static_cast(len); + } else { + payload = nullptr; + payload_len = 0; + } + return true; + } + + // Helper to release (uses RAMAllocator) + void release() { + RAMAllocator allocator; + if (topic) { + allocator.deallocate(topic, strlen(topic) + 1); + topic = nullptr; + } + if (payload) { + allocator.deallocate(payload, payload_len); + payload = nullptr; + } + payload_len = 0; + } +}; + class MQTTBackendESP32 final : public MQTTBackend { public: static const size_t MQTT_BUFFER_SIZE = 4096; + static const size_t TASK_STACK_SIZE = 2048; + static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static const ssize_t TASK_PRIORITY = 5; + static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } @@ -105,15 +180,23 @@ class MQTTBackendESP32 final : public MQTTBackend { } bool subscribe(const char *topic, uint8_t qos) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_SUBSCRIBE, topic, qos); +#else return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1; +#endif + } + bool unsubscribe(const char *topic) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_UNSUBSCRIBE, topic); +#else + return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; +#endif } - bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; } bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { #if defined(USE_MQTT_IDF_ENQUEUE) - // use the non-blocking version - // it can delay sending a couple of seconds but won't block - return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1; + return enqueue_(MQTT_QUEUE_TYPE_PUBLISH, topic, qos, retain, payload, length); #else // might block for several seconds, either due to network timeout (10s) // or if publishing payloads longer than internal buffer (due to message fragmentation) @@ -129,6 +212,12 @@ class MQTTBackendESP32 final : public MQTTBackend { void set_cl_key(const std::string &key) { cl_key_ = key; } void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } + // No destructor needed: ESPHome components live for the entire device runtime. + // The MQTT task and queue will run until the device reboots or loses power, + // at which point the entire process terminates and FreeRTOS cleans up all tasks. + // Implementing a destructor would add complexity and potential race conditions + // for a scenario that never occurs in practice. + protected: bool initialize_(); void mqtt_event_handler_(const Event &event); @@ -160,6 +249,14 @@ class MQTTBackendESP32 final : public MQTTBackend { optional cl_certificate_; optional cl_key_; bool skip_cert_cn_check_{false}; +#if defined(USE_MQTT_IDF_ENQUEUE) + static void esphome_mqtt_task(void *params); + EventPool mqtt_event_pool_; + LockFreeQueue mqtt_queue_; + TaskHandle_t task_handle_{nullptr}; + bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, + size_t len = 0); +#endif // callbacks CallbackManager on_connect_; @@ -169,6 +266,11 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager on_message_; CallbackManager on_publish_; std::queue mqtt_events_; + +#if defined(USE_MQTT_IDF_ENQUEUE) + uint32_t last_dropped_log_time_{0}; + static constexpr uint32_t DROP_LOG_INTERVAL_MS = 10000; // Log every 10 seconds +#endif }; } // namespace mqtt From d592208c74d80e573471774f6401b8244effd5b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 17:45:41 -0500 Subject: [PATCH 29/40] Fix crash when event last_event_type is null in web_server (#9266) --- esphome/components/web_server/web_server.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9f42253794..927659e621 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1683,12 +1683,15 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } +static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; } + std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), - DETAIL_STATE); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_STATE); } std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), DETAIL_ALL); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_ALL); } std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { return json::build_json([this, obj, event_type, start_config](JsonObject root) { From d78b7203503012529680e624efcc7573b366ab41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:38:11 -0500 Subject: [PATCH 30/40] Remove single-use send_*_info wrappers in API connection (#9255) --- esphome/components/api/api_connection.cpp | 70 --------- esphome/components/api/api_connection.h | 23 +-- esphome/components/api/list_entities.cpp | 173 +++++++-------------- esphome/components/api/list_entities.h | 52 ++++--- esphome/components/api/subscribe_state.cpp | 52 +++---- esphome/components/api/subscribe_state.h | 45 +++--- 6 files changed, 132 insertions(+), 283 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f339a4b26f..8550d45bfc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -304,10 +304,6 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary return this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_state, BinarySensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { - this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_info, - ListEntitiesBinarySensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -335,9 +331,6 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool APIConnection::send_cover_state(cover::Cover *cover) { return this->schedule_message_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); } -void APIConnection::send_cover_info(cover::Cover *cover) { - this->schedule_message_(cover, &APIConnection::try_send_cover_info, ListEntitiesCoverResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *cover = static_cast(entity); @@ -399,9 +392,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { bool APIConnection::send_fan_state(fan::Fan *fan) { return this->schedule_message_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); } -void APIConnection::send_fan_info(fan::Fan *fan) { - this->schedule_message_(fan, &APIConnection::try_send_fan_info, ListEntitiesFanResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *fan = static_cast(entity); @@ -461,9 +451,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { bool APIConnection::send_light_state(light::LightState *light) { return this->schedule_message_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); } -void APIConnection::send_light_info(light::LightState *light) { - this->schedule_message_(light, &APIConnection::try_send_light_info, ListEntitiesLightResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *light = static_cast(entity); @@ -556,9 +543,6 @@ void APIConnection::light_command(const LightCommandRequest &msg) { bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { return this->schedule_message_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_sensor_info(sensor::Sensor *sensor) { - this->schedule_message_(sensor, &APIConnection::try_send_sensor_info, ListEntitiesSensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -591,9 +575,6 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * bool APIConnection::send_switch_state(switch_::Switch *a_switch) { return this->schedule_message_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); } -void APIConnection::send_switch_info(switch_::Switch *a_switch) { - this->schedule_message_(a_switch, &APIConnection::try_send_switch_info, ListEntitiesSwitchResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -632,10 +613,6 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) return this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_state, TextSensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { - this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_info, - ListEntitiesTextSensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -696,9 +673,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.target_humidity = climate->target_humidity; return encode_message_to_buffer(resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_climate_info(climate::Climate *climate) { - this->schedule_message_(climate, &APIConnection::try_send_climate_info, ListEntitiesClimateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *climate = static_cast(entity); @@ -766,9 +740,6 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { bool APIConnection::send_number_state(number::Number *number) { return this->schedule_message_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); } -void APIConnection::send_number_info(number::Number *number) { - this->schedule_message_(number, &APIConnection::try_send_number_info, ListEntitiesNumberResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -820,9 +791,6 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c fill_entity_state_base(date, resp); return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_date_info(datetime::DateEntity *date) { - this->schedule_message_(date, &APIConnection::try_send_date_info, ListEntitiesDateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *date = static_cast(entity); @@ -857,9 +825,6 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c fill_entity_state_base(time, resp); return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_time_info(datetime::TimeEntity *time) { - this->schedule_message_(time, &APIConnection::try_send_time_info, ListEntitiesTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *time = static_cast(entity); @@ -896,9 +861,6 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio fill_entity_state_base(datetime, resp); return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { - this->schedule_message_(datetime, &APIConnection::try_send_datetime_info, ListEntitiesDateTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *datetime = static_cast(entity); @@ -922,9 +884,6 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { bool APIConnection::send_text_state(text::Text *text) { return this->schedule_message_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); } -void APIConnection::send_text_info(text::Text *text) { - this->schedule_message_(text, &APIConnection::try_send_text_info, ListEntitiesTextResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -963,9 +922,6 @@ void APIConnection::text_command(const TextCommandRequest &msg) { bool APIConnection::send_select_state(select::Select *select) { return this->schedule_message_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); } -void APIConnection::send_select_info(select::Select *select) { - this->schedule_message_(select, &APIConnection::try_send_select_info, ListEntitiesSelectResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -999,9 +955,6 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -void esphome::api::APIConnection::send_button_info(button::Button *button) { - this->schedule_message_(button, &APIConnection::try_send_button_info, ListEntitiesButtonResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *button = static_cast(entity); @@ -1024,9 +977,6 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg bool APIConnection::send_lock_state(lock::Lock *a_lock) { return this->schedule_message_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); } -void APIConnection::send_lock_info(lock::Lock *a_lock) { - this->schedule_message_(a_lock, &APIConnection::try_send_lock_info, ListEntitiesLockResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1080,9 +1030,6 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * fill_entity_state_base(valve, resp); return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_valve_info(valve::Valve *valve) { - this->schedule_message_(valve, &APIConnection::try_send_valve_info, ListEntitiesValveResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *valve = static_cast(entity); @@ -1128,10 +1075,6 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne fill_entity_state_base(media_player, resp); return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { - this->schedule_message_(media_player, &APIConnection::try_send_media_player_info, - ListEntitiesMediaPlayerResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *media_player = static_cast(entity); @@ -1183,9 +1126,6 @@ void APIConnection::set_camera_state(std::shared_ptr image->was_requested_by(esphome::esp32_camera::IDLE)) this->image_reader_.set_image(std::move(image)); } -void APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { - this->schedule_message_(camera, &APIConnection::try_send_camera_info, ListEntitiesCameraResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *camera = static_cast(entity); @@ -1392,10 +1332,6 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A fill_entity_state_base(a_alarm_control_panel, resp); return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->schedule_message_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_info, - ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *a_alarm_control_panel = static_cast(entity); @@ -1446,9 +1382,6 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe void APIConnection::send_event(event::Event *event, const std::string &event_type) { this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); } -void APIConnection::send_event_info(event::Event *event) { - this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; @@ -1494,9 +1427,6 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection fill_entity_state_base(update, resp); return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_update_info(update::UpdateEntity *update) { - this->schedule_message_(update, &APIConnection::try_send_update_info, ListEntitiesUpdateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *update = static_cast(entity); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 4397462d8e..c9f24a7759 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -22,6 +22,7 @@ static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; class APIConnection : public APIServerConnection { public: friend class APIServer; + friend class ListEntitiesIterator; APIConnection(std::unique_ptr socket, APIServer *parent); virtual ~APIConnection(); @@ -34,93 +35,74 @@ class APIConnection : public APIServerConnection { } #ifdef USE_BINARY_SENSOR bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); - void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void send_cover_info(cover::Cover *cover); void cover_command(const CoverCommandRequest &msg) override; #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void send_fan_info(fan::Fan *fan); void fan_command(const FanCommandRequest &msg) override; #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void send_light_info(light::LightState *light); void light_command(const LightCommandRequest &msg) override; #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); - void send_sensor_info(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void send_switch_info(switch_::Switch *a_switch); void switch_command(const SwitchCommandRequest &msg) override; #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); - void send_text_sensor_info(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_ESP32_CAMERA void set_camera_state(std::shared_ptr image); - void send_camera_info(esp32_camera::ESP32Camera *camera); void camera_image(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void send_climate_info(climate::Climate *climate); void climate_command(const ClimateCommandRequest &msg) override; #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void send_date_info(datetime::DateEntity *date); void date_command(const DateCommandRequest &msg) override; #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void send_time_info(datetime::TimeEntity *time); void time_command(const TimeCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void send_datetime_info(datetime::DateTimeEntity *datetime); void datetime_command(const DateTimeCommandRequest &msg) override; #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void send_text_info(text::Text *text); void text_command(const TextCommandRequest &msg) override; #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void send_select_info(select::Select *select); void select_command(const SelectCommandRequest &msg) override; #endif #ifdef USE_BUTTON - void send_button_info(button::Button *button); void button_command(const ButtonCommandRequest &msg) override; #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void send_lock_info(lock::Lock *a_lock); void lock_command(const LockCommandRequest &msg) override; #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void send_valve_info(valve::Valve *valve); void valve_command(const ValveCommandRequest &msg) override; #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void send_media_player_info(media_player::MediaPlayer *media_player); void media_player_command(const MediaPlayerCommandRequest &msg) override; #endif bool try_send_log_message(int level, const char *tag, const char *line); @@ -167,18 +149,15 @@ class APIConnection : public APIServerConnection { #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; #endif #ifdef USE_EVENT void send_event(event::Event *event, const std::string &event_type); - void send_event_info(event::Event *event); #endif #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void send_update_info(update::UpdateEntity *update); void update_command(const UpdateCommandRequest &msg) override; #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index ceee3f00b8..3f84ef306e 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -1,6 +1,7 @@ #include "list_entities.h" #ifdef USE_API #include "api_connection.h" +#include "api_pb2.h" #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -8,155 +9,85 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - this->client_->send_binary_sensor_info(binary_sensor); - return true; -} +LIST_ENTITIES_HANDLER(binary_sensor, binary_sensor::BinarySensor, ListEntitiesBinarySensorResponse) #endif #ifdef USE_COVER -bool ListEntitiesIterator::on_cover(cover::Cover *cover) { - this->client_->send_cover_info(cover); - return true; -} +LIST_ENTITIES_HANDLER(cover, cover::Cover, ListEntitiesCoverResponse) #endif #ifdef USE_FAN -bool ListEntitiesIterator::on_fan(fan::Fan *fan) { - this->client_->send_fan_info(fan); - return true; -} +LIST_ENTITIES_HANDLER(fan, fan::Fan, ListEntitiesFanResponse) #endif #ifdef USE_LIGHT -bool ListEntitiesIterator::on_light(light::LightState *light) { - this->client_->send_light_info(light); - return true; -} +LIST_ENTITIES_HANDLER(light, light::LightState, ListEntitiesLightResponse) #endif #ifdef USE_SENSOR -bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { - this->client_->send_sensor_info(sensor); - return true; -} +LIST_ENTITIES_HANDLER(sensor, sensor::Sensor, ListEntitiesSensorResponse) #endif #ifdef USE_SWITCH -bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { - this->client_->send_switch_info(a_switch); - return true; -} +LIST_ENTITIES_HANDLER(switch, switch_::Switch, ListEntitiesSwitchResponse) #endif #ifdef USE_BUTTON -bool ListEntitiesIterator::on_button(button::Button *button) { - this->client_->send_button_info(button); - return true; -} +LIST_ENTITIES_HANDLER(button, button::Button, ListEntitiesButtonResponse) #endif #ifdef USE_TEXT_SENSOR -bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - this->client_->send_text_sensor_info(text_sensor); - return true; -} +LIST_ENTITIES_HANDLER(text_sensor, text_sensor::TextSensor, ListEntitiesTextSensorResponse) #endif #ifdef USE_LOCK -bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { - this->client_->send_lock_info(a_lock); - return true; -} +LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse) #endif #ifdef USE_VALVE -bool ListEntitiesIterator::on_valve(valve::Valve *valve) { - this->client_->send_valve_info(valve); - return true; -} +LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse) +#endif +#ifdef USE_ESP32_CAMERA +LIST_ENTITIES_HANDLER(camera, esp32_camera::ESP32Camera, ListEntitiesCameraResponse) +#endif +#ifdef USE_CLIMATE +LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse) +#endif +#ifdef USE_NUMBER +LIST_ENTITIES_HANDLER(number, number::Number, ListEntitiesNumberResponse) +#endif +#ifdef USE_DATETIME_DATE +LIST_ENTITIES_HANDLER(date, datetime::DateEntity, ListEntitiesDateResponse) +#endif +#ifdef USE_DATETIME_TIME +LIST_ENTITIES_HANDLER(time, datetime::TimeEntity, ListEntitiesTimeResponse) +#endif +#ifdef USE_DATETIME_DATETIME +LIST_ENTITIES_HANDLER(datetime, datetime::DateTimeEntity, ListEntitiesDateTimeResponse) +#endif +#ifdef USE_TEXT +LIST_ENTITIES_HANDLER(text, text::Text, ListEntitiesTextResponse) +#endif +#ifdef USE_SELECT +LIST_ENTITIES_HANDLER(select, select::Select, ListEntitiesSelectResponse) +#endif +#ifdef USE_MEDIA_PLAYER +LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMediaPlayerResponse) +#endif +#ifdef USE_ALARM_CONTROL_PANEL +LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel, + ListEntitiesAlarmControlPanelResponse) +#endif +#ifdef USE_EVENT +LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) +#endif +#ifdef USE_UPDATE +LIST_ENTITIES_HANDLER(update, update::UpdateEntity, ListEntitiesUpdateResponse) #endif +// Special cases that don't follow the pattern bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } + ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} + bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp); } -#ifdef USE_ESP32_CAMERA -bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { - this->client_->send_camera_info(camera); - return true; -} -#endif - -#ifdef USE_CLIMATE -bool ListEntitiesIterator::on_climate(climate::Climate *climate) { - this->client_->send_climate_info(climate); - return true; -} -#endif - -#ifdef USE_NUMBER -bool ListEntitiesIterator::on_number(number::Number *number) { - this->client_->send_number_info(number); - return true; -} -#endif - -#ifdef USE_DATETIME_DATE -bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { - this->client_->send_date_info(date); - return true; -} -#endif - -#ifdef USE_DATETIME_TIME -bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { - this->client_->send_time_info(time); - return true; -} -#endif - -#ifdef USE_DATETIME_DATETIME -bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { - this->client_->send_datetime_info(datetime); - return true; -} -#endif - -#ifdef USE_TEXT -bool ListEntitiesIterator::on_text(text::Text *text) { - this->client_->send_text_info(text); - return true; -} -#endif - -#ifdef USE_SELECT -bool ListEntitiesIterator::on_select(select::Select *select) { - this->client_->send_select_info(select); - return true; -} -#endif - -#ifdef USE_MEDIA_PLAYER -bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { - this->client_->send_media_player_info(media_player); - return true; -} -#endif -#ifdef USE_ALARM_CONTROL_PANEL -bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->client_->send_alarm_control_panel_info(a_alarm_control_panel); - return true; -} -#endif -#ifdef USE_EVENT -bool ListEntitiesIterator::on_event(event::Event *event) { - this->client_->send_event_info(event); - return true; -} -#endif -#ifdef USE_UPDATE -bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { - this->client_->send_update_info(update); - return true; -} -#endif - } // namespace api } // namespace esphome #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index e77f21c7a1..b9506073d2 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -9,75 +9,83 @@ namespace api { class APIConnection; +// Macro for generating ListEntitiesIterator handlers +// Calls schedule_message_ with try_send_*_info +#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \ + bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \ + ResponseType::MESSAGE_TYPE); \ + } + class ListEntitiesIterator : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON - bool on_button(button::Button *button) override; + bool on_button(button::Button *entity) override; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif bool on_service(UserServiceDescriptor *service) override; #ifdef USE_ESP32_CAMERA - bool on_camera(esp32_camera::ESP32Camera *camera) override; + bool on_camera(esp32_camera::ESP32Camera *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT - bool on_event(event::Event *event) override; + bool on_event(event::Event *entity) override; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; bool completed() { return this->state_ == IteratorState::NONE; } diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 4180435fcc..12accf4613 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -6,73 +6,67 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - return this->client_->send_binary_sensor_state(binary_sensor); -} +INITIAL_STATE_HANDLER(binary_sensor, binary_sensor::BinarySensor) #endif #ifdef USE_COVER -bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); } +INITIAL_STATE_HANDLER(cover, cover::Cover) #endif #ifdef USE_FAN -bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); } +INITIAL_STATE_HANDLER(fan, fan::Fan) #endif #ifdef USE_LIGHT -bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); } +INITIAL_STATE_HANDLER(light, light::LightState) #endif #ifdef USE_SENSOR -bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_state(sensor); } +INITIAL_STATE_HANDLER(sensor, sensor::Sensor) #endif #ifdef USE_SWITCH -bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_state(a_switch); } +INITIAL_STATE_HANDLER(switch, switch_::Switch) #endif #ifdef USE_TEXT_SENSOR -bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - return this->client_->send_text_sensor_state(text_sensor); -} +INITIAL_STATE_HANDLER(text_sensor, text_sensor::TextSensor) #endif #ifdef USE_CLIMATE -bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); } +INITIAL_STATE_HANDLER(climate, climate::Climate) #endif #ifdef USE_NUMBER -bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number); } +INITIAL_STATE_HANDLER(number, number::Number) #endif #ifdef USE_DATETIME_DATE -bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } +INITIAL_STATE_HANDLER(date, datetime::DateEntity) #endif #ifdef USE_DATETIME_TIME -bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } +INITIAL_STATE_HANDLER(time, datetime::TimeEntity) #endif #ifdef USE_DATETIME_DATETIME -bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) { - return this->client_->send_datetime_state(datetime); -} +INITIAL_STATE_HANDLER(datetime, datetime::DateTimeEntity) #endif #ifdef USE_TEXT -bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text); } +INITIAL_STATE_HANDLER(text, text::Text) #endif #ifdef USE_SELECT -bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select); } +INITIAL_STATE_HANDLER(select, select::Select) #endif #ifdef USE_LOCK -bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock); } +INITIAL_STATE_HANDLER(lock, lock::Lock) #endif #ifdef USE_VALVE -bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); } +INITIAL_STATE_HANDLER(valve, valve::Valve) #endif #ifdef USE_MEDIA_PLAYER -bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { - return this->client_->send_media_player_state(media_player); -} +INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer) #endif #ifdef USE_ALARM_CONTROL_PANEL -bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); -} +INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) #endif #ifdef USE_UPDATE -bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); } +INITIAL_STATE_HANDLER(update, update::UpdateEntity) #endif + +// Special cases (button and event) are already defined inline in subscribe_state.h + InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 3966c97af5..2b7b508056 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -10,71 +10,78 @@ namespace api { class APIConnection; +// Macro for generating InitialStateIterator handlers +// Calls send_*_state +#define INITIAL_STATE_HANDLER(entity_type, EntityClass) \ + bool InitialStateIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->send_##entity_type##_state(entity); \ + } + class InitialStateIterator : public ComponentIterator { public: InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON bool on_button(button::Button *button) override { return true; }; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool completed() { return this->state_ == IteratorState::NONE; } From 24bbfcdce7004b4241393b54034182a8eb6b7ab5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:42:57 -0500 Subject: [PATCH 31/40] Reduce API memory footprint through bitfield consolidation and type sizing (#9252) --- esphome/components/api/__init__.py | 7 +- esphome/components/api/api_connection.cpp | 51 ++++++----- esphome/components/api/api_connection.h | 104 ++++++++++++---------- esphome/components/api/api_server.cpp | 10 +-- esphome/components/api/api_server.h | 8 +- 5 files changed, 96 insertions(+), 84 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index bd131ef8de..501b707678 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -110,9 +110,10 @@ CONFIG_SCHEMA = cv.All( ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Optional(CONF_ENCRYPTION): _encryption_schema, - cv.Optional( - CONF_BATCH_DELAY, default="100ms" - ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(milliseconds=65535)), + ), cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8550d45bfc..e5847e50f7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -93,21 +93,21 @@ APIConnection::~APIConnection() { #ifdef HAS_PROTO_MESSAGE_DUMP void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) { // Set log-only mode - this->log_only_mode_ = true; + this->flags_.log_only_mode = true; // Call the creator - it will create the message and log it via encode_message_to_buffer item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); // Clear log-only mode - this->log_only_mode_ = false; + this->flags_.log_only_mode = false; } #endif void APIConnection::loop() { - if (this->next_close_) { + if (this->flags_.next_close) { // requested a disconnect this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; return; } @@ -148,15 +148,14 @@ void APIConnection::loop() { } else { this->read_message(0, buffer.type, nullptr); } - if (this->remove_) + if (this->flags_.remove) return; } } } // Process deferred batch if scheduled - if (this->deferred_batch_.batch_scheduled && - now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { + if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { this->process_batch_(); } @@ -166,7 +165,7 @@ void APIConnection::loop() { this->initial_state_iterator_.advance(); } - if (this->sent_ping_) { + if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); @@ -174,13 +173,13 @@ void APIConnection::loop() { } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { ESP_LOGVV(TAG, "Sending keepalive PING"); - this->sent_ping_ = this->send_message(PingRequest()); - if (!this->sent_ping_) { + this->flags_.sent_ping = this->send_message(PingRequest()); + if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority ESP_LOGW(TAG, "Buffer full, ping queued"); this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); - this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings } } @@ -240,13 +239,13 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // don't close yet, we still need to send the disconnect response // close will happen on next loop ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); - this->next_close_ = true; + this->flags_.next_close = true; DisconnectResponse resp; return resp; } void APIConnection::on_disconnect_response(const DisconnectResponse &value) { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } // Encodes a message to the buffer and returns the total number of bytes used, @@ -255,7 +254,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes uint32_t remaining_size, bool is_single) { #ifdef HAS_PROTO_MESSAGE_DUMP // If in log-only mode, just log and return - if (conn->log_only_mode_) { + if (conn->flags_.log_only_mode) { conn->log_send_message_(msg.message_name(), msg.dump()); return 1; // Return non-zero to indicate "success" for logging } @@ -1118,7 +1117,7 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { #ifdef USE_ESP32_CAMERA void APIConnection::set_camera_state(std::shared_ptr image) { - if (!this->state_subscription_) + if (!this->flags_.state_subscription) return; if (this->image_reader_.available()) return; @@ -1459,7 +1458,7 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { #endif bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) { - if (this->log_subscription_ < level) + if (this->flags_.log_subscription < level) return false; // Pre-calculate message size to avoid reallocations @@ -1500,7 +1499,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); - this->connection_state_ = ConnectionState::CONNECTED; + this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); return resp; } ConnectResponse APIConnection::connect(const ConnectRequest &msg) { @@ -1511,7 +1510,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { resp.invalid_password = !correct; if (correct) { ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); - this->connection_state_ = ConnectionState::AUTHENTICATED; + this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1625,7 +1624,7 @@ void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistant state_subs_at_ = 0; } bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->remove_) + if (this->flags_.remove) return false; if (this->helper_->can_write_without_blocking()) return true; @@ -1675,7 +1674,7 @@ void APIConnection::on_no_setup_connection() { } void APIConnection::on_fatal_error() { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type) { @@ -1700,8 +1699,8 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre } bool APIConnection::schedule_batch_() { - if (!this->deferred_batch_.batch_scheduled) { - this->deferred_batch_.batch_scheduled = true; + if (!this->flags_.batch_scheduled) { + this->flags_.batch_scheduled = true; this->deferred_batch_.batch_start_time = App.get_loop_component_start_time(); } return true; @@ -1710,14 +1709,14 @@ bool APIConnection::schedule_batch_() { ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); } ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { - ProtoWriteBuffer result = this->prepare_message_buffer(size, this->batch_first_message_); - this->batch_first_message_ = false; + ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message); + this->flags_.batch_first_message = false; return result; } void APIConnection::process_batch_() { if (this->deferred_batch_.empty()) { - this->deferred_batch_.batch_scheduled = false; + this->flags_.batch_scheduled = false; return; } @@ -1770,7 +1769,7 @@ void APIConnection::process_batch_() { // Reserve based on estimated size (much more accurate than 24-byte worst-case) this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead); - this->batch_first_message_ = true; + this->flags_.batch_first_message = true; size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits::max(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c9f24a7759..410a9ad3a5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -107,7 +107,7 @@ class APIConnection : public APIServerConnection { #endif bool try_send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { - if (!this->service_call_subscription_) + if (!this->flags_.service_call_subscription) return; this->send_message(call); } @@ -164,7 +164,7 @@ class APIConnection : public APIServerConnection { void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping - this->sent_ping_ = false; + this->flags_.sent_ping = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; #ifdef USE_HOMEASSISTANT_TIME @@ -177,16 +177,16 @@ class APIConnection : public APIServerConnection { DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void subscribe_states(const SubscribeStatesRequest &msg) override { - this->state_subscription_ = true; + this->flags_.state_subscription = true; this->initial_state_iterator_.begin(); } void subscribe_logs(const SubscribeLogsRequest &msg) override { - this->log_subscription_ = msg.level; + this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); } void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { - this->service_call_subscription_ = true; + this->flags_.service_call_subscription = true; } void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; GetTimeResponse get_time(const GetTimeRequest &msg) override { @@ -198,9 +198,12 @@ class APIConnection : public APIServerConnection { NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; #endif - bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } + bool is_authenticated() override { + return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; + } bool is_connection_setup() override { - return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated(); + return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || + this->is_authenticated(); } void on_fatal_error() override; void on_unauthenticated_access() override; @@ -423,49 +426,28 @@ class APIConnection : public APIServerConnection { static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - // Pointers first (4 bytes each, naturally aligned) + // === Optimal member ordering for 32-bit systems === + + // Group 1: Pointers (4 bytes each on 32-bit) std::unique_ptr helper_; APIServer *parent_; - // 4-byte aligned types - uint32_t last_traffic_; - int state_subs_at_ = -1; - - // Strings (12 bytes each on 32-bit) - std::string client_info_; - std::string client_peername_; - - // 2-byte aligned types - uint16_t client_api_version_major_{0}; - uint16_t client_api_version_minor_{0}; - - // Group all 1-byte types together to minimize padding - enum class ConnectionState : uint8_t { - WAITING_FOR_HELLO, - CONNECTED, - AUTHENTICATED, - } connection_state_{ConnectionState::WAITING_FOR_HELLO}; - uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE}; - bool remove_{false}; - bool state_subscription_{false}; - bool sent_ping_{false}; - bool service_call_subscription_{false}; - bool next_close_ = false; - // 7 bytes used, 1 byte padding -#ifdef HAS_PROTO_MESSAGE_DUMP - // When true, encode_message_to_buffer will only log, not encode - bool log_only_mode_{false}; -#endif - uint8_t ping_retries_{0}; - // 8 bytes used, no padding needed - - // Larger objects at the end + // Group 2: Larger objects (must be 4-byte aligned) + // These contain vectors/pointers internally, so putting them early ensures good alignment InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; #ifdef USE_ESP32_CAMERA esp32_camera::CameraImageReader image_reader_; #endif + // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) + std::string client_info_; + std::string client_peername_; + + // Group 4: 4-byte types + uint32_t last_traffic_; + int state_subs_at_ = -1; + // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); @@ -575,7 +557,6 @@ class APIConnection : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - bool batch_scheduled{false}; DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation @@ -588,13 +569,47 @@ class APIConnection : public APIServerConnection { void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); void clear() { items.clear(); - batch_scheduled = false; batch_start_time = 0; } bool empty() const { return items.empty(); } }; + // DeferredBatch here (16 bytes, 4-byte aligned) DeferredBatch deferred_batch_; + + // ConnectionState enum for type safety + enum class ConnectionState : uint8_t { + WAITING_FOR_HELLO = 0, + CONNECTED = 1, + AUTHENTICATED = 2, + }; + + // Group 5: Pack all small members together to minimize padding + // This group starts at a 4-byte boundary after DeferredBatch + struct APIFlags { + // Connection state only needs 2 bits (3 states) + uint8_t connection_state : 2; + // Log subscription needs 3 bits (log levels 0-7) + uint8_t log_subscription : 3; + // Boolean flags (1 bit each) + uint8_t remove : 1; + uint8_t state_subscription : 1; + uint8_t sent_ping : 1; + + uint8_t service_call_subscription : 1; + uint8_t next_close : 1; + uint8_t batch_scheduled : 1; + uint8_t batch_first_message : 1; // For batch buffer allocation +#ifdef HAS_PROTO_MESSAGE_DUMP + uint8_t log_only_mode : 1; +#endif + } flags_{}; // 2 bytes total + + // 2-byte types immediately after flags_ (no padding between them) + uint16_t client_api_version_major_{0}; + uint16_t client_api_version_minor_{0}; + // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary + uint32_t get_batch_delay_ms_() const; // Message will use 8 more bytes than the minimum size, and typical // MTU is 1500. Sometimes users will see as low as 1460 MTU. @@ -612,9 +627,6 @@ class APIConnection : public APIServerConnection { bool schedule_batch_(); void process_batch_(); - // State for batch buffer allocation - bool batch_first_message_{false}; - #ifdef HAS_PROTO_MESSAGE_DUMP void log_batch_item_(const DeferredBatch::BatchItem &item); #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a33623b15a..b17faf7607 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -104,7 +104,7 @@ void APIServer::setup() { return; } for (auto &c : this->clients_) { - if (!c->remove_) + if (!c->flags_.remove) c->try_send_log_message(level, tag, message); } }); @@ -116,7 +116,7 @@ void APIServer::setup() { esp32_camera::global_esp32_camera->add_image_callback( [this](const std::shared_ptr &image) { for (auto &c : this->clients_) { - if (!c->remove_) + if (!c->flags_.remove) c->set_camera_state(image); } }); @@ -176,7 +176,7 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; - if (!client->remove_) { + if (!client->flags_.remove) { // Common case: process active client client->loop(); client_index++; @@ -431,7 +431,7 @@ void APIServer::set_port(uint16_t port) { this->port_ = port; } void APIServer::set_password(const std::string &password) { this->password_ = password; } -void APIServer::set_batch_delay(uint32_t batch_delay) { this->batch_delay_ = batch_delay; } +void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { for (auto &client : this->clients_) { @@ -502,7 +502,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->remove_ && client->is_authenticated()) + if (!client->flags_.remove && client->is_authenticated()) client->send_time_request(); } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 27341dc596..85c1260448 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -40,8 +40,8 @@ class APIServer : public Component, public Controller { void set_port(uint16_t port); void set_password(const std::string &password); void set_reboot_timeout(uint32_t reboot_timeout); - void set_batch_delay(uint32_t batch_delay); - uint32_t get_batch_delay() const { return batch_delay_; } + void set_batch_delay(uint16_t batch_delay); + uint16_t get_batch_delay() const { return batch_delay_; } // Get reference to shared buffer for API connections std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } @@ -150,7 +150,6 @@ class APIServer : public Component, public Controller { // 4-byte aligned types uint32_t reboot_timeout_{300000}; - uint32_t batch_delay_{100}; // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; @@ -161,8 +160,9 @@ class APIServer : public Component, public Controller { // Group smaller types together uint16_t port_{6053}; + uint16_t batch_delay_{100}; bool shutting_down_ = false; - // 3 bytes used, 1 byte padding + // 5 bytes used, 3 bytes padding #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); From a4b57c7e44415e425d90c698c71093d3b81e5d78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:43:47 -0500 Subject: [PATCH 32/40] Reduce flash usage by making add_message_object non-template (#9258) --- esphome/components/api/api_pb2_size.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_pb2_size.h b/esphome/components/api/api_pb2_size.h index e591a7350f..f371be13a5 100644 --- a/esphome/components/api/api_pb2_size.h +++ b/esphome/components/api/api_pb2_size.h @@ -316,15 +316,13 @@ class ProtoSize { /** * @brief Calculates and adds the size of a nested message field to the total message size * - * This templated version directly takes a message object, calculates its size internally, + * This version takes a ProtoMessage object, calculates its size internally, * and updates the total_size reference. This eliminates the need for a temporary variable * at the call site. * - * @tparam MessageType The type of the nested message (inferred from parameter) * @param message The nested message object */ - template - static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message, + static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message, bool force = false) { uint32_t nested_size = 0; message.calculate_size(nested_size); From e907050a173011e1eb071963cbe962e907b8ec89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:45:03 -0500 Subject: [PATCH 33/40] Remove unused return value from read_message and fix ifdef placement in generated API code (#9256) --- esphome/components/api/api_pb2_service.cpp | 161 ++++++++++----------- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 30 ++-- 4 files changed, 97 insertions(+), 98 deletions(-) diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 03017fdfff..de8e6574b2 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -14,7 +14,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { HelloRequest msg; @@ -106,50 +106,50 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_logs_request(msg); break; } - case 30: { #ifdef USE_COVER + case 30: { CoverCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str()); #endif this->on_cover_command_request(msg); -#endif break; } - case 31: { +#endif #ifdef USE_FAN + case 31: { FanCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str()); #endif this->on_fan_command_request(msg); -#endif break; } - case 32: { +#endif #ifdef USE_LIGHT + case 32: { LightCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str()); #endif this->on_light_command_request(msg); -#endif break; } - case 33: { +#endif #ifdef USE_SWITCH + case 33: { SwitchCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str()); #endif this->on_switch_command_request(msg); -#endif break; } +#endif case 34: { SubscribeHomeassistantServicesRequest msg; msg.decode(msg_data, msg_size); @@ -204,395 +204,394 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_execute_service_request(msg); break; } - case 45: { #ifdef USE_ESP32_CAMERA + case 45: { CameraImageRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str()); #endif this->on_camera_image_request(msg); -#endif break; } - case 48: { +#endif #ifdef USE_CLIMATE + case 48: { ClimateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); #endif this->on_climate_command_request(msg); -#endif break; } - case 51: { +#endif #ifdef USE_NUMBER + case 51: { NumberCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); #endif this->on_number_command_request(msg); -#endif break; } - case 54: { +#endif #ifdef USE_SELECT + case 54: { SelectCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); #endif this->on_select_command_request(msg); -#endif break; } - case 57: { +#endif #ifdef USE_SIREN + case 57: { SirenCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str()); #endif this->on_siren_command_request(msg); -#endif break; } - case 60: { +#endif #ifdef USE_LOCK + case 60: { LockCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str()); #endif this->on_lock_command_request(msg); -#endif break; } - case 62: { +#endif #ifdef USE_BUTTON + case 62: { ButtonCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); #endif this->on_button_command_request(msg); -#endif break; } - case 65: { +#endif #ifdef USE_MEDIA_PLAYER + case 65: { MediaPlayerCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); #endif this->on_media_player_command_request(msg); -#endif break; } - case 66: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 66: { SubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 68: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 68: { BluetoothDeviceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_device_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_device_request(msg); -#endif break; } - case 70: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 70: { BluetoothGATTGetServicesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_get_services_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_get_services_request(msg); -#endif break; } - case 73: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 73: { BluetoothGATTReadRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_request(msg); -#endif break; } - case 75: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 75: { BluetoothGATTWriteRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_request(msg); -#endif break; } - case 76: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 76: { BluetoothGATTReadDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_descriptor_request(msg); -#endif break; } - case 77: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 77: { BluetoothGATTWriteDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_descriptor_request(msg); -#endif break; } - case 78: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 78: { BluetoothGATTNotifyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_notify_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_notify_request(msg); -#endif break; } - case 80: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 80: { SubscribeBluetoothConnectionsFreeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_connections_free_request(msg); -#endif break; } - case 87: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 87: { UnsubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_unsubscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 89: { +#endif #ifdef USE_VOICE_ASSISTANT + case 89: { SubscribeVoiceAssistantRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_voice_assistant_request: %s", msg.dump().c_str()); #endif this->on_subscribe_voice_assistant_request(msg); -#endif break; } - case 91: { +#endif #ifdef USE_VOICE_ASSISTANT + case 91: { VoiceAssistantResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_response(msg); -#endif break; } - case 92: { +#endif #ifdef USE_VOICE_ASSISTANT + case 92: { VoiceAssistantEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_event_response(msg); -#endif break; } - case 96: { +#endif #ifdef USE_ALARM_CONTROL_PANEL + case 96: { AlarmControlPanelCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); #endif this->on_alarm_control_panel_command_request(msg); -#endif break; } - case 99: { +#endif #ifdef USE_TEXT + case 99: { TextCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str()); #endif this->on_text_command_request(msg); -#endif break; } - case 102: { +#endif #ifdef USE_DATETIME_DATE + case 102: { DateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str()); #endif this->on_date_command_request(msg); -#endif break; } - case 105: { +#endif #ifdef USE_DATETIME_TIME + case 105: { TimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str()); #endif this->on_time_command_request(msg); -#endif break; } - case 106: { +#endif #ifdef USE_VOICE_ASSISTANT + case 106: { VoiceAssistantAudio msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_audio: %s", msg.dump().c_str()); #endif this->on_voice_assistant_audio(msg); -#endif break; } - case 111: { +#endif #ifdef USE_VALVE + case 111: { ValveCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str()); #endif this->on_valve_command_request(msg); -#endif break; } - case 114: { +#endif #ifdef USE_DATETIME_DATETIME + case 114: { DateTimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str()); #endif this->on_date_time_command_request(msg); -#endif break; } - case 115: { +#endif #ifdef USE_VOICE_ASSISTANT + case 115: { VoiceAssistantTimerEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_timer_event_response(msg); -#endif break; } - case 118: { +#endif #ifdef USE_UPDATE + case 118: { UpdateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); #endif this->on_update_command_request(msg); -#endif break; } - case 119: { +#endif #ifdef USE_VOICE_ASSISTANT + case 119: { VoiceAssistantAnnounceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_announce_request(msg); -#endif break; } - case 121: { +#endif #ifdef USE_VOICE_ASSISTANT + case 121: { VoiceAssistantConfigurationRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_configuration_request(msg); -#endif break; } - case 123: { +#endif #ifdef USE_VOICE_ASSISTANT + case 123: { VoiceAssistantSetConfiguration msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); #endif this->on_voice_assistant_set_configuration(msg); -#endif break; } - case 124: { +#endif #ifdef USE_API_NOISE + case 124: { NoiseEncryptionSetKeyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str()); #endif this->on_noise_encryption_set_key_request(msg); -#endif break; } - case 127: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 127: { BluetoothScannerSetModeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_scanner_set_mode_request(msg); -#endif break; } +#endif default: - return false; + break; } - return true; } void APIServerConnection::on_hello_request(const HelloRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 047c56198a..3cc774f91c 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -199,7 +199,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_update_command_request(const UpdateCommandRequest &value){}; #endif protected: - bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d9c9e3c85d..764bac2f39 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -364,7 +364,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0; - virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint16_t message_type) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 419b5aa97d..ad8e41ba5e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1034,7 +1034,7 @@ SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 -RECEIVE_CASES: dict[int, str] = {} +RECEIVE_CASES: dict[int, tuple[str, str | None]] = {} ifdefs: dict[str, str] = {} @@ -1208,8 +1208,6 @@ def build_service_message_type( func = f"on_{snake}" hout += f"virtual void {func}(const {mt.name} &value){{}};\n" case = "" - if ifdef is not None: - case += f"#ifdef {ifdef}\n" case += f"{mt.name} msg;\n" case += "msg.decode(msg_data, msg_size);\n" if log: @@ -1217,10 +1215,9 @@ def build_service_message_type( case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' case += "#endif\n" case += f"this->{func}(msg);\n" - if ifdef is not None: - case += "#endif\n" case += "break;" - RECEIVE_CASES[id_] = case + # Store the ifdef with the case for later use + RECEIVE_CASES[id_] = (case, ifdef) # Only close ifdef if we opened it if ifdef is not None: @@ -1379,18 +1376,21 @@ def main() -> None: cases = list(RECEIVE_CASES.items()) cases.sort() hpp += " protected:\n" - hpp += " bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" - out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" - for i, case in cases: - c = f"case {i}: {{\n" - c += indent(case) + "\n" - c += "}" - out += indent(c, " ") + "\n" + for i, (case, ifdef) in cases: + if ifdef is not None: + out += f"#ifdef {ifdef}\n" + c = f" case {i}: {{\n" + c += indent(case, " ") + "\n" + c += " }" + out += c + "\n" + if ifdef is not None: + out += "#endif\n" out += " default:\n" - out += " return false;\n" + out += " break;\n" out += " }\n" - out += " return true;\n" out += "}\n" cpp += out hpp += "};\n" From 687cb1cd2bcd2ef03f7d50ff1c16243d44def214 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:47:20 -0500 Subject: [PATCH 34/40] Reduce web_server RAM usage by 96 bytes with conditional sorting compilation (#9227) --- esphome/components/web_server/__init__.py | 2 + esphome/components/web_server/web_server.cpp | 155 ++++-------------- esphome/components/web_server/web_server.h | 6 + .../web_server_idf/web_server_idf.cpp | 2 + esphome/core/defines.h | 1 + 5 files changed, 46 insertions(+), 120 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index d846a3418b..8ff7ce1d16 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -211,6 +211,7 @@ async def add_entity_config(entity, config): sorting_weight = config.get(CONF_SORTING_WEIGHT, 50) sorting_group_hash = hash(config.get(CONF_SORTING_GROUP_ID)) + cg.add_define("USE_WEBSERVER_SORTING") cg.add( web_server.add_entity_config( entity, @@ -296,4 +297,5 @@ async def to_code(config): cg.add_define("USE_WEBSERVER_LOCAL") if (sorting_group_config := config.get(CONF_SORTING_GROUPS)) is not None: + cg.add_define("USE_WEBSERVER_SORTING") add_sorting_groups(var, sorting_group_config) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 927659e621..1bf3ed11cb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -184,6 +184,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp std::string message = ws->get_config_json(); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +#ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; @@ -193,6 +194,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp // up to 31 groups should be able to be queued initially without defer source->try_send_nodefer(message.c_str(), "sorting_group"); } +#endif source->entities_iterator_.begin(ws->include_internal_); @@ -413,12 +415,7 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail } set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); if (!obj->get_unit_of_measurement().empty()) root["uom"] = obj->get_unit_of_measurement(); } @@ -458,12 +455,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -511,12 +503,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -552,12 +539,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -595,12 +577,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -681,12 +658,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -802,12 +774,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -888,12 +855,7 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_tilt()) root["tilt"] = obj->tilt; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -954,12 +916,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["mode"] = (int) obj->traits.get_mode(); if (!obj->traits.get_unit_of_measurement().empty()) root["uom"] = obj->traits.get_unit_of_measurement(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } if (std::isnan(value)) { root["value"] = "\"NaN\""; @@ -1028,12 +985,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1091,12 +1043,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1155,12 +1102,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1221,12 +1163,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json root["value"] = value; if (start_config == DETAIL_ALL) { root["mode"] = (int) obj->traits.get_mode(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1282,12 +1219,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value for (auto &option : obj->traits.get_options()) { opt.add(option); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1404,12 +1336,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } bool has_state = false; @@ -1502,12 +1429,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1579,12 +1501,7 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_position()) root["position"] = obj->position; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1652,12 +1569,7 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1705,12 +1617,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty event_types.add(event_type); } root["device_class"] = obj->get_device_class(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1774,12 +1681,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c root["title"] = obj->update_info.title; root["summary"] = obj->update_info.summary; root["release_url"] = obj->update_info.release_url; - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -2089,6 +1991,18 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { bool WebServer::isRequestHandlerTrivial() const { return false; } +void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) { +#ifdef USE_WEBSERVER_SORTING + if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[entity].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name; + } + } +#endif +} + +#ifdef USE_WEBSERVER_SORTING void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { this->sorting_entitys_[entity] = SortingComponents{weight, group}; } @@ -2096,6 +2010,7 @@ void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t gro void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_name, float weight) { this->sorting_groups_[group_id] = SortingGroup{group_name, weight}; } +#endif void WebServer::schedule_(std::function &&f) { #ifdef USE_ESP32 diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 53ee4d1212..3be99eebae 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -46,6 +46,7 @@ struct UrlMatch { bool valid; ///< Whether this match is valid }; +#ifdef USE_WEBSERVER_SORTING struct SortingComponents { float weight; uint64_t group_id; @@ -55,6 +56,7 @@ struct SortingGroup { std::string name; float weight; }; +#endif enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; @@ -474,14 +476,18 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// This web handle is not trivial. bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming) +#ifdef USE_WEBSERVER_SORTING void add_entity_config(EntityBase *entity, float weight, uint64_t group); void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); std::map sorting_entitys_; std::map sorting_groups_; +#endif + bool include_internal_{false}; protected: + void add_sorting_info_(JsonObject &root, EntityBase *entity); void schedule_(std::function &&f); web_server_base::WebServerBase *base_; #ifdef USE_ARDUINO diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 90fdf720cd..30c6b04fb2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -338,6 +338,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * std::string message = ws->get_config_json(); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +#ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; @@ -348,6 +349,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * // since the only thing in the send buffer at this point is the initial ping/config this->try_send_nodefer(message.c_str(), "sorting_group"); } +#endif this->entities_iterator_->begin(ws->include_internal_); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8abd6598f7..22454249aa 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -151,6 +151,7 @@ #define USE_VOICE_ASSISTANT #define USE_WEBSERVER #define USE_WEBSERVER_PORT 80 // NOLINT +#define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO From 2289073a1e0b8013cc6b840ea47cb0002078d94f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:47:50 -0500 Subject: [PATCH 35/40] Add interrupt support to GPIO binary sensors (#9115) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/gpio/binary_sensor/__init__.py | 17 ++++ .../gpio/binary_sensor/gpio_binary_sensor.cpp | 80 ++++++++++++++++++- .../gpio/binary_sensor/gpio_binary_sensor.h | 40 ++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 23f2781095..9f50fd779a 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -10,11 +10,24 @@ GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) +CONF_USE_INTERRUPT = "use_interrupt" +CONF_INTERRUPT_TYPE = "interrupt_type" + +INTERRUPT_TYPES = { + "RISING": gpio_ns.INTERRUPT_RISING_EDGE, + "FALLING": gpio_ns.INTERRUPT_FALLING_EDGE, + "ANY": gpio_ns.INTERRUPT_ANY_EDGE, +} + CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(GPIOBinarySensor) .extend( { cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, + cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( + INTERRUPT_TYPES, upper=True + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -27,3 +40,7 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) + + cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) + if config[CONF_USE_INTERRUPT]: + cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index cf4b088580..4b8369cd59 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -6,17 +6,91 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; +void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { + bool new_state = arg->isr_pin_.digital_read(); + if (new_state != arg->last_state_) { + arg->state_ = new_state; + arg->last_state_ = new_state; + arg->changed_ = true; + // Wake up the component from its disabled loop state + if (arg->component_ != nullptr) { + arg->component_->enable_loop_soon_any_context(); + } + } +} + +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { + pin->setup(); + this->isr_pin_ = pin->to_isr(); + this->component_ = component; + + // Read initial state + this->last_state_ = pin->digital_read(); + this->state_ = this->last_state_; + + // Attach interrupt - from this point on, any changes will be caught by the interrupt + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); +} + void GPIOBinarySensor::setup() { - this->pin_->setup(); - this->publish_initial_state(this->pin_->digital_read()); + if (this->use_interrupt_ && !this->pin_->is_internal()) { + ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); + this->use_interrupt_ = false; + } + + if (this->use_interrupt_) { + auto *internal_pin = static_cast(this->pin_); + this->store_.setup(internal_pin, this->interrupt_type_, this); + this->publish_initial_state(this->store_.get_state()); + } else { + this->pin_->setup(); + this->publish_initial_state(this->pin_->digital_read()); + } } void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); + const char *mode = this->use_interrupt_ ? "interrupt" : "polling"; + ESP_LOGCONFIG(TAG, " Mode: %s", mode); + if (this->use_interrupt_) { + const char *interrupt_type; + switch (this->interrupt_type_) { + case gpio::INTERRUPT_RISING_EDGE: + interrupt_type = "RISING_EDGE"; + break; + case gpio::INTERRUPT_FALLING_EDGE: + interrupt_type = "FALLING_EDGE"; + break; + case gpio::INTERRUPT_ANY_EDGE: + interrupt_type = "ANY_EDGE"; + break; + default: + interrupt_type = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type); + } } -void GPIOBinarySensor::loop() { this->publish_state(this->pin_->digital_read()); } +void GPIOBinarySensor::loop() { + if (this->use_interrupt_) { + if (this->store_.is_changed()) { + // Clear the flag immediately to minimize the window where we might miss changes + this->store_.clear_changed(); + // Read the state and publish it + // Note: If the ISR fires between clear_changed() and get_state(), that's fine - + // we'll process the new change on the next loop iteration + bool state = this->store_.get_state(); + this->publish_state(state); + } else { + // No changes, disable the loop until the next interrupt + this->disable_loop(); + } + } else { + this->publish_state(this->pin_->digital_read()); + } +} float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 33a173fe2e..8cf52f540b 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -2,14 +2,51 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { namespace gpio { +// Store class for ISR data (no vtables, ISR-safe) +class GPIOBinarySensorStore { + public: + void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + + static void gpio_intr(GPIOBinarySensorStore *arg); + + bool get_state() const { + // No lock needed: state_ is atomically updated by ISR + // Volatile ensures we read the latest value + return this->state_; + } + + bool is_changed() const { + // Simple read of volatile bool - no clearing here + return this->changed_; + } + + void clear_changed() { + // Separate method to clear the flag + this->changed_ = false; + } + + protected: + ISRInternalGPIOPin isr_pin_; + volatile bool state_{false}; + volatile bool last_state_{false}; + volatile bool changed_{false}; + Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() +}; + class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { public: + // No destructor needed: ESPHome components are created at boot and live forever. + // Interrupts are only detached on reboot when memory is cleared anyway. + void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -22,6 +59,9 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { protected: GPIOPin *pin_; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; + GPIOBinarySensorStore store_; }; } // namespace gpio From 53e9ffe656c920218ff858c3072c9f1df962a562 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:48:19 +1200 Subject: [PATCH 36/40] [pi4ioe5v6408] Add new IO Expander (#8888) Co-authored-by: Keith Burzinski --- CODEOWNERS | 1 + esphome/components/pi4ioe5v6408/__init__.py | 84 +++++++++ .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 171 ++++++++++++++++++ .../components/pi4ioe5v6408/pi4ioe5v6408.h | 70 +++++++ tests/components/pi4ioe5v6408/common.yaml | 22 +++ .../pi4ioe5v6408/test.esp32-ard.yaml | 5 + .../pi4ioe5v6408/test.esp32-idf.yaml | 5 + .../pi4ioe5v6408/test.rp2040-ard.yaml | 5 + 8 files changed, 363 insertions(+) create mode 100644 esphome/components/pi4ioe5v6408/__init__.py create mode 100644 esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp create mode 100644 esphome/components/pi4ioe5v6408/pi4ioe5v6408.h create mode 100644 tests/components/pi4ioe5v6408/common.yaml create mode 100644 tests/components/pi4ioe5v6408/test.esp32-ard.yaml create mode 100644 tests/components/pi4ioe5v6408/test.esp32-idf.yaml create mode 100644 tests/components/pi4ioe5v6408/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 832c571ae4..b3c66c775b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,6 +332,7 @@ esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman +esphome/components/pi4ioe5v6408/* @jesserockz esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py new file mode 100644 index 0000000000..c64f923823 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -0,0 +1,84 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, + CONF_RESET, +) + +AUTO_LOAD = ["gpio_expander"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + + +pi4ioe5v6408_ns = cg.esphome_ns.namespace("pi4ioe5v6408") +PI4IOE5V6408Component = pi4ioe5v6408_ns.class_( + "PI4IOE5V6408Component", cg.Component, i2c.I2CDevice +) +PI4IOE5V6408GPIOPin = pi4ioe5v6408_ns.class_("PI4IOE5V6408GPIOPin", cg.GPIOPin) + +CONF_PI4IOE5V6408 = "pi4ioe5v6408" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), + cv.Optional(CONF_RESET, default=True): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x43)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_reset(config[CONF_RESET])) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +PI4IOE5V6408_PIN_SCHEMA = pins.gpio_base_schema( + PI4IOE5V6408GPIOPin, + cv.int_range(min=0, max=7), + modes=[ + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, + CONF_PULLDOWN, + ], + mode_validator=validate_mode, +).extend( + { + cv.Required(CONF_PI4IOE5V6408): cv.use_id(PI4IOE5V6408Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_PI4IOE5V6408, PI4IOE5V6408_PIN_SCHEMA) +async def pi4ioe5v6408_pin_schema(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_PI4IOE5V6408]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp new file mode 100644 index 0000000000..55b8edffc8 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -0,0 +1,171 @@ +#include "pi4ioe5v6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pi4ioe5v6408 { + +static const uint8_t PI4IOE5V6408_REGISTER_DEVICE_ID = 0x01; +static const uint8_t PI4IOE5V6408_REGISTER_IO_DIR = 0x03; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_SET = 0x05; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE = 0x07; +static const uint8_t PI4IOE5V6408_REGISTER_IN_DEFAULT_STATE = 0x09; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_ENABLE = 0x0B; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_SELECT = 0x0D; +static const uint8_t PI4IOE5V6408_REGISTER_IN_STATE = 0x0F; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK = 0x11; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_STATUS = 0x13; + +static const char *const TAG = "pi4ioe5v6408"; + +void PI4IOE5V6408Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + if (this->reset_) { + this->reg(PI4IOE5V6408_REGISTER_DEVICE_ID) |= 0b00000001; + this->reg(PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE) = 0b00000000; + } else { + if (!this->read_gpio_modes_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO modes"); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO outputs"); + return; + } + } +} +void PI4IOE5V6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} +void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_INPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + if (flags & gpio::FLAG_PULLUP) { + this->pull_up_down_mask_ |= 1 << pin; + this->pull_enable_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_PULLDOWN) { + this->pull_up_down_mask_ &= ~(1 << pin); + this->pull_enable_mask_ |= 1 << pin; + } + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} + +void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } + +bool PI4IOE5V6408Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { + this->status_set_warning("Failed to read output register"); + return false; + } + this->output_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { + this->status_set_warning("Failed to read GPIO modes"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Read GPIO modes: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(data)); +#endif + this->mode_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { + this->status_set_warning("Failed to read GPIO state"); + return false; + } + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) + return; + + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { + this->status_set_warning("Failed to write output register"); + return; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Wrote GPIO output: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->output_mask_)); +#endif + this->status_clear_warning(); +} + +bool PI4IOE5V6408Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + + if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { + this->status_set_warning("Failed to write GPIO modes"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { + this->status_set_warning("Failed to write GPIO pullup/pulldown"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { + this->status_set_warning("Failed to write GPIO pull enable"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, + "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), + BYTE_TO_BINARY(this->pull_enable_mask_)); +#endif + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); } + +float PI4IOE5V6408Component::get_setup_priority() const { return setup_priority::IO; } + +void PI4IOE5V6408GPIOPin::setup() { this->pin_mode(this->flags_); } +void PI4IOE5V6408GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PI4IOE5V6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} +std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); } + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h new file mode 100644 index 0000000000..82b3076fab --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pi4ioe5v6408 { +class PI4IOE5V6408Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + PI4IOE5V6408Component() = default; + + void setup() override; + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + void dump_config() override; + void loop() override; + + /// Indicate if the component should reset the state during setup + void set_reset(bool reset) { this->reset_ = reset; } + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + + /// Mask for the pin mode - 1 means output, 0 means input + uint8_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint8_t input_mask_{0x00}; + /// The mask to write as input buffer state - 1 means enabled, 0 means disabled + uint8_t pull_enable_mask_{0x00}; + /// The mask to write as pullup state - 1 means pullup, 0 means pulldown + uint8_t pull_up_down_mask_{0x00}; + + bool reset_{true}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +class PI4IOE5V6408GPIOPin : public GPIOPin, public Parented { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + gpio::Flags get_flags() const override { return this->flags_; } + + protected: + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml new file mode 100644 index 0000000000..4130dc2652 --- /dev/null +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -0,0 +1,22 @@ +i2c: + id: i2c_pi4ioe5v6408 + sda: ${i2c_sda} + scl: ${i2c_scl} + +pi4ioe5v6408: + id: pi4ioe1 + address: 0x44 + +switch: + - platform: gpio + id: switch1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 0 + +binary_sensor: + - platform: gpio + id: sensor1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 1 diff --git a/tests/components/pi4ioe5v6408/test.esp32-ard.yaml b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b7b6b13bfe --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO4 + i2c_scl: GPIO5 + +<<: !include common.yaml From 0b1b8f05e11e2ec11218f80384b20edabad022db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:49:31 -0500 Subject: [PATCH 37/40] Reduce loop enable/disable log spam by using very verbose level (#9267) --- esphome/core/application.cpp | 2 +- esphome/core/component.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 328de00640..1599c648e7 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -376,7 +376,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; - ESP_LOGD(TAG, "%s loop enabled from ISR", component->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled from ISR", component->get_component_source()); component->component_state_ &= ~COMPONENT_STATE_MASK; component->component_state_ |= COMPONENT_STATE_LOOP; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 625a7b2125..8fa63de84e 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -149,7 +149,7 @@ void Component::mark_failed() { } void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_LOOP_DONE; App.disable_component_loop_(this); @@ -157,7 +157,7 @@ void Component::disable_loop() { } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - ESP_LOGD(TAG, "%s loop enabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_LOOP; App.enable_component_loop_(this); From 7f8dd4b2540b406e51150e750d8d92abf4843435 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 19:19:18 -0500 Subject: [PATCH 38/40] Fix thread-safe cleanup of event source connections in ESP-IDF web server (#9268) --- .../web_server_idf/web_server_idf.cpp | 39 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 3 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 30c6b04fb2..409230806c 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -292,21 +292,38 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { } void AsyncEventSource::loop() { - for (auto *ses : this->sessions_) { - ses->loop(); + // Clean up dead sessions safely + // This follows the ESP-IDF pattern where free_ctx marks resources as dead + // and the main loop handles the actual cleanup to avoid race conditions + auto it = this->sessions_.begin(); + while (it != this->sessions_.end()) { + auto *ses = *it; + // If the session has a dead socket (marked by destroy callback) + if (ses->fd_.load() == 0) { + ESP_LOGD(TAG, "Removing dead event source session"); + it = this->sessions_.erase(it); + delete ses; // NOLINT(cppcoreguidelines-owning-memory) + } else { + ses->loop(); + ++it; + } } } void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { for (auto *ses : this->sessions_) { - ses->try_send_nodefer(message, event, id, reconnect); + if (ses->fd_.load() != 0) { // Skip dead sessions + ses->try_send_nodefer(message, event, id, reconnect); + } } } void AsyncEventSource::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { for (auto *ses : this->sessions_) { - ses->deferrable_send_state(source, event_type, message_generator); + if (ses->fd_.load() != 0) { // Skip dead sessions + ses->deferrable_send_state(source, event_type, message_generator); + } } } @@ -331,7 +348,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * req->free_ctx = AsyncEventSourceResponse::destroy; this->hd_ = req->handle; - this->fd_ = httpd_req_to_sockfd(req); + this->fd_.store(httpd_req_to_sockfd(req)); // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect @@ -362,8 +379,10 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - rsp->server_->sessions_.erase(rsp); - delete rsp; // NOLINT(cppcoreguidelines-owning-memory) + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); + // Mark as dead by setting fd to 0 - will be cleaned up in the main loop + rsp->fd_.store(0); + // Note: We don't delete or remove from set here to avoid race conditions } // helper for allowing only unique entries in the queue @@ -403,9 +422,11 @@ void AsyncEventSourceResponse::process_buffer_() { return; } - int bytes_sent = httpd_socket_send(this->hd_, this->fd_, event_buffer_.c_str() + event_bytes_sent_, + int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, event_buffer_.size() - event_bytes_sent_, 0); if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { + // Socket error - just return, the connection will be closed by httpd + // and our destroy callback will be called return; } event_bytes_sent_ += bytes_sent; @@ -425,7 +446,7 @@ void AsyncEventSourceResponse::loop() { bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { - if (this->fd_ == 0) { + if (this->fd_.load() == 0) { return false; } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 8dafdf11ef..7547117224 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include +#include #include #include #include @@ -271,7 +272,7 @@ class AsyncEventSourceResponse { static void destroy(void *p); AsyncEventSource *server_; httpd_handle_t hd_{}; - int fd_{}; + std::atomic fd_{}; std::vector deferred_queue_; esphome::web_server::WebServer *web_server_; std::unique_ptr entities_iterator_; From 6a354d7c946d5f81e0efd355e38d4a4267f39152 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 22:33:35 -0500 Subject: [PATCH 39/40] Reduce API component memory usage with conditional compilation (#9262) --- esphome/components/api/__init__.py | 34 +-- esphome/components/api/api_connection.cpp | 2 + esphome/components/api/api_server.cpp | 2 + esphome/components/api/api_server.h | 38 +++- esphome/core/defines.h | 3 + .../fixtures/api_conditional_memory.yaml | 71 ++++++ .../test_api_conditional_memory.py | 205 ++++++++++++++++++ 7 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 tests/integration/fixtures/api_conditional_memory.yaml create mode 100644 tests/integration/test_api_conditional_memory.py diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 501b707678..ae83129c21 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -136,23 +136,26 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) - for conf in config.get(CONF_ACTIONS, []): - template_args = [] - func_args = [] - service_arg_names = [] - for name, var_ in conf[CONF_VARIABLES].items(): - native = SERVICE_ARG_NATIVE_TYPES[var_] - template_args.append(native) - func_args.append((native, name)) - service_arg_names.append(name) - templ = cg.TemplateArguments(*template_args) - trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names - ) - cg.add(var.register_user_service(trigger)) - await automation.build_automation(trigger, func_args, conf) + if actions := config.get(CONF_ACTIONS, []): + cg.add_define("USE_API_YAML_SERVICES") + for conf in actions: + template_args = [] + func_args = [] + service_arg_names = [] + for name, var_ in conf[CONF_VARIABLES].items(): + native = SERVICE_ARG_NATIVE_TYPES[var_] + template_args.append(native) + func_args.append((native, name)) + service_arg_names.append(name) + templ = cg.TemplateArguments(*template_args) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names + ) + cg.add(var.register_user_service(trigger)) + await automation.build_automation(trigger, func_args, conf) if CONF_ON_CLIENT_CONNECTED in config: + cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") await automation.build_automation( var.get_client_connected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], @@ -160,6 +163,7 @@ async def to_code(config): ) if CONF_ON_CLIENT_DISCONNECTED in config: + cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER") await automation.build_automation( var.get_client_disconnected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e5847e50f7..6a40f21f99 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1511,7 +1511,9 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { if (correct) { ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); +#endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { this->send_time_request(); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index b17faf7607..ebe80604dc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -184,7 +184,9 @@ void APIServer::loop() { } // Rare case: handle disconnection +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); +#endif ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); // Swap with the last element and pop (avoids expensive vector shifts) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 85c1260448..5a9b0677bc 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -105,7 +105,18 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); - void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } + void register_user_service(UserServiceDescriptor *descriptor) { +#ifdef USE_API_YAML_SERVICES + // Vector is pre-allocated when services are defined in YAML + this->user_services_.push_back(descriptor); +#else + // Lazy allocate vector on first use for CustomAPIDevice + if (!this->user_services_) { + this->user_services_ = std::make_unique>(); + } + this->user_services_->push_back(descriptor); +#endif + } #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif @@ -134,19 +145,34 @@ class APIServer : public Component, public Controller { void get_home_assistant_state(std::string entity_id, optional attribute, std::function f); const std::vector &get_state_subs() const; - const std::vector &get_user_services() const { return this->user_services_; } + const std::vector &get_user_services() const { +#ifdef USE_API_YAML_SERVICES + return this->user_services_; +#else + static const std::vector EMPTY; + return this->user_services_ ? *this->user_services_ : EMPTY; +#endif + } +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER Trigger *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } +#endif protected: void schedule_reboot_timeout_(); // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *client_connected_trigger_ = new Trigger(); +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER Trigger *client_disconnected_trigger_ = new Trigger(); +#endif // 4-byte aligned types uint32_t reboot_timeout_{300000}; @@ -156,7 +182,15 @@ class APIServer : public Component, public Controller { std::string password_; std::vector shared_write_buffer_; // Shared proto write buffer for all connections std::vector state_subs_; +#ifdef USE_API_YAML_SERVICES + // When services are defined in YAML, we know at compile time that services will be registered std::vector user_services_; +#else + // Services can still be registered at runtime by CustomAPIDevice components even when not + // defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common + // case where no services (YAML or custom) are used. + std::unique_ptr> user_services_; +#endif // Group smaller types together uint16_t port_{6053}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 22454249aa..ea3c8bdc17 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -101,8 +101,11 @@ #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT #define USE_API +#define USE_API_CLIENT_CONNECTED_TRIGGER +#define USE_API_CLIENT_DISCONNECTED_TRIGGER #define USE_API_NOISE #define USE_API_PLAINTEXT +#define USE_API_YAML_SERVICES #define USE_MD5 #define USE_MQTT #define USE_NETWORK diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml new file mode 100644 index 0000000000..4bbba5084b --- /dev/null +++ b/tests/integration/fixtures/api_conditional_memory.yaml @@ -0,0 +1,71 @@ +esphome: + name: api-conditional-memory-test +host: +api: + actions: + - action: test_simple_service + then: + - logger.log: "Simple service called" + - binary_sensor.template.publish: + id: service_called_sensor + state: ON + - action: test_service_with_args + variables: + arg_string: string + arg_int: int + arg_bool: bool + arg_float: float + then: + - logger.log: + format: "Service called with: %s, %d, %d, %.2f" + args: [arg_string.c_str(), arg_int, arg_bool, arg_float] + - sensor.template.publish: + id: service_arg_sensor + state: !lambda 'return arg_float;' + on_client_connected: + - logger.log: + format: "Client %s connected from %s" + args: [client_info.c_str(), client_address.c_str()] + - binary_sensor.template.publish: + id: client_connected + state: ON + - text_sensor.template.publish: + id: last_client_info + state: !lambda 'return client_info;' + on_client_disconnected: + - logger.log: + format: "Client %s disconnected from %s" + args: [client_info.c_str(), client_address.c_str()] + - binary_sensor.template.publish: + id: client_connected + state: OFF + - binary_sensor.template.publish: + id: client_disconnected_event + state: ON + +logger: + level: DEBUG + +binary_sensor: + - platform: template + name: "Client Connected" + id: client_connected + device_class: connectivity + - platform: template + name: "Client Disconnected Event" + id: client_disconnected_event + - platform: template + name: "Service Called" + id: service_called_sensor + +sensor: + - platform: template + name: "Service Argument Value" + id: service_arg_sensor + unit_of_measurement: "" + accuracy_decimals: 2 + +text_sensor: + - platform: template + name: "Last Client Info" + id: last_client_info diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py new file mode 100644 index 0000000000..b85e8d91af --- /dev/null +++ b/tests/integration/test_api_conditional_memory.py @@ -0,0 +1,205 @@ +"""Integration test for API conditional memory optimization with triggers and services.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ( + BinarySensorInfo, + EntityState, + SensorInfo, + TextSensorInfo, + UserService, + UserServiceArgType, +) +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_conditional_memory( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API triggers and services work correctly with conditional compilation.""" + loop = asyncio.get_running_loop() + # Keep ESPHome process running throughout the test + async with run_compiled(yaml_config): + # First connection + async with api_client_connected() as client: + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "api-conditional-memory-test" + + # List entities and services + entity_info, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our entities + client_connected: BinarySensorInfo | None = None + client_disconnected_event: BinarySensorInfo | None = None + service_called_sensor: BinarySensorInfo | None = None + service_arg_sensor: SensorInfo | None = None + last_client_info: TextSensorInfo | None = None + + for entity in entity_info: + if isinstance(entity, BinarySensorInfo): + if entity.object_id == "client_connected": + client_connected = entity + elif entity.object_id == "client_disconnected_event": + client_disconnected_event = entity + elif entity.object_id == "service_called": + service_called_sensor = entity + elif isinstance(entity, SensorInfo): + if entity.object_id == "service_argument_value": + service_arg_sensor = entity + elif isinstance(entity, TextSensorInfo): + if entity.object_id == "last_client_info": + last_client_info = entity + + # Verify all entities exist + assert client_connected is not None, "client_connected sensor not found" + assert client_disconnected_event is not None, ( + "client_disconnected_event sensor not found" + ) + assert service_called_sensor is not None, "service_called sensor not found" + assert service_arg_sensor is not None, "service_arg_sensor not found" + assert last_client_info is not None, "last_client_info sensor not found" + + # Verify services exist + assert len(services) == 2, f"Expected 2 services, found {len(services)}" + + # Find our services + simple_service: UserService | None = None + service_with_args: UserService | None = None + + for service in services: + if service.name == "test_simple_service": + simple_service = service + elif service.name == "test_service_with_args": + service_with_args = service + + assert simple_service is not None, "test_simple_service not found" + assert service_with_args is not None, "test_service_with_args not found" + + # Verify service arguments + assert len(service_with_args.args) == 4, ( + f"Expected 4 args, found {len(service_with_args.args)}" + ) + + # Check arg types + arg_types = {arg.name: arg.type for arg in service_with_args.args} + assert arg_types["arg_string"] == UserServiceArgType.STRING + assert arg_types["arg_int"] == UserServiceArgType.INT + assert arg_types["arg_bool"] == UserServiceArgType.BOOL + assert arg_types["arg_float"] == UserServiceArgType.FLOAT + + # Track state changes + states: dict[int, EntityState] = {} + states_future: asyncio.Future[None] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + # Check if we have initial states for connection sensors + if ( + client_connected.key in states + and last_client_info.key in states + and not states_future.done() + ): + states_future.set_result(None) + + client.subscribe_states(on_state) + + # Wait for initial states + await asyncio.wait_for(states_future, timeout=5.0) + + # Verify on_client_connected trigger fired + connected_state = states.get(client_connected.key) + assert connected_state is not None + assert connected_state.state is True, "Client should be connected" + + # Verify client info was captured + client_info_state = states.get(last_client_info.key) + assert client_info_state is not None + assert isinstance(client_info_state.state, str) + assert len(client_info_state.state) > 0, "Client info should not be empty" + + # Test simple service + service_future: asyncio.Future[None] = loop.create_future() + + def check_service_called(state: EntityState) -> None: + if state.key == service_called_sensor.key and state.state is True: + if not service_future.done(): + service_future.set_result(None) + + # Update callback to check for service execution + client.subscribe_states(check_service_called) + + # Call simple service + client.execute_service(simple_service, {}) + + # Wait for service to execute + await asyncio.wait_for(service_future, timeout=5.0) + + # Test service with arguments + arg_future: asyncio.Future[None] = loop.create_future() + expected_float = 42.5 + + def check_arg_sensor(state: EntityState) -> None: + if ( + state.key == service_arg_sensor.key + and abs(state.state - expected_float) < 0.01 + ): + if not arg_future.done(): + arg_future.set_result(None) + + client.subscribe_states(check_arg_sensor) + + # Call service with arguments + client.execute_service( + service_with_args, + { + "arg_string": "test_string", + "arg_int": 123, + "arg_bool": True, + "arg_float": expected_float, + }, + ) + + # Wait for service with args to execute + await asyncio.wait_for(arg_future, timeout=5.0) + + # After disconnecting first client, reconnect and verify triggers work + async with api_client_connected() as client2: + # Subscribe to states with new client + states2: dict[int, EntityState] = {} + connected_future: asyncio.Future[None] = loop.create_future() + + def on_state2(state: EntityState) -> None: + states2[state.key] = state + # Check for reconnection + if state.key == client_connected.key and state.state is True: + if not connected_future.done(): + connected_future.set_result(None) + + client2.subscribe_states(on_state2) + + # Wait for connected state + await asyncio.wait_for(connected_future, timeout=5.0) + + # Verify client is connected again (on_client_connected fired) + assert states2[client_connected.key].state is True, ( + "Client should be reconnected" + ) + + # The client_disconnected_event should be ON from when we disconnected + # (it was set ON by on_client_disconnected trigger) + disconnected_state = states2.get(client_disconnected_event.key) + assert disconnected_state is not None + assert disconnected_state.state is True, ( + "Disconnect event should be ON from previous disconnect" + ) From 140ca070a20c9e1b3d3d380f467c0553c2f8a9b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 22:40:36 -0500 Subject: [PATCH 40/40] Optimize scheduler string storage to eliminate heap allocations (#9251) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/component.cpp | 18 +- esphome/core/component.h | 34 ++++ esphome/core/scheduler.cpp | 175 +++++++++++++----- esphome/core/scheduler.h | 119 ++++++++++-- .../fixtures/scheduler_string_test.yaml | 164 ++++++++++++++++ .../integration/test_scheduler_string_test.py | 166 +++++++++++++++++ 6 files changed, 614 insertions(+), 62 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_string_test.yaml create mode 100644 tests/integration/test_scheduler_string_test.py diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 8fa63de84e..6661223e35 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -60,10 +60,18 @@ void Component::set_interval(const std::string &name, uint32_t interval, std::fu App.scheduler.set_interval(this, name, interval, std::move(f)); } +void Component::set_interval(const char *name, uint32_t interval, std::function &&f) { // NOLINT + App.scheduler.set_interval(this, name, interval, std::move(f)); +} + bool Component::cancel_interval(const std::string &name) { // NOLINT return App.scheduler.cancel_interval(this, name); } +bool Component::cancel_interval(const char *name) { // NOLINT + return App.scheduler.cancel_interval(this, name); +} + void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); @@ -77,10 +85,18 @@ void Component::set_timeout(const std::string &name, uint32_t timeout, std::func App.scheduler.set_timeout(this, name, timeout, std::move(f)); } +void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, name, timeout, std::move(f)); +} + bool Component::cancel_timeout(const std::string &name) { // NOLINT return App.scheduler.cancel_timeout(this, name); } +bool Component::cancel_timeout(const char *name) { // NOLINT + return App.scheduler.cancel_timeout(this, name); +} + void Component::call_loop() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { @@ -189,7 +205,7 @@ bool Component::is_in_loop_state() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; } void Component::defer(std::function &&f) { // NOLINT - App.scheduler.set_timeout(this, "", 0, std::move(f)); + App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f)); } bool Component::cancel_defer(const std::string &name) { // NOLINT return App.scheduler.cancel_timeout(this, name); diff --git a/esphome/core/component.h b/esphome/core/component.h index 7f2bdd8414..5b37deeb68 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -260,6 +260,22 @@ class Component { */ void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT + /** Set an interval function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this interval function (must have static lifetime) + * @param interval The interval in ms + * @param f The function to call + */ + void set_interval(const char *name, uint32_t interval, std::function &&f); // NOLINT + void set_interval(uint32_t interval, std::function &&f); // NOLINT /** Cancel an interval function. @@ -268,6 +284,7 @@ class Component { * @return Whether an interval functions was deleted. */ bool cancel_interval(const std::string &name); // NOLINT + bool cancel_interval(const char *name); // NOLINT /** Set an retry function with a unique name. Empty name means no cancelling possible. * @@ -328,6 +345,22 @@ class Component { */ void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT + /** Set a timeout function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "init") + * - A static const char* variable + * - A pointer with lifetime >= the timeout duration + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this timeout function (must have static lifetime) + * @param timeout The timeout in ms + * @param f The function to call + */ + void set_timeout(const char *name, uint32_t timeout, std::function &&f); // NOLINT + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT /** Cancel a timeout function. @@ -336,6 +369,7 @@ class Component { * @return Whether a timeout functions was deleted. */ bool cancel_timeout(const std::string &name); // NOLINT + bool cancel_timeout(const char *name); // NOLINT /** Defer a callback to the next loop() call. * diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 8144435163..5c01b4f3f4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -7,6 +7,7 @@ #include "esphome/core/log.h" #include #include +#include namespace esphome { @@ -17,75 +18,138 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER +#ifdef ESPHOME_DEBUG_SCHEDULER +// Helper to validate that a pointer looks like it's in static memory +static void validate_static_string(const char *name) { + if (name == nullptr) + return; + + // This is a heuristic check - stack and heap pointers are typically + // much higher in memory than static data + uintptr_t addr = reinterpret_cast(name); + + // Create a stack variable to compare against + int stack_var; + uintptr_t stack_addr = reinterpret_cast(&stack_var); + + // If the string pointer is near our stack variable, it's likely on the stack + // Using 8KB range as ESP32 main task stack is typically 8192 bytes + if (addr > (stack_addr - 0x2000) && addr < (stack_addr + 0x2000)) { + ESP_LOGW(TAG, + "WARNING: Scheduler name '%s' at %p appears to be on the stack - this is unsafe!\n" + " Stack reference at %p", + name, name, &stack_var); + } + + // Also check if it might be on the heap by seeing if it's in a very different range + // This is platform-specific but generally heap is allocated far from static memory + static const char *static_str = "test"; + uintptr_t static_addr = reinterpret_cast(static_str); + + // If the address is very far from known static memory, it might be heap + if (addr > static_addr + 0x100000 || (static_addr > 0x100000 && addr < static_addr - 0x100000)) { + ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str); + } +} +#endif + // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to // avoid the main thread modifying the list while it is being accessed. -void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function func) { - const auto now = this->millis_(); +// Common implementation for both timeout and interval +void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, + const void *name_ptr, uint32_t delay, std::function func) { + // Get the name as const char* + const char *name_cstr = + is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); - if (!name.empty()) - this->cancel_timeout(component, name); + // Cancel existing timer if name is not empty + if (name_cstr != nullptr && name_cstr[0] != '\0') { + this->cancel_item_(component, name_cstr, type); + } - if (timeout == SCHEDULER_DONT_RUN) + if (delay == SCHEDULER_DONT_RUN) return; + const auto now = this->millis_(); + + // Create and populate the scheduler item auto item = make_unique(); item->component = component; - item->name = name; - item->type = SchedulerItem::TIMEOUT; - item->next_execution_ = now + timeout; + item->set_name(name_cstr, !is_static_string); + item->type = type; item->callback = std::move(func); item->remove = false; + + // Type-specific setup + if (type == SchedulerItem::INTERVAL) { + item->interval = delay; + // Calculate random offset (0 to interval/2) + uint32_t offset = (delay != 0) ? (random_uint32() % delay) / 2 : 0; + item->next_execution_ = now + offset; + } else { + item->interval = 0; + item->next_execution_ = now + delay; + } + #ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "set_timeout(name='%s/%s', timeout=%" PRIu32 ")", item->get_source(), name.c_str(), timeout); + // Validate static strings in debug mode + if (is_static_string && name_cstr != nullptr) { + validate_static_string(name_cstr); + } + + // Debug logging + const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; + if (type == SchedulerItem::TIMEOUT) { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(), + name_cstr ? name_cstr : "(null)", type_str, delay); + } else { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), + name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); + } #endif + this->push_(std::move(item)); } + +void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, true, name, timeout, std::move(func)); +} + +void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, + std::function func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); +} bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); } +bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { + return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); +} void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, std::function func) { - const auto now = this->millis_(); + this->set_timer_common_(component, SchedulerItem::INTERVAL, false, &name, interval, std::move(func)); +} - if (!name.empty()) - this->cancel_interval(component, name); - - if (interval == SCHEDULER_DONT_RUN) - return; - - // only put offset in lower half - uint32_t offset = 0; - if (interval != 0) - offset = (random_uint32() % interval) / 2; - - auto item = make_unique(); - item->component = component; - item->name = name; - item->type = SchedulerItem::INTERVAL; - item->interval = interval; - item->next_execution_ = now + offset; - item->callback = std::move(func); - item->remove = false; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "set_interval(name='%s/%s', interval=%" PRIu32 ", offset=%" PRIu32 ")", item->get_source(), - name.c_str(), interval, offset); -#endif - this->push_(std::move(item)); +void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval, + std::function func) { + this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::INTERVAL); } +bool HOT Scheduler::cancel_interval(Component *component, const char *name) { + return this->cancel_item_(component, name, SchedulerItem::INTERVAL); +} struct RetryArgs { std::function func; uint8_t retry_countdown; uint32_t current_interval; Component *component; - std::string name; + std::string name; // Keep as std::string since retry uses it dynamically float backoff_increase_factor; Scheduler *scheduler; }; @@ -154,7 +218,7 @@ void HOT Scheduler::call() { if (now - last_print > 2000) { last_print = now; std::vector> old_items; - ESP_LOGD(TAG, "Items: count=%u, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, this->last_millis_); while (!this->empty_()) { this->lock_.lock(); @@ -162,8 +226,9 @@ void HOT Scheduler::call() { this->pop_raw_(); this->lock_.unlock(); + const char *name = item->get_name(); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, - item->get_type_str(), item->get_source(), item->name.c_str(), item->interval, + item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, item->next_execution_ - now, item->next_execution_); old_items.push_back(std::move(item)); @@ -220,9 +285,10 @@ void HOT Scheduler::call() { App.set_current_component(item->component); #ifdef ESPHOME_DEBUG_SCHEDULER + const char *item_name = item->get_name(); ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), item->get_source(), item->name.c_str(), item->interval, item->next_execution_, - now); + item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, + item->next_execution_, now); #endif // Warning: During callback(), a lot of stuff can happen, including: @@ -298,19 +364,33 @@ void HOT Scheduler::push_(std::unique_ptr item) { LockGuard guard{this->lock_}; this->to_add_.push_back(std::move(item)); } -bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { +// Common implementation for cancel operations +bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, + SchedulerItem::Type type) { + // Get the name as const char* + const char *name_cstr = + is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + + // Handle null or empty names + if (name_cstr == nullptr) + return false; + // obtain lock because this function iterates and can be called from non-loop task context LockGuard guard{this->lock_}; bool ret = false; + for (auto &it : this->items_) { - if (it->component == component && it->name == name && it->type == type && !it->remove) { + const char *item_name = it->get_name(); + if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type && + !it->remove) { to_remove_++; it->remove = true; ret = true; } } for (auto &it : this->to_add_) { - if (it->component == component && it->name == name && it->type == type) { + const char *item_name = it->get_name(); + if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type) { it->remove = true; ret = true; } @@ -318,6 +398,15 @@ bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, return ret; } + +bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { + return this->cancel_item_common_(component, false, &name, type); +} + +bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) { + return this->cancel_item_common_(component, true, name, type); +} + uint64_t Scheduler::millis_() { // Get the current 32-bit millis value const uint32_t now = millis(); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 1284bcd4a7..a64968932e 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -12,11 +12,40 @@ class Component; class Scheduler { public: + // Public API - accepts std::string for backward compatibility void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); - bool cancel_timeout(Component *component, const std::string &name); - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); - bool cancel_interval(Component *component, const std::string &name); + /** Set a timeout with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ + void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func); + + bool cancel_timeout(Component *component, const std::string &name); + bool cancel_timeout(Component *component, const char *name); + + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); + + /** Set an interval with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ + void set_interval(Component *component, const char *name, uint32_t interval, std::function func); + + bool cancel_interval(Component *component, const std::string &name); + bool cancel_interval(Component *component, const char *name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); @@ -36,32 +65,86 @@ class Scheduler { // with a 16-bit rollover counter to create a 64-bit time that won't roll over for // billions of years. This ensures correct scheduling even when devices run for months. uint64_t next_execution_; - std::string name; - std::function callback; - enum Type : uint8_t { TIMEOUT, INTERVAL } type; - bool remove; - static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - const char *get_type_str() { - switch (this->type) { - case SchedulerItem::INTERVAL: - return "interval"; - case SchedulerItem::TIMEOUT: - return "timeout"; - default: - return ""; + // Optimized name storage using tagged union + union { + const char *static_name; // For string literals (no allocation) + char *dynamic_name; // For allocated strings + } name_; + + std::function callback; + + // Bit-packed fields to minimize padding + enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; + bool remove : 1; + bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) + // 5 bits padding + + // Constructor + SchedulerItem() + : component(nullptr), interval(0), next_execution_(0), type(TIMEOUT), remove(false), name_is_dynamic(false) { + name_.static_name = nullptr; + } + + // Destructor to clean up dynamic names + ~SchedulerItem() { + if (name_is_dynamic) { + delete[] name_.dynamic_name; } } - const char *get_source() { - return this->component != nullptr ? this->component->get_component_source() : "unknown"; + + // Delete copy operations to prevent accidental copies + SchedulerItem(const SchedulerItem &) = delete; + SchedulerItem &operator=(const SchedulerItem &) = delete; + + // Default move operations + SchedulerItem(SchedulerItem &&) = default; + SchedulerItem &operator=(SchedulerItem &&) = default; + + // Helper to get the name regardless of storage type + const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + + // Helper to set name with proper ownership + void set_name(const char *name, bool make_copy = false) { + // Clean up old dynamic name if any + if (name_is_dynamic && name_.dynamic_name) { + delete[] name_.dynamic_name; + name_is_dynamic = false; + } + + if (!name || !name[0]) { + name_.static_name = nullptr; + } else if (make_copy) { + // Make a copy for dynamic strings + size_t len = strlen(name); + name_.dynamic_name = new char[len + 1]; + memcpy(name_.dynamic_name, name, len + 1); + name_is_dynamic = true; + } else { + // Use static string directly + name_.static_name = name; + } } + + static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); + const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + const char *get_source() const { return component ? component->get_component_source() : "unknown"; } }; + // Common implementation for both timeout and interval + void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, + uint32_t delay, std::function func); + uint64_t millis_(); void cleanup_(); void pop_raw_(); void push_(std::unique_ptr item); + // Common implementation for cancel operations + bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); + bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type); + bool empty_() { this->cleanup_(); return this->items_.empty(); diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml new file mode 100644 index 0000000000..1188577e15 --- /dev/null +++ b/tests/integration/fixtures/scheduler_string_test.yaml @@ -0,0 +1,164 @@ +esphome: + name: scheduler-string-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler string tests" + platformio_options: + build_flags: + - "-DESPHOME_DEBUG_SCHEDULER" # Enable scheduler debug logging + +host: +api: +logger: + level: VERBOSE + +globals: + - id: timeout_counter + type: int + initial_value: '0' + - id: interval_counter + type: int + initial_value: '0' + - id: dynamic_counter + type: int + initial_value: '0' + - id: static_tests_done + type: bool + initial_value: 'false' + - id: dynamic_tests_done + type: bool + initial_value: 'false' + - id: results_reported + type: bool + initial_value: 'false' + +script: + - id: test_static_strings + then: + - logger.log: "Testing static string timeouts and intervals" + - lambda: |- + auto *component1 = id(test_sensor1); + // Test 1: Static string literals with set_timeout + App.scheduler.set_timeout(component1, "static_timeout_1", 50, []() { + ESP_LOGI("test", "Static timeout 1 fired"); + id(timeout_counter) += 1; + }); + + // Test 2: Static const char* with set_timeout + static const char* TIMEOUT_NAME = "static_timeout_2"; + App.scheduler.set_timeout(component1, TIMEOUT_NAME, 100, []() { + ESP_LOGI("test", "Static timeout 2 fired"); + id(timeout_counter) += 1; + }); + + // Test 3: Static string literal with set_interval + App.scheduler.set_interval(component1, "static_interval_1", 200, []() { + ESP_LOGI("test", "Static interval 1 fired, count: %d", id(interval_counter)); + id(interval_counter) += 1; + if (id(interval_counter) >= 3) { + App.scheduler.cancel_interval(id(test_sensor1), "static_interval_1"); + ESP_LOGI("test", "Cancelled static interval 1"); + } + }); + + // Test 4: Empty string (should be handled safely) + App.scheduler.set_timeout(component1, "", 150, []() { + ESP_LOGI("test", "Empty string timeout fired"); + }); + + // Test 5: Cancel timeout with const char* literal + App.scheduler.set_timeout(component1, "cancel_static_timeout", 5000, []() { + ESP_LOGI("test", "This static timeout should be cancelled"); + }); + // Cancel using const char* directly + App.scheduler.cancel_timeout(component1, "cancel_static_timeout"); + ESP_LOGI("test", "Cancelled static timeout using const char*"); + + - id: test_dynamic_strings + then: + - logger.log: "Testing dynamic string timeouts and intervals" + - lambda: |- + auto *component2 = id(test_sensor2); + + // Test 6: Dynamic string with set_timeout (std::string) + std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++); + App.scheduler.set_timeout(component2, dynamic_name, 100, []() { + ESP_LOGI("test", "Dynamic timeout fired"); + id(timeout_counter) += 1; + }); + + // Test 7: Dynamic string with set_interval + std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++); + App.scheduler.set_interval(component2, interval_name, 250, [interval_name]() { + ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str()); + id(interval_counter) += 1; + if (id(interval_counter) >= 6) { + App.scheduler.cancel_interval(id(test_sensor2), interval_name); + ESP_LOGI("test", "Cancelled dynamic interval"); + } + }); + + // Test 8: Cancel with different string object but same content + std::string cancel_name = "cancel_test"; + App.scheduler.set_timeout(component2, cancel_name, 2000, []() { + ESP_LOGI("test", "This should be cancelled"); + }); + + // Cancel using a different string object + std::string cancel_name_2 = "cancel_test"; + App.scheduler.cancel_timeout(component2, cancel_name_2); + ESP_LOGI("test", "Cancelled timeout using different string object"); + + - id: report_results + then: + - lambda: |- + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d", + id(timeout_counter), id(interval_counter)); + +sensor: + - platform: template + name: Test Sensor 1 + id: test_sensor1 + lambda: return 1.0; + update_interval: never + + - platform: template + name: Test Sensor 2 + id: test_sensor2 + lambda: return 2.0; + update_interval: never + +interval: + # Run static string tests after boot - using script to run once + - interval: 0.1s + then: + - if: + condition: + lambda: 'return id(static_tests_done) == false;' + then: + - lambda: 'id(static_tests_done) = true;' + - script.execute: test_static_strings + - logger.log: "Started static string tests" + + # Run dynamic string tests after static tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(static_tests_done) && !id(dynamic_tests_done);' + then: + - lambda: 'id(dynamic_tests_done) = true;' + - delay: 0.2s + - script.execute: test_dynamic_strings + + # Report results after all tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(dynamic_tests_done) && !id(results_reported);' + then: + - lambda: 'id(results_reported) = true;' + - delay: 1s + - script.execute: report_results diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py new file mode 100644 index 0000000000..b5ca07f9db --- /dev/null +++ b/tests/integration/test_scheduler_string_test.py @@ -0,0 +1,166 @@ +"""Test scheduler string optimization with static and dynamic strings.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_string_test( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler handles both static and dynamic strings correctly.""" + # Track counts + timeout_count = 0 + interval_count = 0 + + # Events for each test completion + static_timeout_1_fired = asyncio.Event() + static_timeout_2_fired = asyncio.Event() + static_interval_fired = asyncio.Event() + static_interval_cancelled = asyncio.Event() + empty_string_timeout_fired = asyncio.Event() + static_timeout_cancelled = asyncio.Event() + dynamic_timeout_fired = asyncio.Event() + dynamic_interval_fired = asyncio.Event() + cancel_test_done = asyncio.Event() + final_results_logged = asyncio.Event() + + # Track interval counts + static_interval_count = 0 + dynamic_interval_count = 0 + + def on_log_line(line: str) -> None: + nonlocal \ + timeout_count, \ + interval_count, \ + static_interval_count, \ + dynamic_interval_count + + # Strip ANSI color codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + # Check for static timeout completions + if "Static timeout 1 fired" in clean_line: + static_timeout_1_fired.set() + timeout_count += 1 + + elif "Static timeout 2 fired" in clean_line: + static_timeout_2_fired.set() + timeout_count += 1 + + # Check for static interval + elif "Static interval 1 fired" in clean_line: + match = re.search(r"count: (\d+)", clean_line) + if match: + static_interval_count = int(match.group(1)) + static_interval_fired.set() + + elif "Cancelled static interval 1" in clean_line: + static_interval_cancelled.set() + + # Check for empty string timeout + elif "Empty string timeout fired" in clean_line: + empty_string_timeout_fired.set() + + # Check for static timeout cancellation + elif "Cancelled static timeout using const char*" in clean_line: + static_timeout_cancelled.set() + + # Check for dynamic string tests + elif "Dynamic timeout fired" in clean_line: + dynamic_timeout_fired.set() + timeout_count += 1 + + elif "Dynamic interval fired" in clean_line: + dynamic_interval_count += 1 + dynamic_interval_fired.set() + + # Check for cancel test + elif "Cancelled timeout using different string object" in clean_line: + cancel_test_done.set() + + # Check for final results + elif "Final results" in clean_line: + match = re.search(r"Timeouts: (\d+), Intervals: (\d+)", clean_line) + if match: + timeout_count = int(match.group(1)) + interval_count = int(match.group(2)) + final_results_logged.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-string-test" + + # Wait for static string tests + try: + await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static timeout 1 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static timeout 2 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(static_interval_fired.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Static interval did not fire within 1 second") + + try: + await asyncio.wait_for(static_interval_cancelled.wait(), timeout=2.0) + except asyncio.TimeoutError: + pytest.fail("Static interval was not cancelled within 2 seconds") + + # Verify static interval ran at least 3 times + assert static_interval_count >= 2, ( + f"Expected static interval to run at least 3 times, got {static_interval_count + 1}" + ) + + # Verify static timeout was cancelled + assert static_timeout_cancelled.is_set(), ( + "Static timeout should have been cancelled" + ) + + # Wait for dynamic string tests + try: + await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Dynamic timeout did not fire within 1 second") + + try: + await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=1.5) + except asyncio.TimeoutError: + pytest.fail("Dynamic interval did not fire within 1.5 seconds") + + # Wait for cancel test + try: + await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Cancel test did not complete within 1 second") + + # Wait for final results + try: + await asyncio.wait_for(final_results_logged.wait(), timeout=4.0) + except asyncio.TimeoutError: + pytest.fail("Final results were not logged within 4 seconds") + + # Verify results + assert timeout_count >= 3, f"Expected at least 3 timeouts, got {timeout_count}" + assert interval_count >= 3, ( + f"Expected at least 3 interval fires, got {interval_count}" + ) + + # Empty string timeout DOES fire (scheduler accepts empty names) + assert empty_string_timeout_fired.is_set(), "Empty string timeout should fire"