From 26e4cda610d32c21c980c1fe96eda4b70551d8b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 02:25:54 +0100 Subject: [PATCH 01/95] [logger] Use vsnprintf_P directly for ESP8266 flash format strings (#13716) --- esphome/components/logger/logger.cpp | 51 +++++++--------------------- esphome/components/logger/logger.h | 45 ++++++++++++++---------- 2 files changed, 40 insertions(+), 56 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 3a726d4046..25243ff3f6 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -128,22 +128,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. // // This function handles format strings stored in flash memory (PROGMEM) to save RAM. -// The buffer is used in a special way to avoid allocating extra memory: -// -// Memory layout during execution: -// Step 1: Copy format string from flash to buffer -// tx_buffer_: [format_string][null][.....................] -// tx_buffer_at_: ------------------^ -// msg_start: saved here -----------^ -// -// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning -// and writes formatted output starting at msg_start position -// tx_buffer_: [format_string][null][formatted_message][null] -// tx_buffer_at_: -------------------------------------^ -// -// Step 3: Output the formatted message (starting at msg_start) -// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start -// which points to: [formatted_message][null] +// Uses vsnprintf_P to read the format string directly from flash without copying to RAM. // void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT @@ -153,35 +138,25 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas RecursionGuard guard(global_recursion_guard_); this->tx_buffer_at_ = 0; - // Copy format string from progmem - auto *format_pgm_p = reinterpret_cast(format); - char ch = '.'; - while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') { - this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++); - } + // Write header, format body directly from flash, and write footer + this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_, + this->tx_buffer_size_); + this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_, + reinterpret_cast(format), args); + this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Buffer full from copying format - RAII guard handles cleanup on return - if (this->tx_buffer_at_ >= this->tx_buffer_size_) { - return; - } - - // Save the offset before calling format_log_to_buffer_with_terminator_ - // since it will increment tx_buffer_at_ to the end of the formatted string - uint16_t msg_start = this->tx_buffer_at_; - this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, - &this->tx_buffer_at_, this->tx_buffer_size_); - - uint16_t msg_length = - this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + // Ensure null termination + uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_; + this->tx_buffer_[null_pos] = '\0'; // Listeners get message first (before console write) #ifdef USE_LOG_LISTENERS for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); + listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); #endif - // Write to console starting at the msg_start - this->write_tx_buffer_to_console_(msg_start, &msg_length); + // Write to console + this->write_tx_buffer_to_console_(); } #endif // USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index fe9cab4993..40ac9a38aa 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -597,31 +597,40 @@ class Logger : public Component { *buffer_at = pos; } + // Helper to process vsnprintf return value and strip trailing newlines. + // Updates buffer_at with the formatted length, handling truncation: + // - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + __attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at, + uint16_t remaining, int ret) { + if (ret < 0) + return; // Encoding error, do not increment buffer_at + *buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast(ret); + // Remove all trailing newlines right after formatting + while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') + (*buffer_at)--; + } + inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, va_list args) { - // Get remaining capacity in the buffer + // Check remaining capacity in the buffer if (*buffer_at >= buffer_size) return; const uint16_t remaining = buffer_size - *buffer_at; - - const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); - - if (ret < 0) { - return; // Encoding error, do not increment buffer_at - } - - // Update buffer_at with the formatted length (handle truncation) - // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator - // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator - uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; - *buffer_at += formatted_len; - - // Remove all trailing newlines right after formatting - while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') { - (*buffer_at)--; - } + process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args)); } +#ifdef USE_STORE_LOG_STR_IN_FLASH + // ESP8266 variant that reads format string directly from flash using vsnprintf_P + inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format, + va_list args) { + if (*buffer_at >= buffer_size) + return; + const uint16_t remaining = buffer_size - *buffer_at; + process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args)); + } +#endif + inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); From efecea9450462e3cb65d1c361e57aa5b9d0463ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:27:34 +0100 Subject: [PATCH 02/95] Bump github/codeql-action from 4.32.0 to 4.32.1 (#13726) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index be761cee3d..817ea1d2be 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 with: category: "/language:${{matrix.language}}" From ccf5c1f7e9103a470f9167105ae266af760dd7e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 03:12:12 +0100 Subject: [PATCH 03/95] [esp32] Exclude additional unused IDF components (driver, dac, mcpwm, twai, openthread, ulp) (#13664) --- esphome/components/esp32/__init__.py | 6 ++++++ esphome/components/esp32_can/canbus.py | 5 +++++ esphome/components/esp32_dac/output.py | 8 +++++++- esphome/components/esp32_touch/__init__.py | 2 ++ esphome/components/openthread/__init__.py | 4 ++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index aed4ecad90..4c53b42e6f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -124,10 +124,14 @@ COMPILER_OPTIMIZATIONS = { # - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing + "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers "esp_adc", # ADC driver - only needed by adc component + "esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch + "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component "esp_eth", # Ethernet driver - only needed by ethernet component "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality "esp_http_client", # HTTP client - only needed by http_request component @@ -138,9 +142,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "espcoredump", # Core dump support - ESPHome has its own debug component "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation + "openthread", # Thread protocol - only needed by openthread component "perfmon", # Xtensa performance monitor - ESPHome has its own debug component "protocomm", # Protocol communication for provisioning - unused by ESPHome "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) + "ulp", # ULP coprocessor - not currently used by any ESPHome component "unity", # Unit testing framework - ESPHome doesn't use IDF's testing "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index 0768b35507..7245ba7513 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -15,6 +15,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S2, VARIANT_ESP32S3, get_esp32_variant, + include_builtin_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate): async def to_code(config): + # Legacy driver component provides driver/twai.h header + include_builtin_idf_component("driver") + # Also enable esp_driver_twai for future migration to new API + include_builtin_idf_component("esp_driver_twai") var = cg.new_Pvariable(config[CONF_ID]) await canbus.register_canbus(var, config) diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py index daace596d3..7c63d7bd11 100644 --- a/esphome/components/esp32_dac/output.py +++ b/esphome/components/esp32_dac/output.py @@ -1,7 +1,12 @@ from esphome import pins import esphome.codegen as cg from esphome.components import output -from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + get_esp32_variant, + include_builtin_idf_component, +) import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN @@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( async def to_code(config): + include_builtin_idf_component("esp_driver_dac") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await output.register_output(var, config) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 6accb89c35..a02370a343 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -269,6 +269,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) include_builtin_idf_component("esp_driver_touch_sens") + # Legacy driver component provides driver/touch_sensor.h header + include_builtin_idf_component("driver") touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 26c05a0a86..89d335c574 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -4,6 +4,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, + include_builtin_idf_component, only_on_variant, require_vfs_select, ) @@ -172,6 +173,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): + # Re-enable openthread IDF component (excluded by default) + include_builtin_idf_component("openthread") + cg.add_define("USE_OPENTHREAD") # OpenThread SRP needs access to mDNS services after setup From ae71f07abb34b9b053eb5233f1ee39fd1936de7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 03:19:38 +0100 Subject: [PATCH 04/95] [http_request] Fix requests taking full timeout when response is already complete (#13649) --- .../update/esp32_hosted_update.cpp | 14 ++++- .../components/http_request/http_request.h | 55 ++++++++++++++----- .../http_request/http_request_arduino.cpp | 20 ++++++- .../http_request/http_request_idf.cpp | 9 +-- .../http_request/ota/ota_http_request.cpp | 6 +- 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index dac2b01425..a7d5f7e3d5 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -211,11 +211,14 @@ bool Esp32HostedUpdate::fetch_manifest_() { int read_or_error = container->read(buf, sizeof(buf)); App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. if (result != http_request::HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT json_str.append(reinterpret_cast(buf), read_or_error); } container->end(); @@ -336,9 +339,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. + if (result == http_request::HttpReadLoopResult::COMPLETE) + break; if (result != http_request::HttpReadLoopResult::DATA) { if (result == http_request::HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading firmware data"); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 79098a6b72..c88360ca78 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -26,6 +26,7 @@ struct Header { enum HttpStatus { HTTP_STATUS_OK = 200, HTTP_STATUS_NO_CONTENT = 204, + HTTP_STATUS_RESET_CONTENT = 205, HTTP_STATUS_PARTIAL_CONTENT = 206, /* 3xx - Redirection */ @@ -126,19 +127,21 @@ struct HttpReadResult { /// Result of processing a non-blocking read with timeout (for manual loops) enum class HttpReadLoopResult : uint8_t { - DATA, ///< Data was read, process it - RETRY, ///< No data yet, already delayed, caller should continue loop - ERROR, ///< Read error, caller should exit loop - TIMEOUT, ///< Timeout waiting for data, caller should exit loop + DATA, ///< Data was read, process it + COMPLETE, ///< All content has been read, caller should exit loop + RETRY, ///< No data yet, already delayed, caller should continue loop + ERROR, ///< Read error, caller should exit loop + TIMEOUT, ///< Timeout waiting for data, caller should exit loop }; /// Process a read result with timeout tracking and delay handling /// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error /// @param last_data_time Time of last successful read, updated when data received /// @param timeout_ms Maximum time to wait for data -/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit -inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, - uint32_t timeout_ms) { +/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete()) +/// @return How the caller should proceed - see HttpReadLoopResult enum +inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, + bool is_read_complete) { if (bytes_read_or_error > 0) { last_data_time = millis(); return HttpReadLoopResult::DATA; @@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_ if (bytes_read_or_error < 0) { return HttpReadLoopResult::ERROR; } - // bytes_read_or_error == 0: no data available yet + // bytes_read_or_error == 0: either "no data yet" or "all content read" + if (is_read_complete) { + return HttpReadLoopResult::COMPLETE; + } if (millis() - last_data_time >= timeout_ms) { return HttpReadLoopResult::TIMEOUT; } @@ -159,9 +165,9 @@ class HttpRequestComponent; class HttpContainer : public Parented { public: virtual ~HttpContainer() = default; - size_t content_length; - int status_code; - uint32_t duration_ms; + size_t content_length{0}; + int status_code{-1}; ///< -1 indicates no response received yet + uint32_t duration_ms{0}; /** * @brief Read data from the HTTP response body. @@ -194,9 +200,24 @@ class HttpContainer : public Parented { virtual void end() = 0; void set_secure(bool secure) { this->secure_ = secure; } + void set_chunked(bool chunked) { this->is_chunked_ = chunked; } size_t get_bytes_read() const { return this->bytes_read_; } + /// Check if all expected content has been read + /// For chunked responses, returns false (completion detected via read() returning error/EOF) + bool is_read_complete() const { + // Per RFC 9112, these responses have no body: + // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified + if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || + this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) { + return true; + } + // For non-chunked responses, complete when bytes_read >= content_length + // This handles both Content-Length: 0 and Content-Length: N cases + return !this->is_chunked_ && this->bytes_read_ >= this->content_length; + } + /** * @brief Get response headers. * @@ -209,6 +230,7 @@ class HttpContainer : public Parented { protected: size_t bytes_read_{0}; bool secure_{false}; + bool is_chunked_{false}; ///< True if response uses chunked transfer encoding std::map> response_headers_{}; }; @@ -219,7 +241,7 @@ class HttpContainer : public Parented { /// @param total_size Total bytes to read /// @param chunk_size Maximum bytes per read call /// @param timeout_ms Read timeout in milliseconds -/// @return HttpReadResult with status and error_code on failure +/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms) { size_t read_index = 0; @@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms); + auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + if (result == HttpReadLoopResult::COMPLETE) + break; // Server sent less data than requested, but transfer is complete if (result == HttpReadLoopResult::ERROR) return {HttpReadStatus::ERROR, read_bytes_or_error}; if (result == HttpReadLoopResult::TIMEOUT) @@ -393,11 +417,12 @@ template class HttpRequestSendAction : public Action { int read_or_error = container->read(buf + read_index, std::min(max_length - read_index, 512)); App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; if (result != HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT read_index += read_or_error; } response_body.reserve(read_index); diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 82538b2cb3..2f12b58766 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -135,9 +135,23 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the // early return check (bytes_read_ >= content_length) will never trigger. + // + // TODO: Chunked transfer encoding is NOT properly supported on Arduino. + // The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where + // esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr() + // returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead + // of decoded content. This wasn't noticed because requests would complete and payloads + // were only examined on IDF. The long transfer times were also masked by the misleading + // "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues: + // 1. Response body is corrupted - contains chunk size headers mixed with data + // 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout + // The proper fix would be to use getString() for chunked responses, which decodes chunks + // internally, but this buffers the entire response in memory. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; + // -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding + container->set_chunked(content_length == -1); container->duration_ms = millis() - start; return container; @@ -178,9 +192,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) - // For chunked encoding (content_length == SIZE_MAX), we can't use this check - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've read all expected content (non-chunked only) + // For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false + if (this->is_read_complete()) { return 0; // All content read successfully } // No data available - check if connection is still open diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 2b4dee953a..bd12b7d123 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -160,6 +160,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -195,6 +196,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -239,10 +241,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content - // Skip this check when content_length is 0 (chunked transfer encoding or unknown length) - // For chunked responses, esp_http_client_read() will return 0 when all data is received - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've already read all expected content (non-chunked only) + // For chunked responses (content_length == 0), esp_http_client_read() handles EOF + if (this->is_read_complete()) { return 0; // All content read successfully } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8a4b3684cf..8f4ecfab2d 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout); + auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added for OTA in the future. + if (result == HttpReadLoopResult::COMPLETE) + break; if (result != HttpReadLoopResult::DATA) { if (result == HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading data"); From 9f1a427ce235aa6ab3f1206cfd7940fa7f338b04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:03:52 +0100 Subject: [PATCH 05/95] [preferences] Use static storage for singletons and flash buffer (#13727) --- esphome/components/esp32/preferences.cpp | 7 ++++--- esphome/components/esp8266/preferences.cpp | 17 +++++++++-------- esphome/components/host/preferences.cpp | 7 ++++--- esphome/components/libretiny/preferences.cpp | 7 ++++--- esphome/components/rp2040/preferences.cpp | 17 +++++++++-------- esphome/components/zephyr/preferences.cpp | 7 ++++--- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 4e0bb68133..7d5af023b4 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -203,10 +203,11 @@ class ESP32Preferences : public ESPPreferences { } }; +static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->open(); - global_preferences = prefs; + s_preferences.open(); + global_preferences = &s_preferences; } } // namespace esp32 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 35d1cd07f7..f037b881a8 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -17,10 +17,6 @@ namespace esphome::esp8266 { static const char *const TAG = "esp8266.preferences"; -static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; @@ -43,6 +39,11 @@ static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif +static uint32_t + s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { return false; @@ -180,7 +181,6 @@ class ESP8266Preferences : public ESPPreferences { uint32_t current_flash_offset = 0; // in words void setup() { - s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT ESP_LOGVV(TAG, "Loading preferences from flash"); { @@ -283,10 +283,11 @@ class ESP8266Preferences : public ESPPreferences { } }; +static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - pref->setup(); - global_preferences = pref; + s_preferences.setup(); + global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 7b939cdebb..5ad87c1f2a 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -66,10 +66,11 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ return ESPPreferenceObject(backend); }; +static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - host_preferences = pref; - global_preferences = pref; + host_preferences = &s_preferences; + global_preferences = &s_preferences; } bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 978dcce3fa..5a60b535da 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -189,10 +189,11 @@ class LibreTinyPreferences : public ESPPreferences { } }; +static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->open(); - global_preferences = prefs; + s_preferences.open(); + global_preferences = &s_preferences; } } // namespace libretiny diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index e84033bc52..172da32adc 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -18,11 +18,12 @@ namespace rp2040 { static const char *const TAG = "rp2040.preferences"; -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static uint8_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512; -static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512; +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static uint8_t + s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation static constexpr size_t PREF_BUFFER_SIZE = 64; @@ -91,7 +92,6 @@ class RP2040Preferences : public ESPPreferences { RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} void setup() { - s_flash_storage = new uint8_t[RP2040_FLASH_STORAGE_SIZE]; // NOLINT ESP_LOGVV(TAG, "Loading preferences from flash"); memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); } @@ -149,10 +149,11 @@ class RP2040Preferences : public ESPPreferences { uint8_t *eeprom_sector_; }; +static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->setup(); - global_preferences = prefs; + s_preferences.setup(); + global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index 311133a813..f02fa16326 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -152,10 +152,11 @@ class ZephyrPreferences : public ESPPreferences { } }; +static ZephyrPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - global_preferences = prefs; - prefs->open(); + global_preferences = &s_preferences; + s_preferences.open(); } } // namespace zephyr From d41c84d624165984d1e1eaf6cde6312b4bb6f9a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:29:18 +0100 Subject: [PATCH 06/95] [wifi] Conditionally compile on_connect/on_disconnect triggers (#13684) --- esphome/components/wifi/__init__.py | 2 ++ esphome/components/wifi/automation.h | 16 ++++++++-------- esphome/components/wifi/wifi_component.cpp | 13 ++++++++++--- esphome/components/wifi/wifi_component.h | 19 ++++++++++++++----- esphome/core/defines.h | 2 ++ tests/components/wifi/common.yaml | 4 ++++ 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 98266eb589..6863e6fb62 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -585,11 +585,13 @@ async def to_code(config): await cg.past_safe_mode() if on_connect_config := config.get(CONF_ON_CONNECT): + cg.add_define("USE_WIFI_CONNECT_TRIGGER") await automation.build_automation( var.get_connect_trigger(), [], on_connect_config ) if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + cg.add_define("USE_WIFI_DISCONNECT_TRIGGER") await automation.build_automation( var.get_disconnect_trigger(), [], on_disconnect_config ) diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index fb0e71bcf6..1ad69b3992 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -48,7 +48,7 @@ template class WiFiConfigureAction : public Action, publi char ssid_buf[SSID_BUFFER_SIZE]; if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) { // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); + this->connect_trigger_.trigger(); return; } // Create a new WiFiAP object with the new SSID and password @@ -79,13 +79,13 @@ template class WiFiConfigureAction : public Action, publi // Start a timeout for the fallback if the connection to the old AP fails this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { this->connecting_ = false; - this->error_trigger_->trigger(); + this->error_trigger_.trigger(); }); }); } - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } - Trigger<> *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } + Trigger<> *get_error_trigger() { return &this->error_trigger_; } void loop() override { if (!this->connecting_) @@ -98,10 +98,10 @@ template class WiFiConfigureAction : public Action, publi char ssid_buf[SSID_BUFFER_SIZE]; if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) { // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); + this->connect_trigger_.trigger(); } else { // Callback to notify the user that the connection failed - this->error_trigger_->trigger(); + this->error_trigger_.trigger(); } } } @@ -110,8 +110,8 @@ template class WiFiConfigureAction : public Action, publi bool connecting_{false}; WiFiAP new_sta_; WiFiAP old_sta_; - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *error_trigger_{new Trigger<>()}; + Trigger<> connect_trigger_; + Trigger<> error_trigger_; }; } // namespace esphome::wifi diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 65c653a62a..c4bfdf3c42 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -651,14 +651,21 @@ void WiFiComponent::loop() { const uint32_t now = App.get_loop_component_start_time(); if (this->has_sta()) { +#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) if (this->is_connected() != this->handled_connected_state_) { +#ifdef USE_WIFI_DISCONNECT_TRIGGER if (this->handled_connected_state_) { - this->disconnect_trigger_->trigger(); - } else { - this->connect_trigger_->trigger(); + this->disconnect_trigger_.trigger(); } +#endif +#ifdef USE_WIFI_CONNECT_TRIGGER + if (!this->handled_connected_state_) { + this->connect_trigger_.trigger(); + } +#endif this->handled_connected_state_ = this->is_connected(); } +#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f27c522a1b..4bdc253f66 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -454,8 +454,12 @@ class WiFiComponent : public Component { void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; - Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } +#endif int32_t get_wifi_channel(); @@ -706,7 +710,9 @@ class WiFiComponent : public Component { // Group all boolean values together bool has_ap_{false}; +#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; +#endif bool error_from_callback_{false}; bool scan_done_{false}; bool ap_setup_{false}; @@ -733,9 +739,12 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif - // Pointers at the end (naturally aligned) - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *disconnect_trigger_{new Trigger<>()}; +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif private: // Stores a pointer to a string literal (static storage duration). diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1edc648084..ee865a7e65 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -227,6 +227,8 @@ #define USE_WIFI_SCAN_RESULTS_LISTENERS #define USE_WIFI_CONNECT_STATE_LISTENERS #define USE_WIFI_POWER_SAVE_LISTENERS +#define USE_WIFI_CONNECT_TRIGGER +#define USE_WIFI_DISCONNECT_TRIGGER #define ESPHOME_WIFI_IP_STATE_LISTENERS 2 #define ESPHOME_WIFI_SCAN_RESULTS_LISTENERS 2 #define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2 diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 7ce74ab00d..10b68347eb 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -26,3 +26,7 @@ wifi: - ssid: MySSID3 password: password3 priority: 0 + on_connect: + - logger.log: "WiFi connected!" + on_disconnect: + - logger.log: "WiFi disconnected!" From 8cb701e4128abdf0508ad90e97bffda0263accda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:29:31 +0100 Subject: [PATCH 07/95] [water_heater] Store mode strings in flash and avoid heap allocation in set_mode (#13728) --- .../components/water_heater/water_heater.cpp | 19 ++++++++++--------- .../components/water_heater/water_heater.h | 3 ++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 7266294d84..286addf7db 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/controller_registry.h" +#include "esphome/core/progmem.h" #include @@ -22,23 +23,23 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) { return *this; } -WaterHeaterCall &WaterHeaterCall::set_mode(const std::string &mode) { - if (str_equals_case_insensitive(mode, "OFF")) { +WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { + if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) { this->set_mode(WATER_HEATER_MODE_OFF); - } else if (str_equals_case_insensitive(mode, "ECO")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) { this->set_mode(WATER_HEATER_MODE_ECO); - } else if (str_equals_case_insensitive(mode, "ELECTRIC")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) { this->set_mode(WATER_HEATER_MODE_ELECTRIC); - } else if (str_equals_case_insensitive(mode, "PERFORMANCE")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) { this->set_mode(WATER_HEATER_MODE_PERFORMANCE); - } else if (str_equals_case_insensitive(mode, "HIGH_DEMAND")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) { this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); - } else if (str_equals_case_insensitive(mode, "HEAT_PUMP")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) { this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); - } else if (str_equals_case_insensitive(mode, "GAS")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) { this->set_mode(WATER_HEATER_MODE_GAS); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode); } return *this; } diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 84fc46d208..7bd05ba7f5 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -75,7 +75,8 @@ class WaterHeaterCall { WaterHeaterCall(WaterHeater *parent); WaterHeaterCall &set_mode(WaterHeaterMode mode); - WaterHeaterCall &set_mode(const std::string &mode); + WaterHeaterCall &set_mode(const char *mode); + WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); } WaterHeaterCall &set_target_temperature(float temperature); WaterHeaterCall &set_target_temperature_low(float temperature); WaterHeaterCall &set_target_temperature_high(float temperature); From 9d63642bdb16598ab8c0ec3eafbb00178df96955 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:29:43 +0100 Subject: [PATCH 08/95] [media_player] Store command strings in flash and avoid heap allocation in set_command (#13731) --- .../components/media_player/media_player.cpp | 21 ++++++++++--------- .../components/media_player/media_player.h | 3 ++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index b46ec39d30..17d9b054da 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace media_player { @@ -107,25 +108,25 @@ MediaPlayerCall &MediaPlayerCall::set_command(optional comma this->command_ = command; return *this; } -MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) { - if (str_equals_case_insensitive(command, "PLAY")) { +MediaPlayerCall &MediaPlayerCall::set_command(const char *command) { + if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PLAY")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_PLAY); - } else if (str_equals_case_insensitive(command, "PAUSE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PAUSE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_PAUSE); - } else if (str_equals_case_insensitive(command, "STOP")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_STOP); - } else if (str_equals_case_insensitive(command, "MUTE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("MUTE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_MUTE); - } else if (str_equals_case_insensitive(command, "UNMUTE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNMUTE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE); - } else if (str_equals_case_insensitive(command, "TOGGLE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE); - } else if (str_equals_case_insensitive(command, "TURN_ON")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_ON")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); - } else if (str_equals_case_insensitive(command, "TURN_OFF")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); } return *this; } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index b753e2d088..f75a68dd85 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -114,7 +114,8 @@ class MediaPlayerCall { MediaPlayerCall &set_command(MediaPlayerCommand command); MediaPlayerCall &set_command(optional command); - MediaPlayerCall &set_command(const std::string &command); + MediaPlayerCall &set_command(const char *command); + MediaPlayerCall &set_command(const std::string &command) { return this->set_command(command.c_str()); } MediaPlayerCall &set_media_url(const std::string &url); From fbeb0e8e542af3b8b4f2f86a1fc8d46e2bb915ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:40:44 +0100 Subject: [PATCH 09/95] [opentherm] Fix ESP-IDF build by re-enabling legacy driver component (#13732) --- esphome/components/opentherm/__init__.py | 8 ++++++++ esphome/components/opentherm/opentherm.cpp | 2 ++ esphome/components/opentherm/opentherm.h | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index 8cbee1eed2..dddb9dc891 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -4,8 +4,10 @@ from typing import Any from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266 +from esphome.core import CORE from . import const, generate, schema, validate @@ -83,6 +85,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config: dict[str, Any]) -> None: + if CORE.is_esp32: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # Provides driver/timer.h header for hardware timer API + # TODO: Remove this once opentherm migrates to GPTimer API (driver/gptimer.h) + include_builtin_idf_component("driver") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 2bf438a52f..cdf89207bc 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -8,6 +8,8 @@ #include "opentherm.h" #include "esphome/core/helpers.h" #include +// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) +// The legacy timer API is deprecated in ESP-IDF 5.x. See opentherm.h for details. #ifdef USE_ESP32 #include "driver/timer.h" #include "esp_err.h" diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index 3996481760..a2c347d0d8 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -12,6 +12,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) +// The legacy timer API is deprecated in ESP-IDF 5.x. Migration would allow removing the +// "driver" IDF component dependency. See: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id4 #ifdef USE_ESP32 #include "driver/timer.h" #endif From a430b3a42606696bda318af4314c1be26103a0b5 Mon Sep 17 00:00:00 2001 From: Roger Fachini Date: Mon, 2 Feb 2026 19:46:46 -0800 Subject: [PATCH 10/95] [speaker.media_player]: Add verbose error message for puremagic parsing (#13725) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/speaker/media_player/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 370b4576a7..034312236c 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -157,8 +157,14 @@ def _read_audio_file_and_type(file_config): import puremagic - file_type: str = puremagic.from_string(data) - file_type = file_type.removeprefix(".") + try: + file_type: str = puremagic.from_string(data) + file_type = file_type.removeprefix(".") + except puremagic.PureError as e: + raise cv.Invalid( + f"Unable to determine audio file type of '{path}'. " + f"Try re-encoding the file into a supported format. Details: {e}" + ) media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] if file_type in ("wav"): From ff6f7d3248926c7594a2de969b3fc2f6d93b3550 Mon Sep 17 00:00:00 2001 From: Andrew Gillis Date: Mon, 2 Feb 2026 22:59:51 -0500 Subject: [PATCH 11/95] [mipi_dsi] Add WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B (#13608) --- .../components/mipi_dsi/models/waveshare.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index c3d080f8b2..f83cacc5a3 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -94,3 +94,29 @@ DriverChip( (0x29, 0x00), ], ) + +DriverChip( + "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B", + height=600, + width=1024, + hsync_back_porch=160, + hsync_pulse_width=10, + hsync_front_porch=160, + vsync_back_porch=23, + vsync_pulse_width=1, + vsync_front_porch=12, + pclk_frequency="52MHz", + lane_bit_rate="900Mbps", + no_transform=True, + color_order="RGB", + initsequence=[ + (0x80, 0x8B), + (0x81, 0x78), + (0x82, 0x84), + (0x83, 0x88), + (0x84, 0xA8), + (0x85, 0xE3), + (0x86, 0x88), + (0xB2, 0x10), + ], +) From d4110bf6507be83235a5c397dd442574519cf99d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 05:29:24 +0100 Subject: [PATCH 12/95] [lock] Store state strings in flash and avoid heap allocation in set_state (#13729) --- esphome/components/lock/lock.cpp | 17 +++++++++-------- esphome/components/lock/lock.h | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 9fa1ba3600..4c61418256 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::lock { @@ -84,21 +85,21 @@ LockCall &LockCall::set_state(optional state) { this->state_ = state; return *this; } -LockCall &LockCall::set_state(const std::string &state) { - if (str_equals_case_insensitive(state, "LOCKED")) { +LockCall &LockCall::set_state(const char *state) { + if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) { this->set_state(LOCK_STATE_LOCKED); - } else if (str_equals_case_insensitive(state, "UNLOCKED")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) { this->set_state(LOCK_STATE_UNLOCKED); - } else if (str_equals_case_insensitive(state, "JAMMED")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) { this->set_state(LOCK_STATE_JAMMED); - } else if (str_equals_case_insensitive(state, "LOCKING")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) { this->set_state(LOCK_STATE_LOCKING); - } else if (str_equals_case_insensitive(state, "UNLOCKING")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) { this->set_state(LOCK_STATE_UNLOCKING); - } else if (str_equals_case_insensitive(state, "NONE")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("NONE")) == 0) { this->set_state(LOCK_STATE_NONE); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state); } return *this; } diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 69fc405713..bebd296eac 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -83,7 +83,8 @@ class LockCall { /// Set the state of the lock device. LockCall &set_state(optional state); /// Set the state of the lock device based on a string. - LockCall &set_state(const std::string &state); + LockCall &set_state(const char *state); + LockCall &set_state(const std::string &state) { return this->set_state(state.c_str()); } void perform(); From b3e09e5c68b4734bb8cd896ce8aedd00523e1a02 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:14:09 +1100 Subject: [PATCH 13/95] [key_collector] Add text sensor and allow multiple callbacks (#13617) --- esphome/components/key_collector/__init__.py | 91 +++++++++++++------ .../key_collector/key_collector.cpp | 45 +++++---- .../components/key_collector/key_collector.h | 36 ++++---- .../key_collector/text_sensor/__init__.py | 28 ++++++ tests/components/key_collector/common.yaml | 14 +++ 5 files changed, 146 insertions(+), 68 deletions(-) create mode 100644 esphome/components/key_collector/text_sensor/__init__.py diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index 17af40da1a..badb28c32c 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -1,4 +1,7 @@ +from dataclasses import dataclass + from esphome import automation +from esphome.automation import Trigger import esphome.codegen as cg from esphome.components import key_provider import esphome.config_validation as cv @@ -10,7 +13,10 @@ from esphome.const import ( CONF_ON_TIMEOUT, CONF_SOURCE_ID, CONF_TIMEOUT, + CONF_TRIGGER_ID, ) +from esphome.cpp_generator import MockObj, literal +from esphome.types import TemplateArgsType CODEOWNERS = ["@ssieb"] @@ -32,22 +38,50 @@ KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component) EnableAction = key_collector_ns.class_("EnableAction", automation.Action) DisableAction = key_collector_ns.class_("DisableAction", automation.Action) +X_TYPE = cg.std_string_ref.operator("const") + + +@dataclass +class Argument: + type: MockObj + name: str + + +TRIGGER_TYPES = { + CONF_ON_PROGRESS: [Argument(X_TYPE, "x"), Argument(cg.uint8, "start")], + CONF_ON_RESULT: [ + Argument(X_TYPE, "x"), + Argument(cg.uint8, "start"), + Argument(cg.uint8, "end"), + ], + CONF_ON_TIMEOUT: [Argument(X_TYPE, "x"), Argument(cg.uint8, "start")], +} + CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(KeyCollector), - cv.Optional(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider), - cv.Optional(CONF_MIN_LENGTH): cv.int_, - cv.Optional(CONF_MAX_LENGTH): cv.int_, + cv.Optional(CONF_SOURCE_ID): cv.ensure_list( + cv.use_id(key_provider.KeyProvider) + ), + cv.Optional(CONF_MIN_LENGTH): cv.uint16_t, + cv.Optional(CONF_MAX_LENGTH): cv.uint16_t, cv.Optional(CONF_START_KEYS): cv.string, cv.Optional(CONF_END_KEYS): cv.string, cv.Optional(CONF_END_KEY_REQUIRED): cv.boolean, cv.Optional(CONF_BACK_KEYS): cv.string, cv.Optional(CONF_CLEAR_KEYS): cv.string, cv.Optional(CONF_ALLOWED_KEYS): cv.string, - cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(single=True), - cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True), + **{ + cv.Optional(trigger_type): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(*[arg.type for arg in args]) + ), + } + ) + for trigger_type, args in TRIGGER_TYPES.items() + }, cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, } @@ -59,9 +93,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CONF_SOURCE_ID in config: - source = await cg.get_variable(config[CONF_SOURCE_ID]) - cg.add(var.set_provider(source)) + for source_conf in config.get(CONF_SOURCE_ID, ()): + source = await cg.get_variable(source_conf) + cg.add(var.add_provider(source)) if CONF_MIN_LENGTH in config: cg.add(var.set_min_length(config[CONF_MIN_LENGTH])) if CONF_MAX_LENGTH in config: @@ -78,26 +112,25 @@ async def to_code(config): cg.add(var.set_clear_keys(config[CONF_CLEAR_KEYS])) if CONF_ALLOWED_KEYS in config: cg.add(var.set_allowed_keys(config[CONF_ALLOWED_KEYS])) - if CONF_ON_PROGRESS in config: - await automation.build_automation( - var.get_progress_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start")], - config[CONF_ON_PROGRESS], - ) - if CONF_ON_RESULT in config: - await automation.build_automation( - var.get_result_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start"), (cg.uint8, "end")], - config[CONF_ON_RESULT], - ) - if CONF_ON_TIMEOUT in config: - await automation.build_automation( - var.get_timeout_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start")], - config[CONF_ON_TIMEOUT], - ) - if CONF_TIMEOUT in config: - cg.add(var.set_timeout(config[CONF_TIMEOUT])) + + for trigger_name, args in TRIGGER_TYPES.items(): + arglist: TemplateArgsType = [(arg.type, arg.name) for arg in args] + for conf in config.get(trigger_name, ()): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + add_trig = getattr( + var, + f"add_on_{trigger_name.rsplit('_', maxsplit=1)[-1].lower()}_callback", + ) + await automation.build_automation( + trigger, + arglist, + conf, + ) + lamb = trigger.trigger(*[literal(arg.name) for arg in args]) + cg.add(add_trig(await cg.process_lambda(lamb, arglist))) + + if timeout := config.get(CONF_TIMEOUT): + cg.add(var.set_timeout(timeout)) cg.add(var.set_enabled(config[CONF_ENABLE_ON_BOOT])) diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp index 9cfc74f50e..68d1c60bf9 100644 --- a/esphome/components/key_collector/key_collector.cpp +++ b/esphome/components/key_collector/key_collector.cpp @@ -7,15 +7,10 @@ namespace key_collector { static const char *const TAG = "key_collector"; -KeyCollector::KeyCollector() - : progress_trigger_(new Trigger()), - result_trigger_(new Trigger()), - timeout_trigger_(new Trigger()) {} - void KeyCollector::loop() { if ((this->timeout_ == 0) || this->result_.empty() || (millis() - this->last_key_time_ < this->timeout_)) return; - this->timeout_trigger_->trigger(this->result_, this->start_key_); + this->timeout_callbacks_.call(this->result_, this->start_key_); this->clear(); } @@ -43,64 +38,68 @@ void KeyCollector::dump_config() { ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0); } -void KeyCollector::set_provider(key_provider::KeyProvider *provider) { - provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); }); +void KeyCollector::add_provider(key_provider::KeyProvider *provider) { + provider->add_on_key_callback([this](uint8_t key) { this->send_key(key); }); } void KeyCollector::set_enabled(bool enabled) { this->enabled_ = enabled; - if (!enabled) + if (!enabled) { this->clear(false); + } } void KeyCollector::clear(bool progress_update) { + auto had_state = !this->result_.empty() || this->start_key_ != 0; this->result_.clear(); this->start_key_ = 0; - if (progress_update) - this->progress_trigger_->trigger(this->result_, 0); + if (progress_update && had_state) { + this->progress_callbacks_.call(this->result_, 0); + } + this->disable_loop(); } -void KeyCollector::send_key(uint8_t key) { this->key_pressed_(key); } - -void KeyCollector::key_pressed_(uint8_t key) { +void KeyCollector::send_key(uint8_t key) { if (!this->enabled_) return; this->last_key_time_ = millis(); if (!this->start_keys_.empty() && !this->start_key_) { if (this->start_keys_.find(key) != std::string::npos) { this->start_key_ = key; - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } return; } if (this->back_keys_.find(key) != std::string::npos) { if (!this->result_.empty()) { this->result_.pop_back(); - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } return; } if (this->clear_keys_.find(key) != std::string::npos) { - if (!this->result_.empty()) - this->clear(); + this->clear(); return; } if (this->end_keys_.find(key) != std::string::npos) { if ((this->min_length_ == 0) || (this->result_.size() >= this->min_length_)) { - this->result_trigger_->trigger(this->result_, this->start_key_, key); + this->result_callbacks_.call(this->result_, this->start_key_, key); this->clear(); } return; } - if (!this->allowed_keys_.empty() && (this->allowed_keys_.find(key) == std::string::npos)) + if (!this->allowed_keys_.empty() && this->allowed_keys_.find(key) == std::string::npos) return; - if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) + if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) { + if (this->result_.empty()) + this->enable_loop(); this->result_.push_back(key); + } if ((this->max_length_ > 0) && (this->result_.size() == this->max_length_) && (!this->end_key_required_)) { - this->result_trigger_->trigger(this->result_, this->start_key_, 0); + this->result_callbacks_.call(this->result_, this->start_key_, 0); this->clear(false); } - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } } // namespace key_collector diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 735f396809..8e30c333df 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -3,27 +3,33 @@ #include #include "esphome/components/key_provider/key_provider.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" namespace esphome { namespace key_collector { class KeyCollector : public Component { public: - KeyCollector(); void loop() override; void dump_config() override; - void set_provider(key_provider::KeyProvider *provider); - void set_min_length(uint32_t min_length) { this->min_length_ = min_length; }; - void set_max_length(uint32_t max_length) { this->max_length_ = max_length; }; + void add_provider(key_provider::KeyProvider *provider); + void set_min_length(uint16_t min_length) { this->min_length_ = min_length; }; + void set_max_length(uint16_t max_length) { this->max_length_ = max_length; }; void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; void set_back_keys(std::string back_keys) { this->back_keys_ = std::move(back_keys); }; void set_clear_keys(std::string clear_keys) { this->clear_keys_ = std::move(clear_keys); }; void set_allowed_keys(std::string allowed_keys) { this->allowed_keys_ = std::move(allowed_keys); }; - Trigger *get_progress_trigger() const { return this->progress_trigger_; }; - Trigger *get_result_trigger() const { return this->result_trigger_; }; - Trigger *get_timeout_trigger() const { return this->timeout_trigger_; }; + void add_on_progress_callback(std::function &&callback) { + this->progress_callbacks_.add(std::move(callback)); + } + void add_on_result_callback(std::function &&callback) { + this->result_callbacks_.add(std::move(callback)); + } + void add_on_timeout_callback(std::function &&callback) { + this->timeout_callbacks_.add(std::move(callback)); + } void set_timeout(int timeout) { this->timeout_ = timeout; }; void set_enabled(bool enabled); @@ -31,10 +37,8 @@ class KeyCollector : public Component { void send_key(uint8_t key); protected: - void key_pressed_(uint8_t key); - - uint32_t min_length_{0}; - uint32_t max_length_{0}; + uint16_t min_length_{0}; + uint16_t max_length_{0}; std::string start_keys_; std::string end_keys_; bool end_key_required_{false}; @@ -43,12 +47,12 @@ class KeyCollector : public Component { std::string allowed_keys_; std::string result_; uint8_t start_key_{0}; - Trigger *progress_trigger_; - Trigger *result_trigger_; - Trigger *timeout_trigger_; - uint32_t last_key_time_; + LazyCallbackManager progress_callbacks_; + LazyCallbackManager result_callbacks_; + LazyCallbackManager timeout_callbacks_; + uint32_t last_key_time_{}; uint32_t timeout_{0}; - bool enabled_; + bool enabled_{}; }; template class EnableAction : public Action, public Parented { diff --git a/esphome/components/key_collector/text_sensor/__init__.py b/esphome/components/key_collector/text_sensor/__init__.py new file mode 100644 index 0000000000..1676cf7bdf --- /dev/null +++ b/esphome/components/key_collector/text_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +from esphome.components.text_sensor import TextSensor +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.cpp_generator import literal +from esphome.types import TemplateArgsType + +from .. import CONF_ON_RESULT, CONF_SOURCE_ID, TRIGGER_TYPES, KeyCollector + +CONFIG_SCHEMA = text_sensor.text_sensor_schema(TextSensor).extend( + { + cv.GenerateID(CONF_SOURCE_ID): cv.use_id(KeyCollector), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_SOURCE_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + await text_sensor.register_text_sensor(var, config) + args = TRIGGER_TYPES[CONF_ON_RESULT] + arglist: TemplateArgsType = [(arg.type, arg.name) for arg in args] + cg.add( + parent.add_on_result_callback( + await cg.process_lambda(var.publish_state(literal(args[0].name)), arglist) + ) + ) diff --git a/tests/components/key_collector/common.yaml b/tests/components/key_collector/common.yaml index 12e541c865..43a0478a18 100644 --- a/tests/components/key_collector/common.yaml +++ b/tests/components/key_collector/common.yaml @@ -18,14 +18,23 @@ key_collector: - logger.log: format: "input progress: '%s', started by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)"] + - logger.log: + format: "second listener - progress: '%s'" + args: ['x.c_str()'] on_result: - logger.log: format: "input result: '%s', started by '%c', ended by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + - logger.log: + format: "second listener - result: '%s'" + args: ['x.c_str()'] on_timeout: - logger.log: format: "input timeout: '%s', started by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)"] + - logger.log: + format: "second listener - timeout: '%s'" + args: ['x.c_str()'] enable_on_boot: false button: @@ -34,3 +43,8 @@ button: on_press: - key_collector.enable: - key_collector.disable: + +text_sensor: + - platform: key_collector + id: collected_keys + source_id: reader From c027d9116febad4cd55d647b2090f22b4f3467e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 12:13:03 +0100 Subject: [PATCH 14/95] [template] Add additional tests for template select (#13741) --- tests/components/template/common-base.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 9dc65fbab8..9dece7c3a5 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -296,6 +296,16 @@ select: // Migration guide: Store in std::string std::string stored_option(id(template_select).current_option()); ESP_LOGI("test", "Stored: %s", stored_option.c_str()); + - platform: template + id: template_select_with_action + name: "Template select with action" + options: + - option_a + - option_b + set_action: + - logger.log: + format: "Selected: %s" + args: ["x.c_str()"] lock: - platform: template From f4d7d06c41a5347ae94bd857e8cfe62f1da5b8ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 12:23:05 +0100 Subject: [PATCH 15/95] [dlms_meter] Rename test UART package key to match directory name (#13743) --- tests/components/dlms_meter/test.esp32-ard.yaml | 2 +- tests/components/dlms_meter/test.esp32-idf.yaml | 2 +- tests/components/dlms_meter/test.esp8266-ard.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/dlms_meter/test.esp32-ard.yaml b/tests/components/dlms_meter/test.esp32-ard.yaml index 4f8a06c31b..c9910aa600 100644 --- a/tests/components/dlms_meter/test.esp32-ard.yaml +++ b/tests/components/dlms_meter/test.esp32-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml + uart_2400: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml <<: !include common-generic.yaml diff --git a/tests/components/dlms_meter/test.esp32-idf.yaml b/tests/components/dlms_meter/test.esp32-idf.yaml index f993515fce..1547532f1e 100644 --- a/tests/components/dlms_meter/test.esp32-idf.yaml +++ b/tests/components/dlms_meter/test.esp32-idf.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml + uart_2400: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml <<: !include common-netznoe.yaml diff --git a/tests/components/dlms_meter/test.esp8266-ard.yaml b/tests/components/dlms_meter/test.esp8266-ard.yaml index 2ce7955c9f..119a1978de 100644 --- a/tests/components/dlms_meter/test.esp8266-ard.yaml +++ b/tests/components/dlms_meter/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml + uart_2400: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml <<: !include common-generic.yaml From d0017ded5bc0cb62204892802986198694624ab2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 12:48:31 +0100 Subject: [PATCH 16/95] [template] Split TemplateSelect into TemplateSelectWithSetAction to save RAM (#13685) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- .../components/template/select/__init__.py | 87 +++++++++++++------ .../template/select/template_select.cpp | 73 ++++++---------- .../template/select/template_select.h | 78 +++++++++++++---- .../fixtures/select_stringref_trigger.yaml | 16 +++- .../test_select_stringref_trigger.py | 24 +++-- 5 files changed, 180 insertions(+), 98 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 574f1f5fb7..8de0b65c2d 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -10,35 +10,65 @@ from esphome.const import ( CONF_OPTIONS, CONF_RESTORE_VALUE, CONF_SET_ACTION, + CONF_UPDATE_INTERVAL, + SCHEDULER_DONT_RUN, ) +from esphome.core import TimePeriodMilliseconds +from esphome.cpp_generator import TemplateArguments from .. import template_ns TemplateSelect = template_ns.class_( "TemplateSelect", select.Select, cg.PollingComponent ) +TemplateSelectWithSetAction = template_ns.class_( + "TemplateSelectWithSetAction", TemplateSelect +) def validate(config): + errors = [] if CONF_LAMBDA in config: if config[CONF_OPTIMISTIC]: - raise cv.Invalid("optimistic cannot be used with lambda") + errors.append( + cv.Invalid( + "optimistic cannot be used with lambda", path=[CONF_OPTIMISTIC] + ) + ) if CONF_INITIAL_OPTION in config: - raise cv.Invalid("initial_value cannot be used with lambda") + errors.append( + cv.Invalid( + "initial_value cannot be used with lambda", + path=[CONF_INITIAL_OPTION], + ) + ) if CONF_RESTORE_VALUE in config: - raise cv.Invalid("restore_value cannot be used with lambda") + errors.append( + cv.Invalid( + "restore_value cannot be used with lambda", + path=[CONF_RESTORE_VALUE], + ) + ) elif CONF_INITIAL_OPTION in config: if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: - raise cv.Invalid( - f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + errors.append( + cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]", + path=[CONF_INITIAL_OPTION], + ) ) else: config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: - raise cv.Invalid( - "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + errors.append( + cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + ) ) + if errors: + raise cv.MultipleInvalid(errors) + return config @@ -62,29 +92,34 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await select.register_select(var, config, options=config[CONF_OPTIONS]) + var_id = config[CONF_ID] + if CONF_SET_ACTION in config: + var_id.type = TemplateSelectWithSetAction + has_lambda = CONF_LAMBDA in config + optimistic = config.get(CONF_OPTIMISTIC, False) + restore_value = config.get(CONF_RESTORE_VALUE, False) + options = config[CONF_OPTIONS] + initial_option = config.get(CONF_INITIAL_OPTION, 0) + initial_option_index = options.index(initial_option) if not has_lambda else 0 + + var = cg.new_Pvariable( + var_id, + TemplateArguments(has_lambda, optimistic, restore_value, initial_option_index), + ) + component_config = config.copy() + if not has_lambda: + # No point in polling if not using a lambda + component_config[CONF_UPDATE_INTERVAL] = TimePeriodMilliseconds( + milliseconds=SCHEDULER_DONT_RUN + ) + await cg.register_component(var, component_config) + await select.register_select(var, config, options=options) if CONF_LAMBDA in config: - template_ = await cg.process_lambda( + lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) ) - cg.add(var.set_template(template_)) - - else: - # Only set if non-default to avoid bloating setup() function - if config[CONF_OPTIMISTIC]: - cg.add(var.set_optimistic(True)) - initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) - # Only set if non-zero to avoid bloating setup() function - # (initial_option_index_ is zero-initialized in the header) - if initial_option_index != 0: - cg.add(var.set_initial_option_index(initial_option_index)) - - # Only set if True (default is False) - if config.get(CONF_RESTORE_VALUE): - cg.add(var.set_restore_value(True)) + cg.add(var.set_lambda(lambda_)) if CONF_SET_ACTION in config: await automation.build_automation( diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index fa34aa9fa7..e68729c2d4 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -5,61 +5,44 @@ namespace esphome::template_ { static const char *const TAG = "template.select"; -void TemplateSelect::setup() { - if (this->f_.has_value()) - return; - - size_t index = this->initial_option_index_; - if (this->restore_value_) { - this->pref_ = this->make_entity_preference(); - size_t restored_index; - if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { - index = restored_index; - ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); - } else { - ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); - } +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, + const size_t initial_option_index, bool restore_value) { + LOG_SELECT("", "Template Select", sel_comp); + if (has_lambda) { + LOG_UPDATE_INTERVAL(sel_comp); } else { - ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); + ESP_LOGCONFIG(TAG, + " Optimistic: %s\n" + " Initial Option: %s\n" + " Restore Value: %s", + YESNO(optimistic), sel_comp->option_at(initial_option_index), YESNO(restore_value)); } - - this->publish_state(index); } -void TemplateSelect::update() { - if (!this->f_.has_value()) - return; +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index) { + ESP_LOGD(TAG, "State from initial: %s", sel_comp->option_at(initial_index)); + sel_comp->publish_state(initial_index); +} - auto val = this->f_(); +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index) { + size_t index = initial_index; + if (pref.load(&index) && sel_comp->has_index(index)) { + ESP_LOGD(TAG, "State from restore: %s", sel_comp->option_at(index)); + } else { + index = initial_index; + ESP_LOGD(TAG, "State from initial (no valid stored index): %s", sel_comp->option_at(initial_index)); + } + sel_comp->publish_state(index); +} + +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val) { if (val.has_value()) { - if (!this->has_option(*val)) { + if (!sel_comp->has_option(*val)) { ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); return; } - this->publish_state(*val); + sel_comp->publish_state(*val); } } -void TemplateSelect::control(size_t index) { - this->set_trigger_->trigger(StringRef(this->option_at(index))); - - if (this->optimistic_) - this->publish_state(index); - - if (this->restore_value_) - this->pref_.save(&index); -} - -void TemplateSelect::dump_config() { - LOG_SELECT("", "Template Select", this); - LOG_UPDATE_INTERVAL(this); - if (this->f_.has_value()) - return; - ESP_LOGCONFIG(TAG, - " Optimistic: %s\n" - " Initial Option: %s\n" - " Restore Value: %s", - YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); -} - } // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 114d25b9ce..5da6d732bd 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,29 +9,71 @@ namespace esphome::template_ { -class TemplateSelect final : public select::Select, public PollingComponent { - public: - template void set_template(F &&f) { this->f_.set(std::forward(f)); } +struct Empty {}; +class BaseTemplateSelect : public select::Select, public PollingComponent {}; - void setup() override; - void update() override; - void dump_config() override; +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index, + bool restore_value); +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index); +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index); +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val); + +/// Base template select class - used when no set_action is configured + +template +class TemplateSelect : public BaseTemplateSelect { + public: + template void set_lambda(F &&f) { + if constexpr (HAS_LAMBDA) { + this->f_.set(std::forward(f)); + } + } + + void setup() override { + if constexpr (!HAS_LAMBDA) { + if constexpr (RESTORE_VALUE) { + this->pref_ = this->template make_entity_preference(); + setup_with_restore(this, this->pref_, INITIAL_OPTION_INDEX); + } else { + setup_initial(this, INITIAL_OPTION_INDEX); + } + } + } + + void update() override { + if constexpr (HAS_LAMBDA) { + update_lambda(this, this->f_()); + } + } + void dump_config() override { + dump_config_helper(this, OPTIMISTIC, HAS_LAMBDA, INITIAL_OPTION_INDEX, RESTORE_VALUE); + }; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } - void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } - void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + protected: + void control(size_t index) override { + if constexpr (OPTIMISTIC) + this->publish_state(index); + if constexpr (RESTORE_VALUE) + this->pref_.save(&index); + } + [[no_unique_address]] std::conditional_t, Empty> f_{}; + [[no_unique_address]] std::conditional_t pref_{}; +}; + +/// Template select with set_action trigger - only instantiated when set_action is configured +template +class TemplateSelectWithSetAction final + : public TemplateSelect { + public: + Trigger *get_set_trigger() { return &this->set_trigger_; } protected: - void control(size_t index) override; - bool optimistic_ = false; - size_t initial_option_index_{0}; - bool restore_value_ = false; - Trigger *set_trigger_ = new Trigger(); - TemplateLambda f_; - - ESPPreferenceObject pref_; + void control(size_t index) override { + this->set_trigger_.trigger(StringRef(this->option_at(index))); + TemplateSelect::control(index); + } + Trigger set_trigger_; }; } // namespace esphome::template_ diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml index bb1e1fd843..5858e2f529 100644 --- a/tests/integration/fixtures/select_stringref_trigger.yaml +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -56,7 +56,21 @@ select: std::string prefix = x.substr(0, 6); ESP_LOGI("test", "Substr prefix: %s", prefix.c_str()); - # Second select with numeric options to test ADL functions + # Second select with set_action trigger (uses TemplateSelectWithSetAction subclass) + - platform: template + name: "Action Select" + id: action_select + options: + - "Action A" + - "Action B" + set_action: + then: + # Test: set_action trigger receives StringRef + - logger.log: + format: "set_action triggered: %s" + args: ['x.c_str()'] + + # Third select with numeric options to test ADL functions - platform: template name: "Baud Rate" id: baud_select diff --git a/tests/integration/test_select_stringref_trigger.py b/tests/integration/test_select_stringref_trigger.py index 7fc72a2290..5baba9c7f5 100644 --- a/tests/integration/test_select_stringref_trigger.py +++ b/tests/integration/test_select_stringref_trigger.py @@ -28,6 +28,8 @@ async def test_select_stringref_trigger( find_substr_future = loop.create_future() find_char_future = loop.create_future() substr_future = loop.create_future() + # set_action trigger (TemplateSelectWithSetAction subclass) + set_action_future = loop.create_future() # ADL functions stoi_future = loop.create_future() stol_future = loop.create_future() @@ -43,6 +45,8 @@ async def test_select_stringref_trigger( find_substr_pattern = re.compile(r"Found 'Option' in value") find_char_pattern = re.compile(r"Space at position: 6") # space at index 6 substr_pattern = re.compile(r"Substr prefix: Option") + # set_action trigger pattern (TemplateSelectWithSetAction subclass) + set_action_pattern = re.compile(r"set_action triggered: Action B") # ADL function patterns (115200 from baud rate select) stoi_pattern = re.compile(r"stoi result: 115200") stol_pattern = re.compile(r"stol result: 115200") @@ -67,6 +71,9 @@ async def test_select_stringref_trigger( find_char_future.set_result(True) if not substr_future.done() and substr_pattern.search(line): substr_future.set_result(True) + # set_action trigger + if not set_action_future.done() and set_action_pattern.search(line): + set_action_future.set_result(True) # ADL functions if not stoi_future.done() and stoi_pattern.search(line): stoi_future.set_result(True) @@ -89,22 +96,21 @@ async def test_select_stringref_trigger( # List entities to find our select entities, _ = await client.list_entities_services() - select_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Test Select"), - None, - ) + select_entity = next((e for e in entities if e.name == "Test Select"), None) assert select_entity is not None, "Test Select entity not found" - baud_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"), - None, - ) + baud_entity = next((e for e in entities if e.name == "Baud Rate"), None) assert baud_entity is not None, "Baud Rate entity not found" + action_entity = next((e for e in entities if e.name == "Action Select"), None) + assert action_entity is not None, "Action Select entity not found" + # Change select to Option B - this should trigger on_value with StringRef client.select_command(select_entity.key, "Option B") # Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod) client.select_command(baud_entity.key, "115200") + # Change action select - tests set_action trigger (TemplateSelectWithSetAction) + client.select_command(action_entity.key, "Action B") # Wait for all log messages confirming StringRef operations work try: @@ -118,6 +124,7 @@ async def test_select_stringref_trigger( find_substr_future, find_char_future, substr_future, + set_action_future, stoi_future, stol_future, stof_future, @@ -135,6 +142,7 @@ async def test_select_stringref_trigger( "find_substr": find_substr_future.done(), "find_char": find_char_future.done(), "substr": substr_future.done(), + "set_action": set_action_future.done(), "stoi": stoi_future.done(), "stol": stol_future.done(), "stof": stof_future.done(), From 21bd0ff6aaa23bc32e4ff52f850da33bd4cfc5f2 Mon Sep 17 00:00:00 2001 From: Tomer Shalev Date: Tue, 3 Feb 2026 15:37:27 +0200 Subject: [PATCH 17/95] [mqtt] Stop sending deprecated color_mode and brightness in light discovery (fixes #13666) (#13667) --- esphome/components/mqtt/mqtt_light.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 3e3537fa5c..aa47bdf996 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -47,7 +47,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery root[ESPHOME_F("schema")] = ESPHOME_F("json"); auto traits = this->state_->get_traits(); - root[MQTT_COLOR_MODE] = true; // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson JsonArray color_modes = root[ESPHOME_F("supported_color_modes")].to(); if (traits.supports_color_mode(ColorMode::ON_OFF)) @@ -68,10 +67,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) color_modes.add(ESPHOME_F("rgbww")); - // legacy API - if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) - root[ESPHOME_F("brightness")] = true; - if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { root[MQTT_MIN_MIREDS] = traits.get_min_mireds(); From 8d0ce49eb4b6d25761d59442f890e95705641527 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:34:15 +0100 Subject: [PATCH 18/95] [api] Eliminate intermediate buffers in protobuf dump helpers (#13742) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_pb2_dump.cpp | 27 ++++++------------------- esphome/components/api/proto.h | 14 +++++++++++++ script/api_protobuf/api_protobuf.py | 27 ++++++------------------- 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 29121f05e0..e9db36ae21 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -23,15 +23,8 @@ static inline void append_field_prefix(DumpBuffer &out, const char *field_name, out.append(indent, ' ').append(field_name).append(": "); } -static inline void append_with_newline(DumpBuffer &out, const char *str) { - out.append(str); - out.append("\n"); -} - static inline void append_uint(DumpBuffer &out, uint32_t value) { - char buf[16]; - snprintf(buf, sizeof(buf), "%" PRIu32, value); - out.append(buf); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value)); } // RAII helper for message dump formatting @@ -49,31 +42,23 @@ class MessageDumpHelper { // Helper functions to reduce code duplication in dump methods static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRId32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%g", value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu64, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) { @@ -112,7 +97,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); - append_with_newline(out, hex_buf); + out.append(hex_buf).append("\n"); } template<> const char *proto_enum_to_string(enums::EntityCategory value) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 2e0df297c3..552b4a4625 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -402,6 +402,20 @@ class DumpBuffer { const char *c_str() const { return buf_; } size_t size() const { return pos_; } + /// Get writable buffer pointer for use with buf_append_printf + char *data() { return buf_; } + /// Get current position for use with buf_append_printf + size_t pos() const { return pos_; } + /// Update position after buf_append_printf call + void set_pos(size_t pos) { + if (pos >= CAPACITY) { + pos_ = CAPACITY - 1; + } else { + pos_ = pos; + } + buf_[pos_] = '\0'; + } + private: void append_impl_(const char *str, size_t len) { size_t space = CAPACITY - 1 - pos_; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7625458f9f..72103285e8 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2599,15 +2599,8 @@ static inline void append_field_prefix(DumpBuffer &out, const char *field_name, out.append(indent, ' ').append(field_name).append(": "); } -static inline void append_with_newline(DumpBuffer &out, const char *str) { - out.append(str); - out.append("\\n"); -} - static inline void append_uint(DumpBuffer &out, uint32_t value) { - char buf[16]; - snprintf(buf, sizeof(buf), "%" PRIu32, value); - out.append(buf); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value)); } // RAII helper for message dump formatting @@ -2625,31 +2618,23 @@ class MessageDumpHelper { // Helper functions to reduce code duplication in dump methods static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRId32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%g", value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu64, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) { @@ -2689,7 +2674,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); - append_with_newline(out, hex_buf); + out.append(hex_buf).append("\\n"); } """ From 18f7e0e6b33526821b99cefd0b9575dbe68eee6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:42:45 +0100 Subject: [PATCH 19/95] [pulse_counter][hlw8012] Fix ESP-IDF build by re-enabling legacy driver component (#13747) --- esphome/components/hlw8012/sensor.py | 8 ++++++++ .../components/pulse_counter/pulse_counter_sensor.h | 4 ++++ esphome/components/pulse_counter/sensor.py | 10 +++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 201ea4451f..4727877633 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHANGE_MODE_EVERY, @@ -25,6 +26,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.core import CORE AUTO_LOAD = ["pulse_counter"] @@ -91,6 +93,12 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): + if CORE.is_esp32: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h + # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) + include_builtin_idf_component("driver") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 5ba59cca2a..f906e9e5cb 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -6,6 +6,10 @@ #include +// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h) +// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the +// "driver" IDF component dependency. See: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6 #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #include #define HAS_PCNT diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index dbf67fd2ad..65be5ee793 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_COUNT_MODE, @@ -126,7 +127,14 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = await sensor.new_sensor(config, config.get(CONF_USE_PCNT)) + use_pcnt = config.get(CONF_USE_PCNT) + if CORE.is_esp32 and use_pcnt: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # Provides driver/pcnt.h header for hardware pulse counter API + # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) + include_builtin_idf_component("driver") + + var = await sensor.new_sensor(config, use_pcnt) await cg.register_component(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) From b8b072cf8643dd8f0741d6657a92c99f7fcf2e92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:43:27 +0100 Subject: [PATCH 20/95] [web_server_idf] Add const char* overloads for getParam/hasParam to avoid temporary string allocations (#13746) --- esphome/components/web_server_idf/utils.cpp | 13 +++++-------- esphome/components/web_server_idf/utils.h | 5 ++++- .../components/web_server_idf/web_server_idf.cpp | 6 +++--- esphome/components/web_server_idf/web_server_idf.h | 11 ++++++++--- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index f27814062c..d3c3c3dc55 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -73,18 +73,15 @@ optional request_get_url_query(httpd_req_t *req) { return {str}; } -optional query_key_value(const std::string &query_url, const std::string &key) { - if (query_url.empty()) { +optional query_key_value(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { return {}; } - auto val = std::unique_ptr(new char[query_url.size()]); - if (!val) { - ESP_LOGE(TAG, "Not enough memory to the query key value"); - return {}; - } + // Use stack buffer for typical query strings, heap fallback for large ones + SmallBufferWithHeapFallback<256, char> val(query_len); - if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) { + if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 3a86aec7ac..ae58f82398 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -15,7 +15,10 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); optional request_get_url_query(httpd_req_t *req); -optional query_key_value(const std::string &query_url, const std::string &key); +optional query_key_value(const char *query_url, size_t query_len, const char *key); +inline optional query_key_value(const std::string &query_url, const std::string &key) { + return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); +} // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 2e5a74cbef..9860810452 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -366,7 +366,7 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { } #endif -AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { +AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { // Check cache first - only successful lookups are cached for (auto *param : this->params_) { if (param->name() == name) { @@ -375,11 +375,11 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { } // Look up value from query strings - optional val = query_key_value(this->post_query_, name); + optional val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); if (!val.has_value()) { auto url_query = request_get_url_query(*this); if (url_query.has_value()) { - val = query_key_value(url_query.value(), name); + val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); } } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index e38913ef4a..817f47da79 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -162,19 +162,24 @@ class AsyncWebServerRequest { } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; } + bool hasParam(const char *name) { return this->getParam(name) != nullptr; } // NOLINTNEXTLINE(readability-identifier-naming) - AsyncWebParameter *getParam(const std::string &name); + bool hasParam(const std::string &name) { return this->getParam(name.c_str()) != nullptr; } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const char *name); + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const std::string &name) { + std::string arg(const char *name) { auto *param = this->getParam(name); if (param) { return param->value(); } return {}; } + std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } optional get_header(const char *name) const; From 5d4bde98dcd251fc2762ca14356977bba0310fa1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:56:48 +0100 Subject: [PATCH 21/95] [mqtt] Refactor state publishing with dedicated enum-to-string helpers (#13544) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../mqtt/mqtt_alarm_control_panel.cpp | 65 ++--- esphome/components/mqtt/mqtt_climate.cpp | 269 ++++++++---------- esphome/components/mqtt/mqtt_component.cpp | 17 ++ esphome/components/mqtt/mqtt_component.h | 33 +++ esphome/components/mqtt/mqtt_cover.cpp | 25 +- esphome/components/mqtt/mqtt_fan.cpp | 16 +- esphome/components/mqtt/mqtt_valve.cpp | 25 +- 7 files changed, 253 insertions(+), 197 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 263e554778..dc8f75d8f5 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -13,6 +13,33 @@ static const char *const TAG = "mqtt.alarm_control_panel"; using namespace esphome::alarm_control_panel; +static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_DISARMED: + return ESPHOME_F("disarmed"); + case ACP_STATE_ARMED_HOME: + return ESPHOME_F("armed_home"); + case ACP_STATE_ARMED_AWAY: + return ESPHOME_F("armed_away"); + case ACP_STATE_ARMED_NIGHT: + return ESPHOME_F("armed_night"); + case ACP_STATE_ARMED_VACATION: + return ESPHOME_F("armed_vacation"); + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return ESPHOME_F("armed_custom_bypass"); + case ACP_STATE_PENDING: + return ESPHOME_F("pending"); + case ACP_STATE_ARMING: + return ESPHOME_F("arming"); + case ACP_STATE_DISARMING: + return ESPHOME_F("disarming"); + case ACP_STATE_TRIGGERED: + return ESPHOME_F("triggered"); + default: + return ESPHOME_F("unknown"); + } +} + MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} void MQTTAlarmControlPanelComponent::setup() { @@ -85,43 +112,9 @@ const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return th bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } bool MQTTAlarmControlPanelComponent::publish_state() { - const char *state_s; - switch (this->alarm_control_panel_->get_state()) { - case ACP_STATE_DISARMED: - state_s = "disarmed"; - break; - case ACP_STATE_ARMED_HOME: - state_s = "armed_home"; - break; - case ACP_STATE_ARMED_AWAY: - state_s = "armed_away"; - break; - case ACP_STATE_ARMED_NIGHT: - state_s = "armed_night"; - break; - case ACP_STATE_ARMED_VACATION: - state_s = "armed_vacation"; - break; - case ACP_STATE_ARMED_CUSTOM_BYPASS: - state_s = "armed_custom_bypass"; - break; - case ACP_STATE_PENDING: - state_s = "pending"; - break; - case ACP_STATE_ARMING: - state_s = "arming"; - break; - case ACP_STATE_DISARMING: - state_s = "disarming"; - break; - case ACP_STATE_TRIGGERED: - state_s = "triggered"; - break; - default: - state_s = "unknown"; - } char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - return this->publish(this->get_state_topic_to_(topic_buf), state_s); + return this->publish(this->get_state_topic_to_(topic_buf), + alarm_state_to_mqtt_str(this->alarm_control_panel_->get_state())); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 37d643f9e7..673593ef84 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -1,5 +1,6 @@ #include "mqtt_climate.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,111 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; +static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { + switch (mode) { + case CLIMATE_MODE_OFF: + return ESPHOME_F("off"); + case CLIMATE_MODE_HEAT_COOL: + return ESPHOME_F("heat_cool"); + case CLIMATE_MODE_AUTO: + return ESPHOME_F("auto"); + case CLIMATE_MODE_COOL: + return ESPHOME_F("cool"); + case CLIMATE_MODE_HEAT: + return ESPHOME_F("heat"); + case CLIMATE_MODE_FAN_ONLY: + return ESPHOME_F("fan_only"); + case CLIMATE_MODE_DRY: + return ESPHOME_F("dry"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { + switch (action) { + case CLIMATE_ACTION_OFF: + return ESPHOME_F("off"); + case CLIMATE_ACTION_COOLING: + return ESPHOME_F("cooling"); + case CLIMATE_ACTION_HEATING: + return ESPHOME_F("heating"); + case CLIMATE_ACTION_IDLE: + return ESPHOME_F("idle"); + case CLIMATE_ACTION_DRYING: + return ESPHOME_F("drying"); + case CLIMATE_ACTION_FAN: + return ESPHOME_F("fan"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) { + switch (fan_mode) { + case CLIMATE_FAN_ON: + return ESPHOME_F("on"); + case CLIMATE_FAN_OFF: + return ESPHOME_F("off"); + case CLIMATE_FAN_AUTO: + return ESPHOME_F("auto"); + case CLIMATE_FAN_LOW: + return ESPHOME_F("low"); + case CLIMATE_FAN_MEDIUM: + return ESPHOME_F("medium"); + case CLIMATE_FAN_HIGH: + return ESPHOME_F("high"); + case CLIMATE_FAN_MIDDLE: + return ESPHOME_F("middle"); + case CLIMATE_FAN_FOCUS: + return ESPHOME_F("focus"); + case CLIMATE_FAN_DIFFUSE: + return ESPHOME_F("diffuse"); + case CLIMATE_FAN_QUIET: + return ESPHOME_F("quiet"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) { + switch (swing_mode) { + case CLIMATE_SWING_OFF: + return ESPHOME_F("off"); + case CLIMATE_SWING_BOTH: + return ESPHOME_F("both"); + case CLIMATE_SWING_VERTICAL: + return ESPHOME_F("vertical"); + case CLIMATE_SWING_HORIZONTAL: + return ESPHOME_F("horizontal"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) { + switch (preset) { + case CLIMATE_PRESET_NONE: + return ESPHOME_F("none"); + case CLIMATE_PRESET_HOME: + return ESPHOME_F("home"); + case CLIMATE_PRESET_ECO: + return ESPHOME_F("eco"); + case CLIMATE_PRESET_AWAY: + return ESPHOME_F("away"); + case CLIMATE_PRESET_BOOST: + return ESPHOME_F("boost"); + case CLIMATE_PRESET_COMFORT: + return ESPHOME_F("comfort"); + case CLIMATE_PRESET_SLEEP: + return ESPHOME_F("sleep"); + case CLIMATE_PRESET_ACTIVITY: + return ESPHOME_F("activity"); + default: + return ESPHOME_F("unknown"); + } +} + void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson auto traits = this->device_->get_traits(); @@ -260,34 +366,8 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); // mode - const char *mode_s; - switch (this->device_->mode) { - case CLIMATE_MODE_OFF: - mode_s = "off"; - break; - case CLIMATE_MODE_AUTO: - mode_s = "auto"; - break; - case CLIMATE_MODE_COOL: - mode_s = "cool"; - break; - case CLIMATE_MODE_HEAT: - mode_s = "heat"; - break; - case CLIMATE_MODE_FAN_ONLY: - mode_s = "fan_only"; - break; - case CLIMATE_MODE_DRY: - mode_s = "dry"; - break; - case CLIMATE_MODE_HEAT_COOL: - mode_s = "heat_cool"; - break; - default: - mode_s = "unknown"; - } bool success = true; - if (!this->publish(this->get_mode_state_topic(), mode_s)) + if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode))) success = false; int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -327,134 +407,37 @@ bool MQTTClimateComponent::publish_state_() { } if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { - std::string payload; - if (this->device_->preset.has_value()) { - switch (this->device_->preset.value()) { - case CLIMATE_PRESET_NONE: - payload = "none"; - break; - case CLIMATE_PRESET_HOME: - payload = "home"; - break; - case CLIMATE_PRESET_AWAY: - payload = "away"; - break; - case CLIMATE_PRESET_BOOST: - payload = "boost"; - break; - case CLIMATE_PRESET_COMFORT: - payload = "comfort"; - break; - case CLIMATE_PRESET_ECO: - payload = "eco"; - break; - case CLIMATE_PRESET_SLEEP: - payload = "sleep"; - break; - case CLIMATE_PRESET_ACTIVITY: - payload = "activity"; - break; - default: - payload = "unknown"; - } - } - if (this->device_->has_custom_preset()) - payload = this->device_->get_custom_preset().c_str(); - if (!this->publish(this->get_preset_state_topic(), payload)) + if (this->device_->has_custom_preset()) { + if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset())) + success = false; + } else if (this->device_->preset.has_value()) { + if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value()))) + success = false; + } else if (!this->publish(this->get_preset_state_topic(), "")) { success = false; + } } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - const char *payload; - switch (this->device_->action) { - case CLIMATE_ACTION_OFF: - payload = "off"; - break; - case CLIMATE_ACTION_COOLING: - payload = "cooling"; - break; - case CLIMATE_ACTION_HEATING: - payload = "heating"; - break; - case CLIMATE_ACTION_IDLE: - payload = "idle"; - break; - case CLIMATE_ACTION_DRYING: - payload = "drying"; - break; - case CLIMATE_ACTION_FAN: - payload = "fan"; - break; - default: - payload = "unknown"; - } - if (!this->publish(this->get_action_state_topic(), payload)) + if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action))) success = false; } if (traits.get_supports_fan_modes()) { - std::string payload; - if (this->device_->fan_mode.has_value()) { - switch (this->device_->fan_mode.value()) { - case CLIMATE_FAN_ON: - payload = "on"; - break; - case CLIMATE_FAN_OFF: - payload = "off"; - break; - case CLIMATE_FAN_AUTO: - payload = "auto"; - break; - case CLIMATE_FAN_LOW: - payload = "low"; - break; - case CLIMATE_FAN_MEDIUM: - payload = "medium"; - break; - case CLIMATE_FAN_HIGH: - payload = "high"; - break; - case CLIMATE_FAN_MIDDLE: - payload = "middle"; - break; - case CLIMATE_FAN_FOCUS: - payload = "focus"; - break; - case CLIMATE_FAN_DIFFUSE: - payload = "diffuse"; - break; - case CLIMATE_FAN_QUIET: - payload = "quiet"; - break; - default: - payload = "unknown"; - } - } - if (this->device_->has_custom_fan_mode()) - payload = this->device_->get_custom_fan_mode().c_str(); - if (!this->publish(this->get_fan_mode_state_topic(), payload)) + if (this->device_->has_custom_fan_mode()) { + if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode())) + success = false; + } else if (this->device_->fan_mode.has_value()) { + if (!this->publish(this->get_fan_mode_state_topic(), + climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value()))) + success = false; + } else if (!this->publish(this->get_fan_mode_state_topic(), "")) { success = false; + } } if (traits.get_supports_swing_modes()) { - const char *payload; - switch (this->device_->swing_mode) { - case CLIMATE_SWING_OFF: - payload = "off"; - break; - case CLIMATE_SWING_BOTH: - payload = "both"; - break; - case CLIMATE_SWING_VERTICAL: - payload = "vertical"; - break; - case CLIMATE_SWING_HORIZONTAL: - payload = "horizontal"; - break; - default: - payload = "unknown"; - } - if (!this->publish(this->get_swing_mode_state_topic(), payload)) + if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) success = false; } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index aec6140e3f..a77afd3f4e 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -5,6 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/version.h" #include "mqtt_const.h" @@ -149,6 +150,22 @@ bool MQTTComponent::publish(const char *topic, const char *payload) { return this->publish(topic, payload, strlen(payload)); } +#ifdef USE_ESP8266 +bool MQTTComponent::publish(const std::string &topic, ProgmemStr payload) { + return this->publish(topic.c_str(), payload); +} + +bool MQTTComponent::publish(const char *topic, ProgmemStr payload) { + if (topic[0] == '\0') + return false; + // On ESP8266, ProgmemStr is __FlashStringHelper* - need to copy from flash + char buf[64]; + strncpy_P(buf, reinterpret_cast(payload), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return global_mqtt_client->publish(topic, buf, strlen(buf), this->qos_, this->retain_); +} +#endif + bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 304a2c0d0e..2cec6fda7e 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -9,6 +9,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include "mqtt_client.h" @@ -157,6 +158,15 @@ class MQTTComponent : public Component { */ bool publish(const std::string &topic, const char *payload, size_t payload_length); + /** Send a MQTT message. + * + * @param topic The topic. + * @param payload The null-terminated payload. + */ + bool publish(const std::string &topic, const char *payload) { + return this->publish(topic.c_str(), payload, strlen(payload)); + } + /** Send a MQTT message (no heap allocation for topic). * * @param topic The topic as C string. @@ -189,6 +199,29 @@ class MQTTComponent : public Component { */ bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); } +#ifdef USE_ESP8266 + /** Send a MQTT message with a PROGMEM string payload. + * + * @param topic The topic. + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(const std::string &topic, ProgmemStr payload); + + /** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic). + * + * @param topic The topic as C string. + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(const char *topic, ProgmemStr payload); + + /** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic). + * + * @param topic The topic as StringRef (for use with get_state_topic_to_()). + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(StringRef topic, ProgmemStr payload) { return this->publish(topic.c_str(), payload); } +#endif + /** Construct and send a JSON MQTT message. * * @param topic The topic. diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index d5bd13869a..50e68ecbcc 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -1,5 +1,6 @@ #include "mqtt_cover.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.cover"; using namespace esphome::cover; +static ProgmemStr cover_state_to_mqtt_str(CoverOperation operation, float position, bool supports_position) { + if (operation == COVER_OPERATION_OPENING) + return ESPHOME_F("opening"); + if (operation == COVER_OPERATION_CLOSING) + return ESPHOME_F("closing"); + if (position == COVER_CLOSED) + return ESPHOME_F("closed"); + if (position == COVER_OPEN) + return ESPHOME_F("open"); + if (supports_position) + return ESPHOME_F("open"); + return ESPHOME_F("unknown"); +} + MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {} void MQTTCoverComponent::setup() { auto traits = this->cover_->get_traits(); @@ -109,14 +124,10 @@ bool MQTTCoverComponent::publish_state() { if (!this->publish(this->get_tilt_state_topic(), pos, len)) success = false; } - const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening" - : this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing" - : this->cover_->position == COVER_CLOSED ? "closed" - : this->cover_->position == COVER_OPEN ? "open" - : traits.get_supports_position() ? "open" - : "unknown"; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) + if (!this->publish(this->get_state_topic_to_(topic_buf), + cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, + traits.get_supports_position()))) success = false; return success; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index c9791fb0f1..84d51895c5 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -1,5 +1,6 @@ #include "mqtt_fan.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,14 @@ static const char *const TAG = "mqtt.fan"; using namespace esphome::fan; +static ProgmemStr fan_direction_to_mqtt_str(FanDirection direction) { + return direction == FanDirection::FORWARD ? ESPHOME_F("forward") : ESPHOME_F("reverse"); +} + +static ProgmemStr fan_oscillation_to_mqtt_str(bool oscillating) { + return oscillating ? ESPHOME_F("oscillate_on") : ESPHOME_F("oscillate_off"); +} + MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {} Fan *MQTTFanComponent::get_state() const { return this->state_; } @@ -164,13 +173,12 @@ bool MQTTFanComponent::publish_state() { this->publish(this->get_state_topic_to_(topic_buf), state_s); bool failed = false; if (this->state_->get_traits().supports_direction()) { - bool success = this->publish(this->get_direction_state_topic(), - this->state_->direction == fan::FanDirection::FORWARD ? "forward" : "reverse"); + bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction)); failed = failed || !success; } if (this->state_->get_traits().supports_oscillation()) { - bool success = this->publish(this->get_oscillation_state_topic(), - this->state_->oscillating ? "oscillate_on" : "oscillate_off"); + bool success = + this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating)); failed = failed || !success; } auto traits = this->state_->get_traits(); diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 2e100823bf..16e25f6a8a 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -1,5 +1,6 @@ #include "mqtt_valve.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.valve"; using namespace esphome::valve; +static ProgmemStr valve_state_to_mqtt_str(ValveOperation operation, float position, bool supports_position) { + if (operation == VALVE_OPERATION_OPENING) + return ESPHOME_F("opening"); + if (operation == VALVE_OPERATION_CLOSING) + return ESPHOME_F("closing"); + if (position == VALVE_CLOSED) + return ESPHOME_F("closed"); + if (position == VALVE_OPEN) + return ESPHOME_F("open"); + if (supports_position) + return ESPHOME_F("open"); + return ESPHOME_F("unknown"); +} + MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {} void MQTTValveComponent::setup() { auto traits = this->valve_->get_traits(); @@ -78,14 +93,10 @@ bool MQTTValveComponent::publish_state() { if (!this->publish(this->get_position_state_topic(), pos, len)) success = false; } - const char *state_s = this->valve_->current_operation == VALVE_OPERATION_OPENING ? "opening" - : this->valve_->current_operation == VALVE_OPERATION_CLOSING ? "closing" - : this->valve_->position == VALVE_CLOSED ? "closed" - : this->valve_->position == VALVE_OPEN ? "open" - : traits.get_supports_position() ? "open" - : "unknown"; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) + if (!this->publish(this->get_state_topic_to_(topic_buf), + valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position, + traits.get_supports_position()))) success = false; return success; } From f11b8615dab566c1e6e84264b84aaf618ff8c787 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 17:03:02 +0100 Subject: [PATCH 22/95] [cse7766] Fix power reading stuck when load switches off (#13734) --- esphome/components/cse7766/cse7766.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 4432195365..c36e57c929 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -152,6 +152,10 @@ void CSE7766Component::parse_data_() { if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); } + } else if (this->power_sensor_ != nullptr) { + // No valid power measurement from chip - publish 0W to avoid stale readings + // This typically happens when current is below the measurable threshold (~50mA) + this->power_sensor_->publish_state(0.0f); } float current = 0.0f; From e6bae1a97e863abd4fe82e4901b52daa431b154c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:16:13 -0500 Subject: [PATCH 23/95] [adc] Add ESP32-C2 support for curve fitting calibration (#13749) Co-authored-by: Claude Opus 4.5 --- esphome/components/adc/adc_sensor_esp32.cpp | 42 ++++++++++++--------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ea1263db5f..ece45f3746 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -42,11 +42,11 @@ void ADCSensor::setup() { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize init_config.unit_id = this->adc_unit_; init_config.ulp_mode = ADC_ULP_MODE_DISABLE; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || - // USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#endif // USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || + // USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); if (err != ESP_OK) { ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); @@ -74,8 +74,9 @@ void ADCSensor::setup() { if (this->calibration_handle_ == nullptr) { adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 // RISC-V variants and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) @@ -112,7 +113,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 } this->setup_flags_.init_complete = true; @@ -184,12 +185,13 @@ float ADCSensor::sample_fixed_attenuation_() { } else { ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); if (this->calibration_handle_ != nullptr) { -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 this->calibration_handle_ = nullptr; } } @@ -217,8 +219,9 @@ float ADCSensor::sample_autorange_() { // Need to recalibrate for the new attenuation if (this->calibration_handle_ != nullptr) { // Delete old calibration handle -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else adc_cali_delete_scheme_line_fitting(this->calibration_handle_); @@ -229,8 +232,9 @@ float ADCSensor::sample_autorange_() { // Create new calibration handle for this attenuation adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_curve_fitting_config_t cali_config = {}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -264,8 +268,9 @@ float ADCSensor::sample_autorange_() { if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); if (handle != nullptr) { -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); @@ -286,8 +291,9 @@ float ADCSensor::sample_autorange_() { ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); From 95f39149d76023fee4317fdc416efa6994ef214f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:28:59 -0500 Subject: [PATCH 24/95] [rtttl] Fix dotted note parsing order to match RTTTL spec (#13722) Co-authored-by: Claude Opus 4.5 --- esphome/components/rtttl/rtttl.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 65fcc207d4..c179282c50 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -290,25 +290,26 @@ void Rtttl::loop() { this->position_++; } - // now, get optional '.' dotted note - if (this->rtttl_[this->position_] == '.') { - this->note_duration_ += this->note_duration_ / 2; - this->position_++; - } - // now, get scale uint8_t scale = get_integer_(); - if (scale == 0) + if (scale == 0) { scale = this->default_octave_; + } if (scale < 4 || scale > 7) { ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale); this->finish_(); return; } - bool need_note_gap = false; + + // now, get optional '.' dotted note + if (this->rtttl_[this->position_] == '.') { + this->note_duration_ += this->note_duration_ / 2; + this->position_++; + } // Now play the note + bool need_note_gap = false; if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { From 2541ec15656e79d44501605be72c18edac66e78f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Feb 2026 09:42:13 +0100 Subject: [PATCH 25/95] [wifi] Fix wifi.connected condition returning false in connect state listener automations (#13733) --- esphome/components/wifi/wifi_component.cpp | 21 +++++++++++++++++++ esphome/components/wifi/wifi_component.h | 15 +++++++++++++ .../wifi/wifi_component_esp8266.cpp | 10 ++++++--- .../wifi/wifi_component_esp_idf.cpp | 9 +++++--- .../wifi/wifi_component_libretiny.cpp | 11 ++++++---- .../components/wifi/wifi_component_pico_w.cpp | 12 ++++++----- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c4bfdf3c42..e9b78c9225 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1464,6 +1464,12 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Notify listeners now that state machine has reached STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->notify_connect_state_listeners_(); +#endif + return; } @@ -2183,6 +2189,21 @@ void WiFiComponent::release_scan_results_() { } } +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +void WiFiComponent::notify_connect_state_listeners_() { + if (!this->pending_.connect_state) + return; + this->pending_.connect_state = false; + // Get current SSID and BSSID from the WiFi driver + char ssid_buf[SSID_BUFFER_SIZE]; + const char *ssid = this->wifi_ssid_to(ssid_buf); + bssid_t bssid = this->wifi_bssid(); + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); + } +} +#endif // USE_WIFI_CONNECT_STATE_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4bdc253f66..98f339809a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -632,6 +632,11 @@ class WiFiComponent : public Component { /// Free scan results memory unless a component needs them void release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) + void notify_connect_state_listeners_(); +#endif + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -739,6 +744,16 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Pending listener notifications deferred until state machine reaches appropriate state. + // Listeners are notified after state transitions complete so conditions like + // wifi.connected return correct values in automations. + // Uses bitfields to minimize memory; more flags may be added as needed. + struct { + bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED + } pending_{}; +#endif + #ifdef USE_WIFI_CONNECT_TRIGGER Trigger<> connect_trigger_; #endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c714afaad3..c6bd40037d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -500,6 +500,10 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { } } +// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). +// All listener notifications should be deferred to wifi_loop_() via pending_ flags +// to avoid stack overflow. Currently only connect_state is deferred; disconnect, +// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { @@ -512,9 +516,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + global_wifi_component->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a32232a758..22bf4be483 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -710,6 +710,9 @@ void WiFiComponent::wifi_loop_() { delete data; // NOLINT(cppcoreguidelines-owning-memory) } } +// Events are processed from queue in main loop context, but listener notifications +// must be deferred until after the state machine transitions (in check_connecting_finished) +// so that conditions like wifi.connected return correct values in automations. void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { @@ -743,9 +746,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index af2b82c3c6..285a520ef5 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -423,7 +423,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } } -// Process a single event from the queue - runs in main loop context +// Process a single event from the queue - runs in main loop context. +// Listener notifications must be deferred until after the state machine transitions +// (in check_connecting_finished) so that conditions like wifi.connected return +// correct values in automations. void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { switch (event->event_id) { case ESPHOME_EVENT_ID_WIFI_READY: { @@ -456,9 +459,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // This matches ESP32 IDF behavior where s_sta_connected is set but // wifi_sta_connect_status_() also checks got_ipv4_address_ #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 84c10d5d43..1ce36c2d93 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -252,6 +252,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } +// Pico W uses polling for connection state detection. +// Connect state listener notifications are deferred until after the state machine +// transitions (in check_connecting_finished) so that conditions like wifi.connected +// return correct values in automations. void WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { @@ -278,11 +282,9 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - String ssid = WiFi.SSID(); - bssid_t bssid = this->wifi_bssid(); - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, notify IP listeners immediately as the IP is already configured #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) From 4d05cd30592ab23208953ad8a1c6c277dab34cc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:24:05 +0000 Subject: [PATCH 26/95] Bump ruff from 0.14.14 to 0.15.0 (#13752) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- esphome/components/i2c/__init__.py | 2 +- esphome/components/lvgl/automation.py | 8 ++---- esphome/components/opentherm/generate.py | 4 ++- esphome/components/zephyr/__init__.py | 7 +++-- esphome/config_validation.py | 2 +- esphome/enum.py | 18 ++---------- esphome/wizard.py | 2 +- requirements_test.txt | 2 +- script/api_protobuf/api_protobuf.py | 2 +- script/merge_component_configs.py | 2 +- tests/integration/test_script_queued.py | 8 ++++-- tests/script/test_helpers.py | 4 +-- tests/unit_tests/core/test_config.py | 26 ++++++++++------- tests/unit_tests/test_writer.py | 36 ++++++++++++------------ 15 files changed, 60 insertions(+), 65 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b068673ecf..991e053d5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.14 + rev: v0.15.0 hooks: # Run the linter. - id: ruff diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 19efda0b49..de3f2be674 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -183,7 +183,7 @@ async def to_code(config): if CORE.using_zephyr: zephyr_add_prj_conf("I2C", True) i2c = "i2c0" - if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + if zephyr_data()[KEY_BOARD] == "xiao_ble": i2c = "i2c1" zephyr_add_overlay( f""" diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 9b58727f2a..b589e42f3b 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -272,9 +272,7 @@ async def obj_hide_to_code(config, action_id, template_arg, args): async def do_hide(widget: Widget): widget.add_flag("LV_OBJ_FLAG_HIDDEN") - widgets = [ - widget.outer if widget.outer else widget for widget in await get_widgets(config) - ] + widgets = [widget.outer or widget for widget in await get_widgets(config)] return await action_to_code(widgets, do_hide, action_id, template_arg, args) @@ -285,9 +283,7 @@ async def obj_show_to_code(config, action_id, template_arg, args): if widget.move_to_foreground: lv_obj.move_foreground(widget.obj) - widgets = [ - widget.outer if widget.outer else widget for widget in await get_widgets(config) - ] + widgets = [widget.outer or widget for widget in await get_widgets(config)] return await action_to_code(widgets, do_show, action_id, template_arg, args) diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index 4e6f3b0a12..0b39895798 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -31,7 +31,9 @@ def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> N cg.RawExpression( " sep ".join( map( - lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})", + lambda key: ( + f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})" + ), keys, ) ) diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 8e3ae86bbe..43d5cebebb 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -213,9 +213,10 @@ def copy_files(): zephyr_data()[KEY_OVERLAY], ) - if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[ - KEY_BOARD - ] in ["xiao_ble"]: + if ( + zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT + or zephyr_data()[KEY_BOARD] == "xiao_ble" + ): fake_board_manifest = """ { "frameworks": [ diff --git a/esphome/config_validation.py b/esphome/config_validation.py index b7ab02013d..55e13a7050 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -682,7 +682,7 @@ def only_with_framework( def validator_(obj): if CORE.target_framework not in frameworks: err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" - if suggestion := suggestions.get(CORE.target_framework, None): + if suggestion := suggestions.get(CORE.target_framework): (component, docs_path) = suggestion err_str += f"\nPlease use '{component}'" if docs_path: diff --git a/esphome/enum.py b/esphome/enum.py index 0fe30cf92a..cf0d8b645b 100644 --- a/esphome/enum.py +++ b/esphome/enum.py @@ -2,19 +2,7 @@ from __future__ import annotations -from enum import Enum -from typing import Any +from enum import StrEnum as _StrEnum - -class StrEnum(str, Enum): - """Partial backport of Python 3.11's StrEnum for our basic use cases.""" - - def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: - """Create a new StrEnum instance.""" - if not isinstance(value, str): - raise TypeError(f"{value!r} is not a string") - return super().__new__(cls, value, *args, **kwargs) - - def __str__(self) -> str: - """Return self.value.""" - return str(self.value) +# Re-export StrEnum from standard library for backwards compatibility +StrEnum = _StrEnum diff --git a/esphome/wizard.py b/esphome/wizard.py index f5e8a1e462..4b74847996 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -470,7 +470,7 @@ def wizard(path: Path) -> int: sleep(1) # Do not create wifi if the board does not support it - if board not in ["rpipico"]: + if board != "rpipico": safe_print_step(3, WIFI_BIG) safe_print("In this step, I'm going to create the configuration for WiFi.") safe_print() diff --git a/requirements_test.txt b/requirements_test.txt index 5d90764021..2cf6f6456e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.14 # also change in .pre-commit-config.yaml when updating +ruff==0.15.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 72103285e8..8baf6acf11 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -280,7 +280,7 @@ class TypeInfo(ABC): """ field_id_size = self.calculate_field_id_size() method = f"{base_method}_force" if force else base_method - value = value_expr if value_expr else name + value = value_expr or name return f"size.{method}({field_id_size}, {value});" @abstractmethod diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 59774edba9..5e98f1fef5 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -249,7 +249,7 @@ def merge_component_configs( if all_packages is None: # First component - initialize package dict - all_packages = comp_packages if comp_packages else {} + all_packages = comp_packages or {} elif comp_packages: # Merge packages - combine all unique package types # If both have the same package type, verify they're identical diff --git a/tests/integration/test_script_queued.py b/tests/integration/test_script_queued.py index c86c289719..84c7f950b6 100644 --- a/tests/integration/test_script_queued.py +++ b/tests/integration/test_script_queued.py @@ -98,9 +98,11 @@ async def test_script_queued( if not test3_complete.done(): loop.call_later( 0.3, - lambda: test3_complete.set_result(True) - if not test3_complete.done() - else None, + lambda: ( + test3_complete.set_result(True) + if not test3_complete.done() + else None + ), ) # Test 4: Rejection diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index c51273f298..7e60ba41fc 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1011,8 +1011,8 @@ def test_get_all_dependencies_handles_missing_components() -> None: comp.dependencies = ["missing_comp"] comp.auto_load = [] - mock_get_component.side_effect = ( - lambda name: comp if name == "existing" else None + mock_get_component.side_effect = lambda name: ( + comp if name == "existing" else None ) result = helpers.get_all_dependencies({"existing", "nonexistent"}) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index ab7bdbb98c..88801a9ca0 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -453,11 +453,14 @@ def test_preload_core_config_no_platform(setup_core: Path) -> None: # Mock _is_target_platform to avoid expensive component loading with patch("esphome.core.config._is_target_platform") as mock_is_platform: # Return True for known platforms - mock_is_platform.side_effect = lambda name: name in [ - "esp32", - "esp8266", - "rp2040", - ] + mock_is_platform.side_effect = lambda name: ( + name + in [ + "esp32", + "esp8266", + "rp2040", + ] + ) with pytest.raises(cv.Invalid, match="Platform missing"): preload_core_config(config, result) @@ -477,11 +480,14 @@ def test_preload_core_config_multiple_platforms(setup_core: Path) -> None: # Mock _is_target_platform to avoid expensive component loading with patch("esphome.core.config._is_target_platform") as mock_is_platform: # Return True for known platforms - mock_is_platform.side_effect = lambda name: name in [ - "esp32", - "esp8266", - "rp2040", - ] + mock_is_platform.side_effect = lambda name: ( + name + in [ + "esp32", + "esp8266", + "rp2040", + ] + ) with pytest.raises(cv.Invalid, match="Found multiple target platform blocks"): preload_core_config(config, result) diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index ac05e0d31b..134b63df4a 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -466,8 +466,8 @@ def test_clean_build( ) as mock_get_instance: mock_config = MagicMock() mock_get_instance.return_value = mock_config - mock_config.get.side_effect = ( - lambda section, option: str(platformio_cache_dir) + mock_config.get.side_effect = lambda section, option: ( + str(platformio_cache_dir) if (section, option) == ("platformio", "cache_dir") else "" ) @@ -630,8 +630,8 @@ def test_clean_build_empty_cache_dir( ) as mock_get_instance: mock_config = MagicMock() mock_get_instance.return_value = mock_config - mock_config.get.side_effect = ( - lambda section, option: " " # Whitespace only + mock_config.get.side_effect = lambda section, option: ( + " " # Whitespace only if (section, option) == ("platformio", "cache_dir") else "" ) @@ -1574,8 +1574,8 @@ def test_copy_src_tree_writes_build_info_files( mock_component.resources = mock_resources # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "Test comment" @@ -1649,8 +1649,8 @@ def test_copy_src_tree_detects_config_hash_change( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF # Different from existing mock_core.comment = "" @@ -1711,8 +1711,8 @@ def test_copy_src_tree_detects_version_change( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1761,8 +1761,8 @@ def test_copy_src_tree_handles_invalid_build_info_json( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1835,8 +1835,8 @@ def test_copy_src_tree_build_info_timestamp_behavior( mock_component.resources = mock_resources # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1930,8 +1930,8 @@ def test_copy_src_tree_detects_removed_source_file( existing_file.write_text("// test file") # Setup mocks - no components, so the file should be removed - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1992,8 +1992,8 @@ def test_copy_src_tree_ignores_removed_generated_file( build_info_h.write_text("// old generated file") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" From 5dc8bfe95efe0a6efd21cbed855b724d05b469ef Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 4 Feb 2026 01:29:27 -0800 Subject: [PATCH 27/95] [ultrasonic] adjust timeouts and bring the parameter back (#13738) Co-authored-by: Samuel Sieb --- CODEOWNERS | 2 +- esphome/components/ultrasonic/__init__.py | 2 +- esphome/components/ultrasonic/sensor.py | 11 +--- .../ultrasonic/ultrasonic_sensor.cpp | 56 ++++++++++++++----- .../components/ultrasonic/ultrasonic_sensor.h | 5 ++ 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1d165a6f57..25e6dc1b29 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -532,7 +532,7 @@ esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli -esphome/components/ultrasonic/* @OttoWinter +esphome/components/ultrasonic/* @ssieb @swoboda1337 esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon esphome/components/usb_cdc_acm/* @kbx81 diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index 71a87b6ae5..3bca12bffc 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@swoboda1337", "@ssieb"] diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 4b04ee7578..fad4e6b11d 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -34,7 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_TIMEOUT): cv.distance, + cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, @@ -52,12 +52,5 @@ async def to_code(config): cg.add(var.set_trigger_pin(trigger)) echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN]) cg.add(var.set_echo_pin(echo)) - - # Remove before 2026.8.0 - if CONF_TIMEOUT in config: - _LOGGER.warning( - "'timeout' option is deprecated and will be removed in 2026.8.0. " - "The option has no effect and can be safely removed." - ) - + cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2))) cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME])) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 369a10edbd..d3f7e69444 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,12 +6,11 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; -static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering) -static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion +static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { + if (arg->echo_pin_isr.digital_read()) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -38,6 +37,7 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); + this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -53,29 +53,55 @@ void UltrasonicSensorComponent::loop() { return; } + if (!this->store_.echo_start) { + uint32_t elapsed = micros() - this->measurement_start_us_; + if (elapsed >= START_TIMEOUT_US) { + ESP_LOGW(TAG, "'%s' - Measurement start timed out", this->name_.c_str()); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } else { + uint32_t elapsed; + if (this->store_.echo_end) { + elapsed = this->store_.echo_end_us - this->store_.echo_start_us; + } else { + elapsed = micros() - this->store_.echo_start_us; + } + if (elapsed >= this->timeout_us_) { + ESP_LOGD(TAG, "'%s' - Measurement pulse timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } + if (this->store_.echo_end) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); - float result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + float result; + if (this->store_.echo_start) { + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", + this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); + result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + } else { + ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); + result = NAN; + } this->publish_state(result); this->measurement_pending_ = false; return; } - - uint32_t elapsed = micros() - this->measurement_start_us_; - if (elapsed >= MEASUREMENT_TIMEOUT_US) { - ESP_LOGD(TAG, "'%s' - Measurement timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); - this->publish_state(NAN); - this->measurement_pending_ = false; - } } void UltrasonicSensorComponent::dump_config() { LOG_SENSOR("", "Ultrasonic Sensor", this); LOG_PIN(" Echo Pin: ", this->echo_pin_); LOG_PIN(" Trigger Pin: ", this->trigger_pin_); - ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " us", this->pulse_time_us_); + ESP_LOGCONFIG(TAG, + " Pulse time: %" PRIu32 " µs\n" + " Timeout: %" PRIu32 " µs", + this->pulse_time_us_, this->timeout_us_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index b0c00e51f0..a38737aff5 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,6 +11,8 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); + ISRInternalGPIOPin echo_pin_isr; + volatile uint32_t wait_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; @@ -29,6 +31,8 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent float get_setup_priority() const override { return setup_priority::DATA; } + /// Set the maximum time in µs to wait for the echo to return + void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } @@ -41,6 +45,7 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent ISRInternalGPIOPin trigger_pin_isr_; InternalGPIOPin *echo_pin_; UltrasonicSensorStore store_; + uint32_t timeout_us_{}; uint32_t pulse_time_us_{}; uint32_t measurement_start_us_{0}; From 5544f0d346a4d689dfe78b9d7ed5e83b1f45ad7e Mon Sep 17 00:00:00 2001 From: J0k3r2k1 <60352302+J0k3r2k1@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:33:45 +0100 Subject: [PATCH 28/95] [mipi_spi] Fix log_pin() FlashStringHelper compatibility (#13624) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/mipi_spi/mipi_spi.cpp | 39 +++++++++++++++-- esphome/components/mipi_spi/mipi_spi.h | 42 ++++--------------- .../components/mipi_spi/test.esp8266-ard.yaml | 10 +++++ 3 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 tests/components/mipi_spi/test.esp8266-ard.yaml diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 272915b4e1..90f6324511 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -1,6 +1,39 @@ #include "mipi_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace mipi_spi {} // namespace mipi_spi -} // namespace esphome +namespace esphome::mipi_spi { + +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) { + ESP_LOGCONFIG(TAG, + "MIPI_SPI Display\n" + " Model: %s\n" + " Width: %d\n" + " Height: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s\n" + " Invert colors: %s\n" + " Color order: %s\n" + " Display pixels: %d bits\n" + " Endianness: %s\n" + " SPI Mode: %d\n" + " SPI Data rate: %uMHz\n" + " SPI Bus width: %d", + model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)), + YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB", + display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast(data_rate / 1000000), + bus_width); + LOG_PIN(" CS Pin: ", cs); + LOG_PIN(" Reset Pin: ", reset); + LOG_PIN(" DC Pin: ", dc); + if (offset_width != 0) + ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width); + if (offset_height != 0) + ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height); + if (brightness.has_value()) + ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value()); +} + +} // namespace esphome::mipi_spi diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index a59cb8104b..083ff9507f 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -63,6 +63,11 @@ enum BusType { BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer }; +// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width); + /** * Base class for MIPI SPI displays. * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. @@ -201,40 +206,9 @@ class MipiSpi : public display::Display, } void dump_config() override { - esph_log_config(TAG, - "MIPI_SPI Display\n" - " Model: %s\n" - " Width: %u\n" - " Height: %u", - this->model_, WIDTH, HEIGHT); - if constexpr (OFFSET_WIDTH != 0) - esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH); - if constexpr (OFFSET_HEIGHT != 0) - esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT); - esph_log_config(TAG, - " Swap X/Y: %s\n" - " Mirror X: %s\n" - " Mirror Y: %s\n" - " Invert colors: %s\n" - " Color order: %s\n" - " Display pixels: %d bits\n" - " Endianness: %s\n", - YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), - YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_), - this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); - if (this->brightness_.has_value()) - esph_log_config(TAG, " Brightness: %u", this->brightness_.value()); - if (this->cs_ != nullptr) - esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str()); - if (this->reset_pin_ != nullptr) - esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str()); - if (this->dc_pin_ != nullptr) - esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str()); - esph_log_config(TAG, - " SPI Mode: %d\n" - " SPI Data rate: %dMHz\n" - " SPI Bus width: %d", - this->mode_, static_cast(this->data_rate_ / 1000000), BUS_TYPE); + internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_, + DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, + this->mode_, this->data_rate_, BUS_TYPE); } protected: diff --git a/tests/components/mipi_spi/test.esp8266-ard.yaml b/tests/components/mipi_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ef6197d852 --- /dev/null +++ b/tests/components/mipi_spi/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + dc_pin: GPIO15 + cs_pin: GPIO5 + enable_pin: GPIO4 + reset_pin: GPIO16 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml From 8314ad9ca01b8bdf5d91f0da768836d236f87595 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:59:13 -0500 Subject: [PATCH 29/95] [max7219] Allocate buffer in constructor (#13660) Co-authored-by: Claude Opus 4.5 --- esphome/components/max7219/display.py | 3 +-- esphome/components/max7219/max7219.cpp | 17 ++++++++--------- esphome/components/max7219/max7219.h | 11 +++++------ 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index a434125148..abb20702bd 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -28,11 +28,10 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS]) await spi.register_spi_device(var, config, write_only=True) await display.register_display(var, config) - cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) cg.add(var.set_intensity(config[CONF_INTENSITY])) cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 157b317c02..d701e6fc86 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { static const char *const TAG = "max7219"; @@ -115,12 +114,14 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { }; float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) { + this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT + memset(this->buffer_, 0, this->num_chips_ * 8); +} + void MAX7219Component::setup() { this->spi_setup(); - this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT - for (uint8_t i = 0; i < this->num_chips_ * 8; i++) - this->buffer_[i] = 0; - // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); // let's use our own ASCII -> led pattern encoding @@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t intensity) { this->intensity_ = intensity; } } -void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; @@ -240,5 +240,4 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time } uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 58d871d54c..ef38628f28 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -6,8 +6,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { class MAX7219Component; @@ -17,6 +16,8 @@ class MAX7219Component : public PollingComponent, public spi::SPIDevice { public: + explicit MAX7219Component(uint8_t num_chips); + void set_writer(max7219_writer_t &&writer); void setup() override; @@ -30,7 +31,6 @@ class MAX7219Component : public PollingComponent, void display(); void set_intensity(uint8_t intensity); - void set_num_chips(uint8_t num_chips); void set_reverse(bool reverse) { this->reverse_ = reverse; }; /// Evaluate the printf-format and print the result at the given position. @@ -56,10 +56,9 @@ class MAX7219Component : public PollingComponent, uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most) bool intensity_changed_{}; // True if we need to re-send the intensity uint8_t num_chips_{1}; - uint8_t *buffer_; + uint8_t *buffer_{nullptr}; bool reverse_{false}; max7219_writer_t writer_{}; }; -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 From 49ef4e00df263618ece4ee5ffa52ca02d432c3e9 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Mon, 2 Feb 2026 10:48:08 -0500 Subject: [PATCH 30/95] [mqtt] resolve warnings related to use of ip.str() (#13719) --- esphome/components/mqtt/mqtt_backend_esp32.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index bd2d2a67b2..adba0cf004 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -139,7 +139,8 @@ class MQTTBackendESP32 final : public MQTTBackend { this->lwt_retain_ = retain; } void set_server(network::IPAddress ip, uint16_t port) final { - this->host_ = ip.str(); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + this->host_ = ip.str_to(ip_buf); this->port_ = port; } void set_server(const char *host, uint16_t port) final { From b085585461120df1b375bf14ce283c929fcd1c4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 22:22:11 +0100 Subject: [PATCH 31/95] [core] Add missing uint32_t ID overloads for defer() and cancel_defer() (#13720) --- esphome/core/component.cpp | 4 ++ esphome/core/component.h | 4 ++ .../fixtures/scheduler_numeric_id_test.yaml | 45 ++++++++++++++++++- .../test_scheduler_numeric_id_test.py | 38 +++++++++++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2f61f7d195..1c398d9ac0 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -356,6 +356,10 @@ void Component::defer(const std::string &name, std::function &&f) { // void Component::defer(const char *name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } +void Component::defer(uint32_t id, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, id, 0, std::move(f)); +} +bool Component::cancel_defer(uint32_t id) { return App.scheduler.cancel_timeout(this, id); } void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast(nullptr), timeout, std::move(f)); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 49349d4199..97f2afe1a4 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -494,11 +494,15 @@ class Component { /// Defer a callback to the next loop() call. void defer(std::function &&f); // NOLINT + /// Defer a callback with a numeric ID (zero heap allocation) + void defer(uint32_t id, std::function &&f); // NOLINT + /// Cancel a defer callback using the specified name, name must not be empty. // Remove before 2026.7.0 ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_defer(const std::string &name); // NOLINT bool cancel_defer(const char *name); // NOLINT + bool cancel_defer(uint32_t id); // NOLINT // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index bf60f2fda9..1669f026f5 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -20,6 +20,9 @@ globals: - id: retry_counter type: int initial_value: '0' + - id: defer_counter + type: int + initial_value: '0' - id: tests_done type: bool initial_value: 'false' @@ -136,11 +139,49 @@ script: App.scheduler.cancel_retry(component1, 6002U); ESP_LOGI("test", "Cancelled numeric retry 6002"); + // Test 12: defer with numeric ID (Component method) + class TestDeferComponent : public Component { + public: + void test_defer_methods() { + // Test defer with uint32_t ID - should execute on next loop + this->defer(7001U, []() { + ESP_LOGI("test", "Component numeric defer 7001 fired"); + id(defer_counter) += 1; + }); + + // Test another defer with numeric ID + this->defer(7002U, []() { + ESP_LOGI("test", "Component numeric defer 7002 fired"); + id(defer_counter) += 1; + }); + } + }; + + static TestDeferComponent test_defer_component; + test_defer_component.test_defer_methods(); + + // Test 13: cancel_defer with numeric ID (Component method) + class TestCancelDeferComponent : public Component { + public: + void test_cancel_defer() { + // Set a defer that should be cancelled + this->defer(8001U, []() { + ESP_LOGE("test", "ERROR: Numeric defer 8001 should have been cancelled"); + }); + // Cancel it immediately + bool cancelled = this->cancel_defer(8001U); + ESP_LOGI("test", "Cancelled numeric defer 8001: %s", cancelled ? "true" : "false"); + } + }; + + static TestCancelDeferComponent test_cancel_defer_component; + test_cancel_defer_component.test_cancel_defer(); + - id: report_results then: - lambda: |- - ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d", - id(timeout_counter), id(interval_counter), id(retry_counter)); + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d", + id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter)); sensor: - platform: template diff --git a/tests/integration/test_scheduler_numeric_id_test.py b/tests/integration/test_scheduler_numeric_id_test.py index 510256b9a4..c1958db685 100644 --- a/tests/integration/test_scheduler_numeric_id_test.py +++ b/tests/integration/test_scheduler_numeric_id_test.py @@ -19,6 +19,7 @@ async def test_scheduler_numeric_id_test( timeout_count = 0 interval_count = 0 retry_count = 0 + defer_count = 0 # Events for each test completion numeric_timeout_1001_fired = asyncio.Event() @@ -33,6 +34,9 @@ async def test_scheduler_numeric_id_test( max_id_timeout_fired = asyncio.Event() numeric_retry_done = asyncio.Event() numeric_retry_cancelled = asyncio.Event() + numeric_defer_7001_fired = asyncio.Event() + numeric_defer_7002_fired = asyncio.Event() + numeric_defer_cancelled = asyncio.Event() final_results_logged = asyncio.Event() # Track interval counts @@ -40,7 +44,7 @@ async def test_scheduler_numeric_id_test( numeric_retry_count = 0 def on_log_line(line: str) -> None: - nonlocal timeout_count, interval_count, retry_count + nonlocal timeout_count, interval_count, retry_count, defer_count nonlocal numeric_interval_count, numeric_retry_count # Strip ANSI color codes @@ -105,15 +109,27 @@ async def test_scheduler_numeric_id_test( elif "Cancelled numeric retry 6002" in clean_line: numeric_retry_cancelled.set() + # Check for numeric defer tests + elif "Component numeric defer 7001 fired" in clean_line: + numeric_defer_7001_fired.set() + + elif "Component numeric defer 7002 fired" in clean_line: + numeric_defer_7002_fired.set() + + elif "Cancelled numeric defer 8001: true" in clean_line: + numeric_defer_cancelled.set() + # Check for final results elif "Final results" in clean_line: match = re.search( - r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+)", clean_line + r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)", + clean_line, ) if match: timeout_count = int(match.group(1)) interval_count = int(match.group(2)) retry_count = int(match.group(3)) + defer_count = int(match.group(4)) final_results_logged.set() async with ( @@ -201,6 +217,23 @@ async def test_scheduler_numeric_id_test( "Numeric retry 6002 should have been cancelled" ) + # Wait for numeric defer tests + try: + await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7001 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(numeric_defer_7002_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7002 did not fire within 0.5 seconds") + + # Verify numeric defer was cancelled + try: + await asyncio.wait_for(numeric_defer_cancelled.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 8001 cancel confirmation not received") + # Wait for final results try: await asyncio.wait_for(final_results_logged.wait(), timeout=3.0) @@ -215,3 +248,4 @@ async def test_scheduler_numeric_id_test( assert retry_count >= 2, ( f"Expected at least 2 retry attempts, got {retry_count}" ) + assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}" From 094d64f872f9bf3856ba6fec8eb8dabf2f5114c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 03:19:38 +0100 Subject: [PATCH 32/95] [http_request] Fix requests taking full timeout when response is already complete (#13649) --- .../update/esp32_hosted_update.cpp | 14 ++++- .../components/http_request/http_request.h | 55 ++++++++++++++----- .../http_request/http_request_arduino.cpp | 20 ++++++- .../http_request/http_request_idf.cpp | 9 +-- .../http_request/ota/ota_http_request.cpp | 6 +- 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 0e0778dd23..7ffa61fc97 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -193,11 +193,14 @@ bool Esp32HostedUpdate::fetch_manifest_() { int read_or_error = container->read(buf, sizeof(buf)); App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. if (result != http_request::HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT json_str.append(reinterpret_cast(buf), read_or_error); } container->end(); @@ -318,9 +321,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. + if (result == http_request::HttpReadLoopResult::COMPLETE) + break; if (result != http_request::HttpReadLoopResult::DATA) { if (result == http_request::HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading firmware data"); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index fb39ca504c..f3c99c6de4 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -26,6 +26,7 @@ struct Header { enum HttpStatus { HTTP_STATUS_OK = 200, HTTP_STATUS_NO_CONTENT = 204, + HTTP_STATUS_RESET_CONTENT = 205, HTTP_STATUS_PARTIAL_CONTENT = 206, /* 3xx - Redirection */ @@ -126,19 +127,21 @@ struct HttpReadResult { /// Result of processing a non-blocking read with timeout (for manual loops) enum class HttpReadLoopResult : uint8_t { - DATA, ///< Data was read, process it - RETRY, ///< No data yet, already delayed, caller should continue loop - ERROR, ///< Read error, caller should exit loop - TIMEOUT, ///< Timeout waiting for data, caller should exit loop + DATA, ///< Data was read, process it + COMPLETE, ///< All content has been read, caller should exit loop + RETRY, ///< No data yet, already delayed, caller should continue loop + ERROR, ///< Read error, caller should exit loop + TIMEOUT, ///< Timeout waiting for data, caller should exit loop }; /// Process a read result with timeout tracking and delay handling /// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error /// @param last_data_time Time of last successful read, updated when data received /// @param timeout_ms Maximum time to wait for data -/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit -inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, - uint32_t timeout_ms) { +/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete()) +/// @return How the caller should proceed - see HttpReadLoopResult enum +inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, + bool is_read_complete) { if (bytes_read_or_error > 0) { last_data_time = millis(); return HttpReadLoopResult::DATA; @@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_ if (bytes_read_or_error < 0) { return HttpReadLoopResult::ERROR; } - // bytes_read_or_error == 0: no data available yet + // bytes_read_or_error == 0: either "no data yet" or "all content read" + if (is_read_complete) { + return HttpReadLoopResult::COMPLETE; + } if (millis() - last_data_time >= timeout_ms) { return HttpReadLoopResult::TIMEOUT; } @@ -159,9 +165,9 @@ class HttpRequestComponent; class HttpContainer : public Parented { public: virtual ~HttpContainer() = default; - size_t content_length; - int status_code; - uint32_t duration_ms; + size_t content_length{0}; + int status_code{-1}; ///< -1 indicates no response received yet + uint32_t duration_ms{0}; /** * @brief Read data from the HTTP response body. @@ -194,9 +200,24 @@ class HttpContainer : public Parented { virtual void end() = 0; void set_secure(bool secure) { this->secure_ = secure; } + void set_chunked(bool chunked) { this->is_chunked_ = chunked; } size_t get_bytes_read() const { return this->bytes_read_; } + /// Check if all expected content has been read + /// For chunked responses, returns false (completion detected via read() returning error/EOF) + bool is_read_complete() const { + // Per RFC 9112, these responses have no body: + // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified + if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || + this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) { + return true; + } + // For non-chunked responses, complete when bytes_read >= content_length + // This handles both Content-Length: 0 and Content-Length: N cases + return !this->is_chunked_ && this->bytes_read_ >= this->content_length; + } + /** * @brief Get response headers. * @@ -209,6 +230,7 @@ class HttpContainer : public Parented { protected: size_t bytes_read_{0}; bool secure_{false}; + bool is_chunked_{false}; ///< True if response uses chunked transfer encoding std::map> response_headers_{}; }; @@ -219,7 +241,7 @@ class HttpContainer : public Parented { /// @param total_size Total bytes to read /// @param chunk_size Maximum bytes per read call /// @param timeout_ms Read timeout in milliseconds -/// @return HttpReadResult with status and error_code on failure +/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms) { size_t read_index = 0; @@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms); + auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + if (result == HttpReadLoopResult::COMPLETE) + break; // Server sent less data than requested, but transfer is complete if (result == HttpReadLoopResult::ERROR) return {HttpReadStatus::ERROR, read_bytes_or_error}; if (result == HttpReadLoopResult::TIMEOUT) @@ -393,11 +417,12 @@ template class HttpRequestSendAction : public Action { int read_or_error = container->read(buf + read_index, std::min(max_length - read_index, 512)); App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; if (result != HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT read_index += read_or_error; } response_body.reserve(read_index); diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 82538b2cb3..2f12b58766 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -135,9 +135,23 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the // early return check (bytes_read_ >= content_length) will never trigger. + // + // TODO: Chunked transfer encoding is NOT properly supported on Arduino. + // The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where + // esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr() + // returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead + // of decoded content. This wasn't noticed because requests would complete and payloads + // were only examined on IDF. The long transfer times were also masked by the misleading + // "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues: + // 1. Response body is corrupted - contains chunk size headers mixed with data + // 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout + // The proper fix would be to use getString() for chunked responses, which decodes chunks + // internally, but this buffers the entire response in memory. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; + // -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding + container->set_chunked(content_length == -1); container->duration_ms = millis() - start; return container; @@ -178,9 +192,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) - // For chunked encoding (content_length == SIZE_MAX), we can't use this check - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've read all expected content (non-chunked only) + // For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false + if (this->is_read_complete()) { return 0; // All content read successfully } // No data available - check if connection is still open diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 95c59aa04c..9cfa825e17 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -155,6 +155,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -190,6 +191,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -234,10 +236,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content - // Skip this check when content_length is 0 (chunked transfer encoding or unknown length) - // For chunked responses, esp_http_client_read() will return 0 when all data is received - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've already read all expected content (non-chunked only) + // For chunked responses (content_length == 0), esp_http_client_read() handles EOF + if (this->is_read_complete()) { return 0; // All content read successfully } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 6c77e75d8c..d073644a37 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout); + auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added for OTA in the future. + if (result == HttpReadLoopResult::COMPLETE) + break; if (result != HttpReadLoopResult::DATA) { if (result == HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading data"); From bc41d25657236e32b04465ae05040b8928f0448c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 17:03:02 +0100 Subject: [PATCH 33/95] [cse7766] Fix power reading stuck when load switches off (#13734) --- esphome/components/cse7766/cse7766.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 71fe15f0ae..e7bcb64f8c 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -152,6 +152,10 @@ void CSE7766Component::parse_data_() { if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); } + } else if (this->power_sensor_ != nullptr) { + // No valid power measurement from chip - publish 0W to avoid stale readings + // This typically happens when current is below the measurable threshold (~50mA) + this->power_sensor_->publish_state(0.0f); } float current = 0.0f; From 900aab45f12b7b6d590d97c7ad086743c36de078 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Feb 2026 09:42:13 +0100 Subject: [PATCH 34/95] [wifi] Fix wifi.connected condition returning false in connect state listener automations (#13733) --- esphome/components/wifi/wifi_component.cpp | 21 +++++++++++++++++++ esphome/components/wifi/wifi_component.h | 15 +++++++++++++ .../wifi/wifi_component_esp8266.cpp | 10 ++++++--- .../wifi/wifi_component_esp_idf.cpp | 9 +++++--- .../wifi/wifi_component_libretiny.cpp | 11 ++++++---- .../components/wifi/wifi_component_pico_w.cpp | 12 ++++++----- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 52d9b2b442..fd7e2c6ee6 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1383,6 +1383,12 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Notify listeners now that state machine has reached STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->notify_connect_state_listeners_(); +#endif + return; } @@ -2090,6 +2096,21 @@ void WiFiComponent::release_scan_results_() { } } +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +void WiFiComponent::notify_connect_state_listeners_() { + if (!this->pending_.connect_state) + return; + this->pending_.connect_state = false; + // Get current SSID and BSSID from the WiFi driver + char ssid_buf[SSID_BUFFER_SIZE]; + const char *ssid = this->wifi_ssid_to(ssid_buf); + bssid_t bssid = this->wifi_bssid(); + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); + } +} +#endif // USE_WIFI_CONNECT_STATE_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index dfc91fb5da..28db486b88 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -618,6 +618,11 @@ class WiFiComponent : public Component { /// Free scan results memory unless a component needs them void release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) + void notify_connect_state_listeners_(); +#endif + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -721,6 +726,16 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Pending listener notifications deferred until state machine reaches appropriate state. + // Listeners are notified after state transitions complete so conditions like + // wifi.connected return correct values in automations. + // Uses bitfields to minimize memory; more flags may be added as needed. + struct { + bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED + } pending_{}; +#endif + // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 59101439c2..41033cca19 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -500,6 +500,10 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { } } +// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). +// All listener notifications should be deferred to wifi_loop_() via pending_ flags +// to avoid stack overflow. Currently only connect_state is deferred; disconnect, +// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { @@ -512,9 +516,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + global_wifi_component->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 15fd407e3c..711d88bd68 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -710,6 +710,9 @@ void WiFiComponent::wifi_loop_() { delete data; // NOLINT(cppcoreguidelines-owning-memory) } } +// Events are processed from queue in main loop context, but listener notifications +// must be deferred until after the state machine transitions (in check_connecting_finished) +// so that conditions like wifi.connected return correct values in automations. void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { @@ -743,9 +746,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 20cd32fa8f..cddca2aa91 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -423,7 +423,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } } -// Process a single event from the queue - runs in main loop context +// Process a single event from the queue - runs in main loop context. +// Listener notifications must be deferred until after the state machine transitions +// (in check_connecting_finished) so that conditions like wifi.connected return +// correct values in automations. void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { switch (event->event_id) { case ESPHOME_EVENT_ID_WIFI_READY: { @@ -456,9 +459,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // This matches ESP32 IDF behavior where s_sta_connected is set but // wifi_sta_connect_status_() also checks got_ipv4_address_ #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 29ac096d94..c55aeef5a4 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -240,6 +240,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } +// Pico W uses polling for connection state detection. +// Connect state listener notifications are deferred until after the state machine +// transitions (in check_connecting_finished) so that conditions like wifi.connected +// return correct values in automations. void WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { @@ -264,11 +268,9 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - String ssid = WiFi.SSID(); - bssid_t bssid = this->wifi_bssid(); - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, notify IP listeners immediately as the IP is already configured #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) From bafbd4235a5ac9d631b79d64ce1a1a796fbc3d18 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 4 Feb 2026 01:29:27 -0800 Subject: [PATCH 35/95] [ultrasonic] adjust timeouts and bring the parameter back (#13738) Co-authored-by: Samuel Sieb --- CODEOWNERS | 2 +- esphome/components/ultrasonic/__init__.py | 2 +- esphome/components/ultrasonic/sensor.py | 11 +--- .../ultrasonic/ultrasonic_sensor.cpp | 56 ++++++++++++++----- .../components/ultrasonic/ultrasonic_sensor.h | 5 ++ 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8a37aeb29f..136152e6ff 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -528,7 +528,7 @@ esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli -esphome/components/ultrasonic/* @OttoWinter +esphome/components/ultrasonic/* @ssieb @swoboda1337 esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon esphome/components/usb_cdc_acm/* @kbx81 diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index 71a87b6ae5..3bca12bffc 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@swoboda1337", "@ssieb"] diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 4b04ee7578..fad4e6b11d 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -34,7 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_TIMEOUT): cv.distance, + cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, @@ -52,12 +52,5 @@ async def to_code(config): cg.add(var.set_trigger_pin(trigger)) echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN]) cg.add(var.set_echo_pin(echo)) - - # Remove before 2026.8.0 - if CONF_TIMEOUT in config: - _LOGGER.warning( - "'timeout' option is deprecated and will be removed in 2026.8.0. " - "The option has no effect and can be safely removed." - ) - + cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2))) cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME])) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 369a10edbd..d3f7e69444 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,12 +6,11 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; -static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering) -static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion +static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { + if (arg->echo_pin_isr.digital_read()) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -38,6 +37,7 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); + this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -53,29 +53,55 @@ void UltrasonicSensorComponent::loop() { return; } + if (!this->store_.echo_start) { + uint32_t elapsed = micros() - this->measurement_start_us_; + if (elapsed >= START_TIMEOUT_US) { + ESP_LOGW(TAG, "'%s' - Measurement start timed out", this->name_.c_str()); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } else { + uint32_t elapsed; + if (this->store_.echo_end) { + elapsed = this->store_.echo_end_us - this->store_.echo_start_us; + } else { + elapsed = micros() - this->store_.echo_start_us; + } + if (elapsed >= this->timeout_us_) { + ESP_LOGD(TAG, "'%s' - Measurement pulse timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } + if (this->store_.echo_end) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); - float result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + float result; + if (this->store_.echo_start) { + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", + this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); + result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + } else { + ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); + result = NAN; + } this->publish_state(result); this->measurement_pending_ = false; return; } - - uint32_t elapsed = micros() - this->measurement_start_us_; - if (elapsed >= MEASUREMENT_TIMEOUT_US) { - ESP_LOGD(TAG, "'%s' - Measurement timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); - this->publish_state(NAN); - this->measurement_pending_ = false; - } } void UltrasonicSensorComponent::dump_config() { LOG_SENSOR("", "Ultrasonic Sensor", this); LOG_PIN(" Echo Pin: ", this->echo_pin_); LOG_PIN(" Trigger Pin: ", this->trigger_pin_); - ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " us", this->pulse_time_us_); + ESP_LOGCONFIG(TAG, + " Pulse time: %" PRIu32 " µs\n" + " Timeout: %" PRIu32 " µs", + this->pulse_time_us_, this->timeout_us_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index b0c00e51f0..a38737aff5 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,6 +11,8 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); + ISRInternalGPIOPin echo_pin_isr; + volatile uint32_t wait_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; @@ -29,6 +31,8 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent float get_setup_priority() const override { return setup_priority::DATA; } + /// Set the maximum time in µs to wait for the echo to return + void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } @@ -41,6 +45,7 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent ISRInternalGPIOPin trigger_pin_isr_; InternalGPIOPin *echo_pin_; UltrasonicSensorStore store_; + uint32_t timeout_us_{}; uint32_t pulse_time_us_{}; uint32_t measurement_start_us_{0}; From 1b3c9aa98efba2235f5a406ca0752faba7c160bf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:01:32 +0100 Subject: [PATCH 36/95] Bump version to 2026.1.4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f9ffa9e25a..140ac06565 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 = 2026.1.3 +PROJECT_NUMBER = 2026.1.4 # 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 1ce6eb4ba3..862c7e37e6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.1.3" +__version__ = "2026.1.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ba18a8b3e33cabbc40772a49348423f3d73ee987 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Feb 2026 06:44:17 -0500 Subject: [PATCH 37/95] [adc] Fix ESP32-C2 ADC calibration to use line fitting (#13756) Co-authored-by: Claude Opus 4.5 --- esphome/components/adc/adc_sensor_esp32.cpp | 44 ++++++++----------- tests/components/adc/test.esp32-c2-idf.yaml | 12 +++++ .../build_components_base.esp32-c2-idf.yaml | 20 +++++++++ 3 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 tests/components/adc/test.esp32-c2-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-c2-idf.yaml diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ece45f3746..1d3138623e 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -74,10 +74,9 @@ void ADCSensor::setup() { if (this->calibration_handle_ == nullptr) { adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 - // RISC-V variants and S3 use curve fitting calibration +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 + // RISC-V variants (except C2) and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -95,14 +94,14 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#else // Other ESP32 variants use line fitting calibration +#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, .atten = this->attenuation_, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, // Default reference voltage in mV -#endif // !defined(USE_ESP32_VARIANT_ESP32S2) +#endif // !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) }; err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); if (err == ESP_OK) { @@ -113,7 +112,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 } this->setup_flags_.init_complete = true; @@ -185,13 +184,12 @@ float ADCSensor::sample_fixed_attenuation_() { } else { ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); if (this->calibration_handle_ != nullptr) { -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 this->calibration_handle_ = nullptr; } } @@ -219,9 +217,8 @@ float ADCSensor::sample_autorange_() { // Need to recalibrate for the new attenuation if (this->calibration_handle_ != nullptr) { // Delete old calibration handle -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else adc_cali_delete_scheme_line_fitting(this->calibration_handle_); @@ -232,9 +229,8 @@ float ADCSensor::sample_autorange_() { // Create new calibration handle for this attenuation adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_curve_fitting_config_t cali_config = {}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -251,7 +247,7 @@ float ADCSensor::sample_autorange_() { .unit_id = this->adc_unit_, .atten = atten, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, #endif }; @@ -268,9 +264,8 @@ float ADCSensor::sample_autorange_() { if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); if (handle != nullptr) { -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); @@ -291,9 +286,8 @@ float ADCSensor::sample_autorange_() { ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); diff --git a/tests/components/adc/test.esp32-c2-idf.yaml b/tests/components/adc/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..e764f0fe21 --- /dev/null +++ b/tests/components/adc/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +sensor: + - id: my_sensor + platform: adc + pin: GPIO1 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/test_build_components/build_components_base.esp32-c2-idf.yaml b/tests/test_build_components/build_components_base.esp32-c2-idf.yaml new file mode 100644 index 0000000000..59691be7aa --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c2-idf.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32c2idf + friendly_name: $component_name + +esp32: + board: esp32-c2-devkitm-1 + framework: + type: esp-idf + # Use custom partition table with larger app partition (3MB) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file From 36f2654fa67cdfc76741b6329e76f789671ed775 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Wed, 4 Feb 2026 17:06:59 +0100 Subject: [PATCH 38/95] [pylontech] Refactor parser to support new firmware version and SysError (#12300) --- esphome/components/pylontech/pylontech.cpp | 138 ++++++++++++++++++--- esphome/components/pylontech/pylontech.h | 7 +- 2 files changed, 122 insertions(+), 23 deletions(-) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 74b7caefb2..61b5356c90 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -2,6 +2,28 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +// Helper macros +#define PARSE_INT(field, field_name) \ + { \ + get_token(token_buf); \ + auto val = parse_number(token_buf); \ + if (val.has_value()) { \ + (field) = val.value(); \ + } else { \ + ESP_LOGD(TAG, "invalid " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \ + return; \ + } \ + } + +#define PARSE_STR(field, field_name) \ + { \ + get_token(field); \ + if (strlen(field) < 2) { \ + ESP_LOGD(TAG, "too short " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \ + return; \ + } \ + } + namespace esphome { namespace pylontech { @@ -64,33 +86,106 @@ void PylontechComponent::loop() { void PylontechComponent::process_line_(std::string &buffer) { ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str()); // clang-format off - // example line to parse: - // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St - // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // example lines to parse: + // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St + // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // 1 46012 1255 9100 5300 5500 3047 3091 SysError Low Normal Normal 4% 2025-11-28 17:56:33 Low Normal 7800 Normal + // newer firmware example: + // Power Volt Curr Tempr Tlow Tlow.Id Thigh Thigh.Id Vlow Vlow.Id Vhigh Vhigh.Id Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St SysAlarm.St + // 1 49405 0 17600 13700 8 14500 0 3293 2 3294 0 Idle Normal Normal Normal 60% 2025-12-05 00:53:41 Normal Normal 16600 Normal Normal // clang-format on PylontechListener::LineContents l{}; - char mostempr_s[6]; - const int parsed = sscanf( // NOLINT - buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT - &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT - l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT - if (l.bat_num <= 0) { - ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); - return; + const char *cursor = buffer.c_str(); + char token_buf[TEXT_SENSOR_MAX_LEN] = {0}; + + // Helper Lambda to extract tokens + auto get_token = [&](char *token_buf) -> void { + // Skip leading whitespace + while (*cursor == ' ' || *cursor == '\t') { + cursor++; + } + + if (*cursor == '\0') { + token_buf[0] = 0; + return; + } + + const char *start = cursor; + + // Find end of field + while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && *cursor != '\r') { + cursor++; + } + + size_t token_len = std::min(static_cast(cursor - start), static_cast(TEXT_SENSOR_MAX_LEN - 1)); + memcpy(token_buf, start, token_len); + token_buf[token_len] = 0; + }; + + { + get_token(token_buf); + auto val = parse_number(token_buf); + if (val.has_value() && val.value() > 0) { + l.bat_num = val.value(); + } else if (strcmp(token_buf, "Power") == 0) { + // header line i.e. "Power Volt Curr" and so on + this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos; + ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without", + buffer.substr(0, buffer.size() - 2).c_str()); + return; + } else { + ESP_LOGD(TAG, "unknown line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } } - if (parsed != 14) { - ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); - return; + PARSE_INT(l.volt, "Volt"); + PARSE_INT(l.curr, "Curr"); + PARSE_INT(l.tempr, "Tempr"); + PARSE_INT(l.tlow, "Tlow"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Tlow.Id } - auto mostempr_parsed = parse_number(mostempr_s); - if (mostempr_parsed.has_value()) { - l.mostempr = mostempr_parsed.value(); - } else { - l.mostempr = -300; - ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num); + PARSE_INT(l.thigh, "Thigh"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Thigh.Id } + PARSE_INT(l.vlow, "Vlow"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Vlow.Id + } + PARSE_INT(l.vhigh, "Vhigh"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Vhigh.Id + } + PARSE_STR(l.base_st, "Base.St"); + PARSE_STR(l.volt_st, "Volt.St"); + PARSE_STR(l.curr_st, "Curr.St"); + PARSE_STR(l.temp_st, "Temp.St"); + { + get_token(token_buf); + for (char &i : token_buf) { + if (i == '%') { + i = 0; + break; + } + } + auto coul_val = parse_number(token_buf); + if (coul_val.has_value()) { + l.coulomb = coul_val.value(); + } else { + ESP_LOGD(TAG, "invalid Coulomb in line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + } + get_token(token_buf); // Skip Date + get_token(token_buf); // Skip Time + get_token(token_buf); // Skip B.V.St + get_token(token_buf); // Skip B.T.St + PARSE_INT(l.mostempr, "Mostempr"); + + ESP_LOGD(TAG, "successful line %s", buffer.substr(0, buffer.size() - 2).c_str()); for (PylontechListener *listener : this->listeners_) { listener->on_line_read(&l); @@ -101,3 +196,6 @@ float PylontechComponent::get_setup_priority() const { return setup_priority::DA } // namespace pylontech } // namespace esphome + +#undef PARSE_INT +#undef PARSE_STR diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h index 3282cb4d9f..10c669ad9a 100644 --- a/esphome/components/pylontech/pylontech.h +++ b/esphome/components/pylontech/pylontech.h @@ -8,14 +8,14 @@ namespace esphome { namespace pylontech { static const uint8_t NUM_BUFFERS = 20; -static const uint8_t TEXT_SENSOR_MAX_LEN = 8; +static const uint8_t TEXT_SENSOR_MAX_LEN = 14; class PylontechListener { public: struct LineContents { int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr; - char base_st[TEXT_SENSOR_MAX_LEN], volt_st[TEXT_SENSOR_MAX_LEN], curr_st[TEXT_SENSOR_MAX_LEN], - temp_st[TEXT_SENSOR_MAX_LEN]; + char base_st[TEXT_SENSOR_MAX_LEN] = {0}, volt_st[TEXT_SENSOR_MAX_LEN] = {0}, curr_st[TEXT_SENSOR_MAX_LEN] = {0}, + temp_st[TEXT_SENSOR_MAX_LEN] = {0}; }; virtual void on_line_read(LineContents *line); @@ -45,6 +45,7 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice { std::string buffer_[NUM_BUFFERS]; int buffer_index_write_ = 0; int buffer_index_read_ = 0; + bool has_tlow_id_ = false; std::vector listeners_{}; }; From becb6559f143fc5c361065785fc8260bfe51bd26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Feb 2026 17:48:41 +0100 Subject: [PATCH 39/95] [components] Remove redundant setup priority overrides that duplicate default (#13745) --- esphome/components/absolute_humidity/absolute_humidity.cpp | 2 -- esphome/components/absolute_humidity/absolute_humidity.h | 1 - esphome/components/adc/adc_sensor.h | 5 ----- esphome/components/adc/adc_sensor_common.cpp | 2 -- esphome/components/adc128s102/sensor/adc128s102_sensor.cpp | 2 -- esphome/components/adc128s102/sensor/adc128s102_sensor.h | 1 - esphome/components/aht10/aht10.cpp | 2 -- esphome/components/aht10/aht10.h | 1 - esphome/components/am2315c/am2315c.cpp | 2 -- esphome/components/am2315c/am2315c.h | 1 - esphome/components/am2320/am2320.cpp | 1 - esphome/components/am2320/am2320.h | 1 - esphome/components/apds9960/apds9960.cpp | 1 - esphome/components/apds9960/apds9960.h | 1 - esphome/components/aqi/aqi_sensor.h | 1 - esphome/components/as3935/as3935.cpp | 2 -- esphome/components/as3935/as3935.h | 1 - esphome/components/as5600/sensor/as5600_sensor.cpp | 2 -- esphome/components/as5600/sensor/as5600_sensor.h | 1 - esphome/components/as7341/as7341.cpp | 2 -- esphome/components/as7341/as7341.h | 1 - esphome/components/atm90e26/atm90e26.cpp | 1 - esphome/components/atm90e26/atm90e26.h | 1 - esphome/components/bh1750/bh1750.cpp | 2 -- esphome/components/bh1750/bh1750.h | 1 - esphome/components/bme280_base/bme280_base.cpp | 1 - esphome/components/bme280_base/bme280_base.h | 1 - esphome/components/bme680/bme680.cpp | 2 -- esphome/components/bme680/bme680.h | 1 - esphome/components/bme680_bsec/bme680_bsec.cpp | 2 -- esphome/components/bme680_bsec/bme680_bsec.h | 1 - esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 2 -- esphome/components/bme68x_bsec2/bme68x_bsec2.h | 1 - esphome/components/bmi160/bmi160.cpp | 1 - esphome/components/bmi160/bmi160.h | 2 -- esphome/components/bmp085/bmp085.cpp | 1 - esphome/components/bmp085/bmp085.h | 2 -- esphome/components/bmp280_base/bmp280_base.cpp | 1 - esphome/components/bmp280_base/bmp280_base.h | 1 - esphome/components/bmp3xx_base/bmp3xx_base.cpp | 1 - esphome/components/bmp3xx_base/bmp3xx_base.h | 1 - esphome/components/cd74hc4067/cd74hc4067.cpp | 2 -- esphome/components/cd74hc4067/cd74hc4067.h | 1 - esphome/components/cm1106/cm1106.h | 2 -- esphome/components/combination/combination.h | 2 -- esphome/components/cse7761/cse7761.cpp | 2 -- esphome/components/cse7761/cse7761.h | 1 - esphome/components/cse7766/cse7766.cpp | 1 - esphome/components/cse7766/cse7766.h | 1 - esphome/components/current_based/current_based_cover.cpp | 1 - esphome/components/current_based/current_based_cover.h | 1 - esphome/components/daly_bms/daly_bms.cpp | 2 -- esphome/components/daly_bms/daly_bms.h | 1 - esphome/components/dht/dht.cpp | 2 -- esphome/components/dht/dht.h | 2 -- esphome/components/dht12/dht12.cpp | 2 +- esphome/components/dht12/dht12.h | 1 - esphome/components/dps310/dps310.cpp | 2 -- esphome/components/dps310/dps310.h | 1 - esphome/components/ds1307/ds1307.cpp | 2 -- esphome/components/ds1307/ds1307.h | 1 - esphome/components/duty_cycle/duty_cycle_sensor.cpp | 2 -- esphome/components/duty_cycle/duty_cycle_sensor.h | 1 - esphome/components/ee895/ee895.cpp | 2 -- esphome/components/ee895/ee895.h | 1 - esphome/components/emc2101/sensor/emc2101_sensor.cpp | 2 -- esphome/components/emc2101/sensor/emc2101_sensor.h | 2 -- esphome/components/endstop/endstop_cover.cpp | 2 +- esphome/components/endstop/endstop_cover.h | 1 - esphome/components/ens210/ens210.cpp | 2 -- esphome/components/ens210/ens210.h | 1 - esphome/components/esp32_camera/esp32_camera.cpp | 2 -- esphome/components/esp32_camera/esp32_camera.h | 1 - esphome/components/esp32_touch/esp32_touch.h | 1 - esphome/components/gdk101/gdk101.cpp | 2 -- esphome/components/gdk101/gdk101.h | 1 - esphome/components/gp8403/gp8403.h | 1 - esphome/components/hc8/hc8.cpp | 2 -- esphome/components/hc8/hc8.h | 2 -- esphome/components/hdc1080/hdc1080.h | 2 -- esphome/components/hlw8012/hlw8012.cpp | 1 - esphome/components/hlw8012/hlw8012.h | 1 - esphome/components/hm3301/hm3301.cpp | 2 -- esphome/components/hm3301/hm3301.h | 1 - esphome/components/hmc5883l/hmc5883l.cpp | 1 - esphome/components/hmc5883l/hmc5883l.h | 1 - .../components/homeassistant/time/homeassistant_time.cpp | 2 -- esphome/components/homeassistant/time/homeassistant_time.h | 1 - esphome/components/honeywell_hih_i2c/honeywell_hih.cpp | 2 -- esphome/components/honeywell_hih_i2c/honeywell_hih.h | 1 - esphome/components/hte501/hte501.cpp | 1 - esphome/components/hte501/hte501.h | 1 - esphome/components/htu21d/htu21d.cpp | 2 -- esphome/components/htu21d/htu21d.h | 2 -- esphome/components/htu31d/htu31d.cpp | 6 ------ esphome/components/htu31d/htu31d.h | 2 -- esphome/components/hx711/hx711.cpp | 1 - esphome/components/hx711/hx711.h | 1 - esphome/components/hydreon_rgxx/hydreon_rgxx.cpp | 2 -- esphome/components/hydreon_rgxx/hydreon_rgxx.h | 2 -- esphome/components/hyt271/hyt271.cpp | 2 -- esphome/components/hyt271/hyt271.h | 2 -- esphome/components/ina219/ina219.cpp | 2 -- esphome/components/ina219/ina219.h | 1 - esphome/components/ina226/ina226.cpp | 2 -- esphome/components/ina226/ina226.h | 1 - esphome/components/ina2xx_base/ina2xx_base.cpp | 2 -- esphome/components/ina2xx_base/ina2xx_base.h | 1 - esphome/components/ina3221/ina3221.cpp | 1 - esphome/components/ina3221/ina3221.h | 1 - esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 2 -- esphome/components/kamstrup_kmp/kamstrup_kmp.h | 1 - esphome/components/kmeteriso/kmeteriso.cpp | 2 -- esphome/components/kmeteriso/kmeteriso.h | 1 - esphome/components/m5stack_8angle/m5stack_8angle.cpp | 2 -- esphome/components/m5stack_8angle/m5stack_8angle.h | 1 - esphome/components/max17043/max17043.cpp | 2 -- esphome/components/max17043/max17043.h | 1 - esphome/components/max31855/max31855.cpp | 1 - esphome/components/max31855/max31855.h | 1 - esphome/components/max31856/max31856.cpp | 2 -- esphome/components/max31856/max31856.h | 1 - esphome/components/max31865/max31865.cpp | 2 -- esphome/components/max31865/max31865.h | 1 - esphome/components/max44009/max44009.cpp | 2 -- esphome/components/max44009/max44009.h | 1 - esphome/components/max6675/max6675.cpp | 1 - esphome/components/max6675/max6675.h | 1 - esphome/components/mcp3008/sensor/mcp3008_sensor.cpp | 2 -- esphome/components/mcp3008/sensor/mcp3008_sensor.h | 1 - esphome/components/mcp3204/sensor/mcp3204_sensor.cpp | 2 -- esphome/components/mcp3204/sensor/mcp3204_sensor.h | 1 - esphome/components/mcp9808/mcp9808.cpp | 1 - esphome/components/mcp9808/mcp9808.h | 1 - esphome/components/mhz19/mhz19.cpp | 2 -- esphome/components/mhz19/mhz19.h | 2 -- esphome/components/mics_4514/mics_4514.cpp | 1 - esphome/components/mics_4514/mics_4514.h | 1 - esphome/components/mlx90393/sensor_mlx90393.cpp | 2 -- esphome/components/mlx90393/sensor_mlx90393.h | 1 - esphome/components/mlx90614/mlx90614.cpp | 2 -- esphome/components/mlx90614/mlx90614.h | 1 - esphome/components/mmc5603/mmc5603.cpp | 2 -- esphome/components/mmc5603/mmc5603.h | 1 - esphome/components/mmc5983/mmc5983.cpp | 2 -- esphome/components/mmc5983/mmc5983.h | 1 - esphome/components/mpu6050/mpu6050.cpp | 1 - esphome/components/mpu6050/mpu6050.h | 2 -- esphome/components/mpu6886/mpu6886.cpp | 2 -- esphome/components/mpu6886/mpu6886.h | 2 -- esphome/components/ms5611/ms5611.cpp | 1 - esphome/components/ms5611/ms5611.h | 1 - esphome/components/msa3xx/msa3xx.cpp | 1 - esphome/components/msa3xx/msa3xx.h | 2 -- esphome/components/nau7802/nau7802.cpp | 2 -- esphome/components/nau7802/nau7802.h | 1 - esphome/components/nextion/nextion.cpp | 1 - esphome/components/nextion/nextion.h | 1 - esphome/components/npi19/npi19.cpp | 2 -- esphome/components/npi19/npi19.h | 1 - esphome/components/ntc/ntc.cpp | 1 - esphome/components/ntc/ntc.h | 1 - esphome/components/opt3001/opt3001.cpp | 2 -- esphome/components/opt3001/opt3001.h | 1 - esphome/components/pcf85063/pcf85063.cpp | 2 -- esphome/components/pcf85063/pcf85063.h | 1 - esphome/components/pcf8563/pcf8563.cpp | 2 -- esphome/components/pcf8563/pcf8563.h | 1 - esphome/components/pm1006/pm1006.cpp | 2 -- esphome/components/pm1006/pm1006.h | 2 -- esphome/components/pm2005/pm2005.h | 2 -- esphome/components/pmwcs3/pmwcs3.cpp | 2 -- esphome/components/pmwcs3/pmwcs3.h | 1 - esphome/components/pn532/pn532.cpp | 2 -- esphome/components/pn532/pn532.h | 1 - esphome/components/pulse_meter/pulse_meter_sensor.cpp | 2 -- esphome/components/pulse_meter/pulse_meter_sensor.h | 1 - esphome/components/pylontech/pylontech.cpp | 2 -- esphome/components/pylontech/pylontech.h | 2 -- esphome/components/qmc5883l/qmc5883l.cpp | 2 -- esphome/components/qmc5883l/qmc5883l.h | 1 - esphome/components/rd03d/rd03d.h | 1 - esphome/components/resampler/speaker/resampler_speaker.h | 1 - esphome/components/rotary_encoder/rotary_encoder.cpp | 1 - esphome/components/rotary_encoder/rotary_encoder.h | 2 -- esphome/components/rx8130/rx8130.h | 2 -- esphome/components/sdp3x/sdp3x.cpp | 2 -- esphome/components/sdp3x/sdp3x.h | 1 - esphome/components/sds011/sds011.cpp | 2 -- esphome/components/sds011/sds011.h | 2 -- esphome/components/sht3xd/sht3xd.cpp | 2 -- esphome/components/sht3xd/sht3xd.h | 1 - esphome/components/shtcx/shtcx.cpp | 2 -- esphome/components/shtcx/shtcx.h | 1 - esphome/components/smt100/smt100.cpp | 2 -- esphome/components/smt100/smt100.h | 2 -- esphome/components/sonoff_d1/sonoff_d1.h | 1 - esphome/components/spi_device/spi_device.cpp | 2 -- esphome/components/spi_device/spi_device.h | 2 -- esphome/components/sts3x/sts3x.cpp | 2 +- esphome/components/sts3x/sts3x.h | 1 - esphome/components/sy6970/sy6970.h | 1 - esphome/components/t6615/t6615.cpp | 1 - esphome/components/t6615/t6615.h | 2 -- esphome/components/tc74/tc74.cpp | 2 -- esphome/components/tc74/tc74.h | 2 -- esphome/components/tcs34725/tcs34725.cpp | 1 - esphome/components/tcs34725/tcs34725.h | 1 - esphome/components/tee501/tee501.cpp | 1 - esphome/components/tee501/tee501.h | 1 - esphome/components/tem3200/tem3200.cpp | 2 -- esphome/components/tem3200/tem3200.h | 1 - esphome/components/time_based/time_based_cover.cpp | 2 +- esphome/components/time_based/time_based_cover.h | 1 - esphome/components/tmp102/tmp102.cpp | 2 -- esphome/components/tmp102/tmp102.h | 2 -- esphome/components/tmp117/tmp117.cpp | 2 +- esphome/components/tmp117/tmp117.h | 1 - esphome/components/tsl2561/tsl2561.cpp | 2 +- esphome/components/tsl2561/tsl2561.h | 1 - esphome/components/tsl2591/tsl2591.cpp | 2 -- esphome/components/tsl2591/tsl2591.h | 2 -- esphome/components/tx20/tx20.cpp | 2 -- esphome/components/tx20/tx20.h | 1 - esphome/components/ultrasonic/ultrasonic_sensor.h | 2 -- esphome/components/version/version_text_sensor.cpp | 1 - esphome/components/version/version_text_sensor.h | 1 - esphome/components/wts01/wts01.h | 1 - 228 files changed, 6 insertions(+), 337 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index b13fcd519a..9c66531d05 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -45,8 +45,6 @@ void AbsoluteHumidityComponent::dump_config() { this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); } -float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; } - void AbsoluteHumidityComponent::loop() { if (!this->next_update_) { return; diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h index 9f3b9eab8b..71feee2c42 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.h +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -24,7 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 526dd57fd5..91cf4eaafc 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -68,11 +68,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage /// This method is called during the ESPHome setup process to log the configuration. void dump_config() override; - /// Return the setup priority for this component. - /// Components with higher priority are initialized earlier during setup. - /// @return A float representing the setup priority. - float get_setup_priority() const override; - #ifdef USE_ZEPHYR /// Set the ADC channel to be used by the ADC sensor. /// @param channel Pointer to an adc_dt_spec structure representing the ADC channel. diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index 748c8634b7..c779fd5893 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -79,7 +79,5 @@ void ADCSensor::set_sample_count(uint8_t sample_count) { void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } -float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace adc } // namespace esphome diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp index 03ce31d3cb..800b2d5261 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp @@ -9,8 +9,6 @@ static const char *const TAG = "adc128s102.sensor"; ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} -float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; } - void ADC128S102Sensor::dump_config() { LOG_SENSOR("", "ADC128S102 Sensor", this); ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_); diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.h b/esphome/components/adc128s102/sensor/adc128s102_sensor.h index 234500c2f4..5e6fc74e9c 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.h +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.h @@ -19,7 +19,6 @@ class ADC128S102Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 03d9d9cd9e..1b1f8335cc 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -150,8 +150,6 @@ void AHT10Component::update() { this->restart_read_(); } -float AHT10Component::get_setup_priority() const { return setup_priority::DATA; } - void AHT10Component::dump_config() { ESP_LOGCONFIG(TAG, "AHT10:"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index a3320c77e0..ce9cd963ad 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -16,7 +16,6 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index b20a8c6cbb..1390b74975 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -176,7 +176,5 @@ void AM2315C::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2315C::get_setup_priority() const { return setup_priority::DATA; } - } // namespace am2315c } // namespace esphome diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h index c8d01beeaa..d7baf01cae 100644 --- a/esphome/components/am2315c/am2315c.h +++ b/esphome/components/am2315c/am2315c.h @@ -33,7 +33,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; void setup() override; - float get_setup_priority() const override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index 055be2aeee..7fef3bb3a6 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -51,7 +51,6 @@ void AM2320Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2320Component::get_setup_priority() const { return setup_priority::DATA; } bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { if (!this->write_bytes(a_register, data, 2)) { diff --git a/esphome/components/am2320/am2320.h b/esphome/components/am2320/am2320.h index da1e87cf65..708dbb632e 100644 --- a/esphome/components/am2320/am2320.h +++ b/esphome/components/am2320/am2320.h @@ -11,7 +11,6 @@ class AM2320Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 93038d3160..260de82d14 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -384,7 +384,6 @@ void APDS9960::process_dataset_(int up, int down, int left, int right) { } } } -float APDS9960::get_setup_priority() const { return setup_priority::DATA; } bool APDS9960::is_proximity_enabled_() const { return #ifdef USE_SENSOR diff --git a/esphome/components/apds9960/apds9960.h b/esphome/components/apds9960/apds9960.h index 2a0fbb5c19..4574b70a42 100644 --- a/esphome/components/apds9960/apds9960.h +++ b/esphome/components/apds9960/apds9960.h @@ -32,7 +32,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void loop() override; diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h index a990f815fe..2e526ca825 100644 --- a/esphome/components/aqi/aqi_sensor.h +++ b/esphome/components/aqi/aqi_sensor.h @@ -10,7 +10,6 @@ class AQISensor : 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_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; } void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; } diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 93a0bff5b3..dd0ab714f7 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -41,8 +41,6 @@ void AS3935Component::dump_config() { #endif } -float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } - void AS3935Component::loop() { if (!this->irq_pin_->digital_read()) return; diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index dc590c268e..5dff1cb0ae 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -74,7 +74,6 @@ class AS3935Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; } diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp index feb8f6cebf..1c0f4bad2c 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -22,8 +22,6 @@ static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R -float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; } - void AS5600Sensor::dump_config() { LOG_SENSOR("", "AS5600 Sensor", this); ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h index 0af9b01ae5..d471be49b5 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -14,7 +14,6 @@ class AS5600Sensor : public PollingComponent, public Parented, public: void update() override; void dump_config() override; - float get_setup_priority() const override; void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } diff --git a/esphome/components/as7341/as7341.cpp b/esphome/components/as7341/as7341.cpp index 893eaa850f..1e78d814c8 100644 --- a/esphome/components/as7341/as7341.cpp +++ b/esphome/components/as7341/as7341.cpp @@ -58,8 +58,6 @@ void AS7341Component::dump_config() { LOG_SENSOR(" ", "NIR", this->nir_); } -float AS7341Component::get_setup_priority() const { return setup_priority::DATA; } - void AS7341Component::update() { this->read_channels(this->channel_readings_); diff --git a/esphome/components/as7341/as7341.h b/esphome/components/as7341/as7341.h index aed7996cef..3ede9d4aa4 100644 --- a/esphome/components/as7341/as7341.h +++ b/esphome/components/as7341/as7341.h @@ -78,7 +78,6 @@ class AS7341Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_f1_sensor(sensor::Sensor *f1_sensor) { this->f1_ = f1_sensor; } diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index cadc06ac6b..2203dd0d71 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -146,7 +146,6 @@ void ATM90E26Component::dump_config() { LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); } -float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; } uint16_t ATM90E26Component::read16_(uint8_t a_register) { uint8_t data[2]; diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h index 3c098d7e91..d15a53ea43 100644 --- a/esphome/components/atm90e26/atm90e26.h +++ b/esphome/components/atm90e26/atm90e26.h @@ -13,7 +13,6 @@ class ATM90E26Component : public PollingComponent, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; } diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index bd7c667c25..045fb7cf45 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -265,6 +265,4 @@ void BH1750Sensor::fail_and_reset_() { this->state_ = IDLE; } -float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace esphome::bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 0460427954..39dbd1d6a9 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -21,7 +21,6 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void dump_config() override; void update() override; void loop() override; - float get_setup_priority() const override; protected: // State machine states diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index c5d4c9c0a5..f396888fd1 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -199,7 +199,6 @@ void BME280Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_)); } -float BME280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bme280_base/bme280_base.h b/esphome/components/bme280_base/bme280_base.h index 0f55ad0101..00781d05b2 100644 --- a/esphome/components/bme280_base/bme280_base.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -76,7 +76,6 @@ class BME280Component : public PollingComponent { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 16435ccfee..5e52c84b3d 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -233,8 +233,6 @@ void BME680Component::dump_config() { } } -float BME680Component::get_setup_priority() const { return setup_priority::DATA; } - void BME680Component::update() { uint8_t meas_control = 0; // No need to fetch, we're setting all fields meas_control |= (this->temperature_oversampling_ & 0b111) << 5; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index cfa7aaca20..d48a42823b 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -99,7 +99,6 @@ class BME680Component : public PollingComponent, public i2c::I2CDevice { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index d969c8fd98..392d071b31 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -181,8 +181,6 @@ void BME680BSECComponent::dump_config() { LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); } -float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } - void BME680BSECComponent::loop() { this->run_(); diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index e52dbe964b..ec919f31df 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -64,7 +64,6 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 91383c8d45..1a42c9d54b 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -106,8 +106,6 @@ void BME68xBSEC2Component::dump_config() { #endif } -float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } - void BME68xBSEC2Component::loop() { this->run_(); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 86d3e5dfbf..8f4d8f61c2 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -48,7 +48,6 @@ class BME68xBSEC2Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; } diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index 4fcc3edb82..1e8c91d7b7 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -263,7 +263,6 @@ void BMI160Component::update() { this->status_clear_warning(); } -float BMI160Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmi160 } // namespace esphome diff --git a/esphome/components/bmi160/bmi160.h b/esphome/components/bmi160/bmi160.h index 47691a4de9..16cab69733 100644 --- a/esphome/components/bmi160/bmi160.h +++ b/esphome/components/bmi160/bmi160.h @@ -14,8 +14,6 @@ class BMI160Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 657da34f9b..9a383b2654 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -131,7 +131,6 @@ bool BMP085Component::set_mode_(uint8_t mode) { ESP_LOGV(TAG, "Setting mode to 0x%02X", mode); return this->write_byte(BMP085_REGISTER_CONTROL, mode); } -float BMP085Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmp085 } // namespace esphome diff --git a/esphome/components/bmp085/bmp085.h b/esphome/components/bmp085/bmp085.h index d84b4d43ef..c7315827e0 100644 --- a/esphome/components/bmp085/bmp085.h +++ b/esphome/components/bmp085/bmp085.h @@ -18,8 +18,6 @@ class BMP085Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - protected: struct CalibrationData { int16_t ac1, ac2, ac3; diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 728eead521..de685e7c27 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -148,7 +148,6 @@ void BMP280Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); } -float BMP280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index a47a794e96..836eafaf8b 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -64,7 +64,6 @@ class BMP280Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index acc28d4e85..d7d9972170 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -179,7 +179,6 @@ void BMP3XXComponent::dump_config() { ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); } } -float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.h b/esphome/components/bmp3xx_base/bmp3xx_base.h index 50f92e04c1..8d2312231b 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.h +++ b/esphome/components/bmp3xx_base/bmp3xx_base.h @@ -73,7 +73,6 @@ class BMP3XXComponent : public PollingComponent { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index 4293d7af07..302c83d7d3 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -7,8 +7,6 @@ namespace cd74hc4067 { static const char *const TAG = "cd74hc4067"; -float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } - void CD74HC4067Component::setup() { this->pin_s0_->setup(); this->pin_s1_->setup(); diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h index 6193513575..76ebc1ebbe 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.h +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -13,7 +13,6 @@ class CD74HC4067Component : public Component { /// Set up the internal sensor array. void setup() override; void dump_config() override; - float get_setup_priority() const override; /// setting pin active by setting the right combination of the four multiplexer input pins void activate_pin(uint8_t pin); diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index ad089bbe7d..8c33e56457 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -10,8 +10,6 @@ namespace cm1106 { class CM1106Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index 901aeaf259..fb5e156da9 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -10,8 +10,6 @@ namespace combination { class CombinationComponent : public Component, public sensor::Sensor { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - /// @brief Logs all source sensor's names virtual void log_source_sensors() = 0; diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 482636dd81..7c5ee833a4 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -62,8 +62,6 @@ void CSE7761Component::dump_config() { this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); } -float CSE7761Component::get_setup_priority() const { return setup_priority::DATA; } - void CSE7761Component::update() { if (this->data_.ready) { this->get_data_(); diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h index 71846cdcab..289c5e7e19 100644 --- a/esphome/components/cse7761/cse7761.h +++ b/esphome/components/cse7761/cse7761.h @@ -28,7 +28,6 @@ class CSE7761Component : public PollingComponent, public uart::UARTDevice { void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index c36e57c929..df4872deac 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -37,7 +37,6 @@ void CSE7766Component::loop() { this->raw_data_index_ = (this->raw_data_index_ + 1) % 24; } } -float CSE7766Component::get_setup_priority() const { return setup_priority::DATA; } bool CSE7766Component::check_byte_() { uint8_t index = this->raw_data_index_; diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 8902eafe3c..efddccd3c5 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -23,7 +23,6 @@ class CSE7766Component : public Component, public uart::UARTDevice { void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } void loop() override; - float get_setup_priority() const override; void dump_config() override; protected: diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 402cf9fee7..5dfaeeff39 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -159,7 +159,6 @@ void CurrentBasedCover::dump_config() { this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } -float CurrentBasedCover::get_setup_priority() const { return setup_priority::DATA; } void CurrentBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index f7993f1550..76bd85cdf7 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -14,7 +14,6 @@ class CurrentBasedCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 2d270cc56e..90ccee78f8 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -104,8 +104,6 @@ void DalyBmsComponent::loop() { } } -float DalyBmsComponent::get_setup_priority() const { return setup_priority::DATA; } - void DalyBmsComponent::request_data_(uint8_t data_id) { uint8_t request_message[DALY_FRAME_SIZE]; diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index e6d476bcdd..1983ba0ef1 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -72,7 +72,6 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void update() override; void loop() override; - float get_setup_priority() const override; void set_address(uint8_t address) { this->addr_ = address; } protected: diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 276ea24717..fef247f168 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -63,8 +63,6 @@ void DHT::update() { } } -float DHT::get_setup_priority() const { return setup_priority::DATA; } - void DHT::set_dht_model(DHTModel model) { this->model_ = model; this->is_auto_detect_ = model == DHT_MODEL_AUTO_DETECT; diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 9047dd2c96..4671ee6f27 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -51,8 +51,6 @@ class DHT : public PollingComponent { void dump_config() override; /// Update sensor values and push them to the frontend. void update() override; - /// HARDWARE_LATE setup priority. - float get_setup_priority() const override; protected: bool read_sensor_(float *temperature, float *humidity, bool report_errors); diff --git a/esphome/components/dht12/dht12.cpp b/esphome/components/dht12/dht12.cpp index 445d150be0..1d884daad6 100644 --- a/esphome/components/dht12/dht12.cpp +++ b/esphome/components/dht12/dht12.cpp @@ -49,7 +49,7 @@ void DHT12Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float DHT12Component::get_setup_priority() const { return setup_priority::DATA; } + bool DHT12Component::read_data_(uint8_t *data) { if (!this->read_bytes(0, data, 5)) { ESP_LOGW(TAG, "Updating DHT12 failed!"); diff --git a/esphome/components/dht12/dht12.h b/esphome/components/dht12/dht12.h index 2a706039ba..ab19d7c723 100644 --- a/esphome/components/dht12/dht12.h +++ b/esphome/components/dht12/dht12.h @@ -11,7 +11,6 @@ class DHT12Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index 6b6f9622fa..aa0a77cdd8 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -98,8 +98,6 @@ void DPS310Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float DPS310Component::get_setup_priority() const { return setup_priority::DATA; } - void DPS310Component::update() { if (!this->update_in_progress_) { this->update_in_progress_ = true; diff --git a/esphome/components/dps310/dps310.h b/esphome/components/dps310/dps310.h index 50e7d93c8a..dce220d44b 100644 --- a/esphome/components/dps310/dps310.h +++ b/esphome/components/dps310/dps310.h @@ -40,7 +40,6 @@ class DPS310Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index adbd7b5487..5c0e98290b 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -26,8 +26,6 @@ void DS1307Component::dump_config() { RealTimeClock::dump_config(); } -float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } - void DS1307Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index f7f06253b7..1712056006 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -12,7 +12,6 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 40a728d025..f801769d27 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -43,8 +43,6 @@ void DutyCycleSensor::update() { this->last_update_ = now; } -float DutyCycleSensor::get_setup_priority() const { return setup_priority::DATA; } - void IRAM_ATTR DutyCycleSensorStore::gpio_intr(DutyCycleSensorStore *arg) { const bool new_level = arg->pin.digital_read(); if (new_level == arg->last_level) diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index ffb1802e14..ffb8e3b622 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -22,7 +22,6 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override; - float get_setup_priority() const override; void dump_config() override; void update() override; diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 602e31db14..22f28be9bc 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -55,8 +55,6 @@ void EE895Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float EE895Component::get_setup_priority() const { return setup_priority::DATA; } - void EE895Component::update() { write_command_(TEMPERATURE_ADDRESS, 2); this->set_timeout(50, [this]() { diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h index 83bd7c6e82..259b7c524b 100644 --- a/esphome/components/ee895/ee895.h +++ b/esphome/components/ee895/ee895.h @@ -14,7 +14,6 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.cpp b/esphome/components/emc2101/sensor/emc2101_sensor.cpp index 2a199f48e9..3014c7da07 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.cpp +++ b/esphome/components/emc2101/sensor/emc2101_sensor.cpp @@ -7,8 +7,6 @@ namespace emc2101 { static const char *const TAG = "EMC2101.sensor"; -float EMC2101Sensor::get_setup_priority() const { return setup_priority::DATA; } - void EMC2101Sensor::dump_config() { ESP_LOGCONFIG(TAG, "Emc2101 sensor:"); LOG_SENSOR(" ", "Internal temperature", this->internal_temperature_sensor_); diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.h b/esphome/components/emc2101/sensor/emc2101_sensor.h index 3e8dcebc8e..3e033f58a7 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.h +++ b/esphome/components/emc2101/sensor/emc2101_sensor.h @@ -15,8 +15,6 @@ class EMC2101Sensor : public PollingComponent { void dump_config() override; /** Used by ESPHome framework. */ void update() override; - /** Used by ESPHome framework. */ - float get_setup_priority() const override; /** Used by ESPHome framework. */ void set_internal_temperature_sensor(sensor::Sensor *sensor) { this->internal_temperature_sensor_ = sensor; } diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index e28f024136..ea8a5ec186 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -111,7 +111,7 @@ void EndstopCover::dump_config() { LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); } -float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } + void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index 6f72b2b805..32ede12335 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -13,7 +13,6 @@ class EndstopCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; Trigger<> *get_open_trigger() { return &this->open_trigger_; } Trigger<> *get_close_trigger() { return &this->close_trigger_; } diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index 98a300f5d7..8bee9bfb18 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -136,8 +136,6 @@ void ENS210Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } - void ENS210Component::update() { // Execute a single measurement if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h index 0fb6ff634d..ae2bf81b5f 100644 --- a/esphome/components/ens210/ens210.h +++ b/esphome/components/ens210/ens210.h @@ -10,7 +10,6 @@ namespace ens210 { /// This class implements support for the ENS210 relative humidity and temperature i2c sensor. class ENS210Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override; void dump_config() override; void setup() override; void update() override; diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 5466d2e7ef..cfe06b1673 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -235,8 +235,6 @@ void ESP32Camera::loop() { this->single_requesters_ = 0; } -float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } - /* ---------------- constructors ---------------- */ ESP32Camera::ESP32Camera() { this->config_.pin_pwdn = -1; diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index e97eb27c70..eea93b7e01 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -159,7 +159,6 @@ class ESP32Camera : public camera::Camera { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; /* public API (specific) */ void start_stream(camera::CameraRequester requester) override; void stop_stream(camera::CameraRequester requester) override; diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 812c746301..7f45f2ccb4 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -51,7 +51,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/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index ddf38f2f55..8b381564b2 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -77,8 +77,6 @@ void GDK101Component::dump_config() { #endif // USE_TEXT_SENSOR } -float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } - bool GDK101Component::read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len) { uint8_t retry = NUMBER_OF_READ_RETRIES; bool status = false; diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index f250a42a54..abe417e0f9 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -40,7 +40,6 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 6613187b20..972f2ce60c 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -20,7 +20,6 @@ class GP8403Component : 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_model(GP8403Model model) { this->model_ = model; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp index 4d0f77df1b..4c2d367b24 100644 --- a/esphome/components/hc8/hc8.cpp +++ b/esphome/components/hc8/hc8.cpp @@ -86,8 +86,6 @@ void HC8Component::calibrate(uint16_t baseline) { this->flush(); } -float HC8Component::get_setup_priority() const { return setup_priority::DATA; } - void HC8Component::dump_config() { ESP_LOGCONFIG(TAG, "HC8:\n" diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h index 7711fb8c97..74257fab14 100644 --- a/esphome/components/hc8/hc8.h +++ b/esphome/components/hc8/hc8.h @@ -11,8 +11,6 @@ namespace esphome::hc8 { class HC8Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index 7ad0764f1f..a5bece82c4 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -16,8 +16,6 @@ class HDC1080Component : 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 *humidity_{nullptr}; diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index f037ee9d8b..d0fd697d8f 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -48,7 +48,6 @@ void HLW8012Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Energy", this->energy_sensor_); } -float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { // HLW8012 has 50% duty cycle pulse_counter::pulse_counter_t raw_cf = this->cf_store_.read_raw_value(); diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index 312391f533..8a13ec07d8 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -31,7 +31,6 @@ class HLW8012Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_initial_mode(HLW8012InitialMode initial_mode) { diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 00fb85397c..9343b47823 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -31,8 +31,6 @@ void HM3301Component::dump_config() { LOG_SENSOR(" ", "AQI", this->aqi_sensor_); } -float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } - void HM3301Component::update() { if (this->read(data_buffer_, 29) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Read result failed"); diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index e155ed6b4b..6b10a5e237 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -23,7 +23,6 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index 101493ad91..b62381a287 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -83,7 +83,6 @@ void HMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); } -float HMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } void HMC5883LComponent::update() { uint16_t raw_x, raw_y, raw_z; if (!this->read_byte_16(HMC5883L_REGISTER_DATA_X_MSB, &raw_x) || diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 06fba2af9d..8eae0f7a50 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -39,7 +39,6 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_oversampling(HMC5883LOversampling oversampling) { oversampling_ = oversampling; } diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index e72c5a21f5..d039892073 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -11,8 +11,6 @@ void HomeassistantTime::dump_config() { RealTimeClock::dump_config(); } -float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } - void HomeassistantTime::setup() { global_homeassistant_time = this; } void HomeassistantTime::update() { api::global_api_server->request_time(); } diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 36e28ea16b..7b5842fefd 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -13,7 +13,6 @@ class HomeassistantTime : public time::RealTimeClock { void update() override; void dump_config() override; void set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); } - float get_setup_priority() const override; }; extern HomeassistantTime *global_homeassistant_time; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp index 5f2b009972..904672d136 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -91,7 +91,5 @@ void HoneywellHIComponent::dump_config() { LOG_UPDATE_INTERVAL(this); } -float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace honeywell_hih_i2c } // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h index 4457eab1da..79140f7399 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.h +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -11,7 +11,6 @@ namespace honeywell_hih_i2c { class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override; void loop() override; void update() override; diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index cde6886109..972e72c170 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -43,7 +43,6 @@ void HTE501Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } void HTE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; this->write(address_1, 2); diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index a7072d5bdb..b47daf9157 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -13,7 +13,6 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index c5d91d3dd0..58a28b213f 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -143,7 +143,5 @@ uint8_t HTU21DComponent::get_heater_level() { return raw_heater & 0xF; } -float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace htu21d } // namespace esphome diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 277c6ca3e5..594be78326 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -28,8 +28,6 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_level(uint8_t level); uint8_t get_heater_level(); - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp index 562078aacb..4bb38a11a2 100644 --- a/esphome/components/htu31d/htu31d.cpp +++ b/esphome/components/htu31d/htu31d.cpp @@ -259,11 +259,5 @@ void HTU31DComponent::set_heater_state(bool desired) { } } -/** - * Sets the startup priority for this component. - * - * @returns The startup priority - */ -float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; } } // namespace htu31d } // namespace esphome diff --git a/esphome/components/htu31d/htu31d.h b/esphome/components/htu31d/htu31d.h index 9462133ced..24d85243cc 100644 --- a/esphome/components/htu31d/htu31d.h +++ b/esphome/components/htu31d/htu31d.h @@ -20,8 +20,6 @@ class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_state(bool desired); bool is_heater_enabled(); - float get_setup_priority() const override; - protected: bool reset_(); uint32_t read_serial_num_(); diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 67ec4549df..f2e3234127 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -22,7 +22,6 @@ void HX711Sensor::dump_config() { LOG_PIN(" SCK Pin: ", this->sck_pin_); LOG_UPDATE_INTERVAL(this); } -float HX711Sensor::get_setup_priority() const { return setup_priority::DATA; } void HX711Sensor::update() { uint32_t result; if (this->read_sensor_(&result)) { diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index a92bb9945d..37723ee81f 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -23,7 +23,6 @@ class HX711Sensor : public sensor::Sensor, public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 4872d68610..983a0a6649 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -284,7 +284,5 @@ void HydreonRGxxComponent::process_line_() { } } -float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hydreon_rgxx } // namespace esphome diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h index 76b0985a24..e3f9798a93 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.h +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -53,8 +53,6 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - void set_disable_led(bool disable_led) { this->disable_led_ = disable_led; } protected: diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp index f187e054a8..4c0e3cd96e 100644 --- a/esphome/components/hyt271/hyt271.cpp +++ b/esphome/components/hyt271/hyt271.cpp @@ -46,7 +46,5 @@ void HYT271Component::update() { this->status_clear_warning(); }); } -float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hyt271 } // namespace esphome diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h index 64f32a651c..19409d830c 100644 --- a/esphome/components/hyt271/hyt271.h +++ b/esphome/components/hyt271/hyt271.h @@ -16,8 +16,6 @@ class HYT271Component : public PollingComponent, public i2c::I2CDevice { /// Update the sensor values (temperature+humidity). void update() override; - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index ea8c5cea9d..278017651b 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -151,8 +151,6 @@ void INA219Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); } -float INA219Component::get_setup_priority() const { return setup_priority::DATA; } - void INA219Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; diff --git a/esphome/components/ina219/ina219.h b/esphome/components/ina219/ina219.h index 115fa886e0..bcadb65e36 100644 --- a/esphome/components/ina219/ina219.h +++ b/esphome/components/ina219/ina219.h @@ -13,7 +13,6 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void on_powerdown() override; diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index c4d4fb896e..cbc44c9a1a 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -104,8 +104,6 @@ void INA226Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); } -float INA226Component::get_setup_priority() const { return setup_priority::DATA; } - void INA226Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h index 61214fea0e..0aa66ff765 100644 --- a/esphome/components/ina226/ina226.h +++ b/esphome/components/ina226/ina226.h @@ -45,7 +45,6 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 7185d21810..de01c99a19 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -79,8 +79,6 @@ void INA2XX::setup() { this->state_ = State::IDLE; } -float INA2XX::get_setup_priority() const { return setup_priority::DATA; } - void INA2XX::update() { ESP_LOGD(TAG, "Updating"); if (this->is_ready() && this->state_ == State::IDLE) { diff --git a/esphome/components/ina2xx_base/ina2xx_base.h b/esphome/components/ina2xx_base/ina2xx_base.h index ba0999b28e..104c384a0d 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.h +++ b/esphome/components/ina2xx_base/ina2xx_base.h @@ -114,7 +114,6 @@ enum INAModel : uint8_t { INA_UNKNOWN = 0, INA_228, INA_229, INA_238, INA_239, I class INA2XX : public PollingComponent { public: void setup() override; - float get_setup_priority() const override; void update() override; void loop() override; void dump_config() override; diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 8243764147..d03183e002 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -113,7 +113,6 @@ void INA3221Component::update() { } } -float INA3221Component::get_setup_priority() const { return setup_priority::DATA; } void INA3221Component::set_shunt_resistance(int channel, float resistance_ohm) { this->channels_[channel].shunt_resistance_ = resistance_ohm; } diff --git a/esphome/components/ina3221/ina3221.h b/esphome/components/ina3221/ina3221.h index f593badc09..3769df77aa 100644 --- a/esphome/components/ina3221/ina3221.h +++ b/esphome/components/ina3221/ina3221.h @@ -12,7 +12,6 @@ class INA3221Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; void set_bus_voltage_sensor(int channel, sensor::Sensor *obj) { this->channels_[channel].bus_voltage_sensor_ = obj; } void set_shunt_voltage_sensor(int channel, sensor::Sensor *obj) { diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index e5fa035682..534939f9af 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -30,8 +30,6 @@ void KamstrupKMPComponent::dump_config() { this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8); } -float KamstrupKMPComponent::get_setup_priority() const { return setup_priority::DATA; } - void KamstrupKMPComponent::update() { if (this->heat_energy_sensor_ != nullptr) { this->command_queue_.push(CMD_HEAT_ENERGY); diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index c9cc9c5a39..f84e360132 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -83,7 +83,6 @@ class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice { void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_volume_sensor(sensor::Sensor *sensor) { this->volume_sensor_ = sensor; } void dump_config() override; - float get_setup_priority() const override; void update() override; void loop() override; void add_custom_sensor(sensor::Sensor *sensor, uint16_t command) { diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 36f6d74ba0..186686e472 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -47,8 +47,6 @@ void KMeterISOComponent::setup() { } } -float KMeterISOComponent::get_setup_priority() const { return setup_priority::DATA; } - void KMeterISOComponent::update() { uint8_t read_buf[4]; diff --git a/esphome/components/kmeteriso/kmeteriso.h b/esphome/components/kmeteriso/kmeteriso.h index c8bed662b0..6f1978105f 100644 --- a/esphome/components/kmeteriso/kmeteriso.h +++ b/esphome/components/kmeteriso/kmeteriso.h @@ -17,7 +17,6 @@ class KMeterISOComponent : public PollingComponent, public i2c::I2CDevice { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void setup() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.cpp b/esphome/components/m5stack_8angle/m5stack_8angle.cpp index 5a9a5e8c9d..2de900c21d 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.cpp +++ b/esphome/components/m5stack_8angle/m5stack_8angle.cpp @@ -69,7 +69,5 @@ int8_t M5Stack8AngleComponent::read_switch() { } } -float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace m5stack_8angle } // namespace esphome diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.h b/esphome/components/m5stack_8angle/m5stack_8angle.h index 831b1422fd..4942518054 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.h +++ b/esphome/components/m5stack_8angle/m5stack_8angle.h @@ -21,7 +21,6 @@ class M5Stack8AngleComponent : public i2c::I2CDevice, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); int8_t read_switch(); diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index e8cf4d5ab1..dfd59f1e7d 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -81,8 +81,6 @@ void MAX17043Component::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_); } -float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; } - void MAX17043Component::sleep_mode() { if (!this->is_failed()) { if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) { diff --git a/esphome/components/max17043/max17043.h b/esphome/components/max17043/max17043.h index 540b977789..f477ce5948 100644 --- a/esphome/components/max17043/max17043.h +++ b/esphome/components/max17043/max17043.h @@ -11,7 +11,6 @@ class MAX17043Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void sleep_mode(); diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index b5be3106cf..99129880f4 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -31,7 +31,6 @@ void MAX31855Sensor::dump_config() { ESP_LOGCONFIG(TAG, " Reference temperature disabled."); } } -float MAX31855Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX31855Sensor::read_data_() { this->enable(); delay(1); diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index 822e256587..b755d240f2 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -18,7 +18,6 @@ class MAX31855Sensor : public sensor::Sensor, void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index cc573cbc53..ff65c8c5c9 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -197,7 +197,5 @@ uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { return value; } -float MAX31856Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace max31856 } // namespace esphome diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h index 8d64cfe8bc..a27ababa2e 100644 --- a/esphome/components/max31856/max31856.h +++ b/esphome/components/max31856/max31856.h @@ -76,7 +76,6 @@ class MAX31856Sensor : public sensor::Sensor, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_filter(MAX31856ConfigFilter filter) { this->filter_ = filter; } void set_thermocouple_type(MAX31856ThermocoupleType thermocouple_type) { this->thermocouple_type_ = thermocouple_type; diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index a9c5204cf5..09b8368d07 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -90,8 +90,6 @@ void MAX31865Sensor::dump_config() { (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); } -float MAX31865Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MAX31865Sensor::read_data_() { // Read temperature, disable V_BIAS (save power) const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG); diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h index b83753a678..440c6523a6 100644 --- a/esphome/components/max31865/max31865.h +++ b/esphome/components/max31865/max31865.h @@ -34,7 +34,6 @@ class MAX31865Sensor : public sensor::Sensor, void set_num_rtd_wires(uint8_t rtd_wires) { rtd_wires_ = rtd_wires; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 928fc47696..8b8e38c1ea 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -51,8 +51,6 @@ void MAX44009Sensor::dump_config() { } } -float MAX44009Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MAX44009Sensor::update() { // update sensor illuminance value float lux = this->read_illuminance_(); diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index c85d1c1028..59eea66ed9 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -16,7 +16,6 @@ class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_mode(MAX44009Mode mode); bool set_continuous_mode(); diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index 54e0330ff7..c329cdfd42 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -23,7 +23,6 @@ void MAX6675Sensor::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); } -float MAX6675Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX6675Sensor::read_data_() { this->enable(); delay(1); diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h index ab0f06b041..f0db4a6c26 100644 --- a/esphome/components/max6675/max6675.h +++ b/esphome/components/max6675/max6675.h @@ -14,7 +14,6 @@ class MAX6675Sensor : public sensor::Sensor, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp index 81eb0a812f..ee052e9fb7 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp @@ -7,8 +7,6 @@ namespace mcp3008 { static const char *const TAG = "mcp3008.sensor"; -float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MCP3008Sensor::dump_config() { ESP_LOGCONFIG(TAG, "MCP3008Sensor:\n" diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.h b/esphome/components/mcp3008/sensor/mcp3008_sensor.h index ebaeab966f..9478d38e74 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.h +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.h @@ -19,7 +19,6 @@ class MCP3008Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index e673537be1..711448cf44 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -7,8 +7,6 @@ namespace mcp3204 { static const char *const TAG = "mcp3204.sensor"; -float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MCP3204Sensor::dump_config() { LOG_SENSOR("", "MCP3204 Sensor", this); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 5665b80b98..2bf75a9c1e 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -19,7 +19,6 @@ class MCP3204Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp index 088d33887f..ed12e52239 100644 --- a/esphome/components/mcp9808/mcp9808.cpp +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -73,7 +73,6 @@ void MCP9808Sensor::update() { this->publish_state(temp); this->status_clear_warning(); } -float MCP9808Sensor::get_setup_priority() const { return setup_priority::DATA; } } // namespace mcp9808 } // namespace esphome diff --git a/esphome/components/mcp9808/mcp9808.h b/esphome/components/mcp9808/mcp9808.h index 19aa3117c3..894e4599d0 100644 --- a/esphome/components/mcp9808/mcp9808.h +++ b/esphome/components/mcp9808/mcp9808.h @@ -11,7 +11,6 @@ class MCP9808Sensor : public sensor::Sensor, public PollingComponent, public i2c public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; }; diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index b6b4031e16..bccea7d423 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -137,8 +137,6 @@ bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *respo return this->read_array(response, MHZ19_RESPONSE_LENGTH); } -float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; } - void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index a27f1c31eb..e577b98537 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -22,8 +22,6 @@ enum MHZ19DetectionRange { class MHZ19Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index 8181ece94c..60413b32d7 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -37,7 +37,6 @@ void MICS4514Component::dump_config() { LOG_SENSOR(" ", "Hydrogen", this->hydrogen_sensor_); LOG_SENSOR(" ", "Ammonia", this->ammonia_sensor_); } -float MICS4514Component::get_setup_priority() const { return setup_priority::DATA; } void MICS4514Component::update() { if (!this->warmed_up_) { return; diff --git a/esphome/components/mics_4514/mics_4514.h b/esphome/components/mics_4514/mics_4514.h index d2fefc3630..e7271314c8 100644 --- a/esphome/components/mics_4514/mics_4514.h +++ b/esphome/components/mics_4514/mics_4514.h @@ -19,7 +19,6 @@ class MICS4514Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index 21a5b3a829..ee52f9b9ab 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -132,8 +132,6 @@ void MLX90393Cls::dump_config() { LOG_SENSOR(" ", "Temperature", this->t_sensor_); } -float MLX90393Cls::get_setup_priority() const { return setup_priority::DATA; } - void MLX90393Cls::update() { MLX90393::txyz data; diff --git a/esphome/components/mlx90393/sensor_mlx90393.h b/esphome/components/mlx90393/sensor_mlx90393.h index 8a6f3321f9..845ae87e09 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.h +++ b/esphome/components/mlx90393/sensor_mlx90393.h @@ -25,7 +25,6 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90 public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_drdy_gpio(GPIOPin *pin) { drdy_pin_ = pin; } diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 8e53b9e3c3..8a514cbc26 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -71,8 +71,6 @@ void MLX90614Component::dump_config() { LOG_SENSOR(" ", "Object", this->object_sensor_); } -float MLX90614Component::get_setup_priority() const { return setup_priority::DATA; } - void MLX90614Component::update() { uint8_t emissivity[3]; if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) { diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index fa6fb523bb..bf081c3e90 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -12,7 +12,6 @@ class MLX90614Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; void set_ambient_sensor(sensor::Sensor *ambient_sensor) { ambient_sensor_ = ambient_sensor; } void set_object_sensor(sensor::Sensor *object_sensor) { object_sensor_ = object_sensor; } diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index d6321eae8f..1cbc84191f 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -91,8 +91,6 @@ void MMC5603Component::dump_config() { LOG_SENSOR(" ", "Heading", this->heading_sensor_); } -float MMC5603Component::get_setup_priority() const { return setup_priority::DATA; } - void MMC5603Component::update() { uint8_t ctrl0 = (this->auto_set_reset_) ? 0x21 : 0x01; if (!this->write_byte(MMC56X3_CTRL0_REG, ctrl0)) { diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index 09718bd3b7..f827e27e04 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -17,7 +17,6 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_datarate(MMC5603Datarate datarate) { datarate_ = datarate; } diff --git a/esphome/components/mmc5983/mmc5983.cpp b/esphome/components/mmc5983/mmc5983.cpp index 1e0065020c..b038084a72 100644 --- a/esphome/components/mmc5983/mmc5983.cpp +++ b/esphome/components/mmc5983/mmc5983.cpp @@ -133,7 +133,5 @@ void MMC5983Component::dump_config() { LOG_SENSOR(" ", "Z", this->z_sensor_); } -float MMC5983Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace mmc5983 } // namespace esphome diff --git a/esphome/components/mmc5983/mmc5983.h b/esphome/components/mmc5983/mmc5983.h index d425418904..3e87e54daa 100644 --- a/esphome/components/mmc5983/mmc5983.h +++ b/esphome/components/mmc5983/mmc5983.h @@ -12,7 +12,6 @@ class MMC5983Component : public PollingComponent, public i2c::I2CDevice { void update() override; void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index ecbee11c48..91a84d061a 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -140,7 +140,6 @@ void MPU6050Component::update() { this->status_clear_warning(); } -float MPU6050Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace mpu6050 } // namespace esphome diff --git a/esphome/components/mpu6050/mpu6050.h b/esphome/components/mpu6050/mpu6050.h index ab410105c0..cc7c3620df 100644 --- a/esphome/components/mpu6050/mpu6050.h +++ b/esphome/components/mpu6050/mpu6050.h @@ -14,8 +14,6 @@ class MPU6050Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp index 6fdf7b8684..68b77b59c9 100644 --- a/esphome/components/mpu6886/mpu6886.cpp +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -146,7 +146,5 @@ void MPU6886Component::update() { this->status_clear_warning(); } -float MPU6886Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace mpu6886 } // namespace esphome diff --git a/esphome/components/mpu6886/mpu6886.h b/esphome/components/mpu6886/mpu6886.h index 04551ae56d..96e2bf61a1 100644 --- a/esphome/components/mpu6886/mpu6886.h +++ b/esphome/components/mpu6886/mpu6886.h @@ -14,8 +14,6 @@ class MPU6886Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 5a7622e783..7ed73400c8 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -38,7 +38,6 @@ void MS5611Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float MS5611Component::get_setup_priority() const { return setup_priority::DATA; } void MS5611Component::update() { // request temperature reading if (!this->write_bytes(MS5611_CMD_CONV_D2 + 0x08, nullptr, 0)) { diff --git a/esphome/components/ms5611/ms5611.h b/esphome/components/ms5611/ms5611.h index 476db79612..7e4806f319 100644 --- a/esphome/components/ms5611/ms5611.h +++ b/esphome/components/ms5611/ms5611.h @@ -11,7 +11,6 @@ class MS5611Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index 56dc919968..e46bfed193 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -287,7 +287,6 @@ void MSA3xxComponent::update() { this->status_.never_published = false; this->status_clear_warning(); } -float MSA3xxComponent::get_setup_priority() const { return setup_priority::DATA; } void MSA3xxComponent::set_offset(float offset_x, float offset_y, float offset_z) { this->offset_x_ = offset_x; diff --git a/esphome/components/msa3xx/msa3xx.h b/esphome/components/msa3xx/msa3xx.h index 644109dab0..439d3b5f4d 100644 --- a/esphome/components/msa3xx/msa3xx.h +++ b/esphome/components/msa3xx/msa3xx.h @@ -220,8 +220,6 @@ class MSA3xxComponent : public PollingComponent, public i2c::I2CDevice { void loop() override; void update() override; - float get_setup_priority() const override; - void set_model(Model model) { this->model_ = model; } void set_offset(float offset_x, float offset_y, float offset_z); void set_range(Range range) { this->range_ = range; } diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 5edbc79862..937239b98d 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -296,8 +296,6 @@ void NAU7802Sensor::loop() { } } -float NAU7802Sensor::get_setup_priority() const { return setup_priority::DATA; } - void NAU7802Sensor::update() { if (!this->is_data_ready_()) { ESP_LOGW(TAG, "No measurements ready!"); diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h index 05452851ca..ae39e167a4 100644 --- a/esphome/components/nau7802/nau7802.h +++ b/esphome/components/nau7802/nau7802.h @@ -62,7 +62,6 @@ class NAU7802Sensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e57a258edb..fd6ce0a24b 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -193,7 +193,6 @@ void Nextion::dump_config() { #endif } -float Nextion::get_setup_priority() const { return setup_priority::DATA; } void Nextion::update() { if (!this->is_setup()) { return; diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c543e14bfe..c42ddba9b5 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1048,7 +1048,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void setup() override; void set_brightness(float brightness) { this->brightness_ = brightness; } - float get_setup_priority() const override; void update() override; void loop() override; void set_writer(const nextion_writer_t &writer); diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index c531d2ec8f..995abdff37 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -29,8 +29,6 @@ void NPI19Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } -float NPI19Component::get_setup_priority() const { return setup_priority::DATA; } - i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pressure) { // initiate data read from device i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND)); diff --git a/esphome/components/npi19/npi19.h b/esphome/components/npi19/npi19.h index df289dffc1..8e6a8e3bfa 100644 --- a/esphome/components/npi19/npi19.h +++ b/esphome/components/npi19/npi19.h @@ -15,7 +15,6 @@ class NPI19Component : public PollingComponent, public i2c::I2CDevice { this->raw_pressure_sensor_ = raw_pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index cc500ba429..e2097bdd77 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -12,7 +12,6 @@ void NTC::setup() { this->process_(this->sensor_->state); } void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this); } -float NTC::get_setup_priority() const { return setup_priority::DATA; } void NTC::process_(float value) { if (std::isnan(value)) { this->publish_state(NAN); diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h index c8592e0fe8..a0c72340de 100644 --- a/esphome/components/ntc/ntc.h +++ b/esphome/components/ntc/ntc.h @@ -14,7 +14,6 @@ class NTC : public Component, public sensor::Sensor { void set_c(double c) { c_ = c; } void setup() override; void dump_config() override; - float get_setup_priority() const override; protected: void process_(float value); diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index f5f7ab9412..a942e45035 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -116,7 +116,5 @@ void OPT3001Sensor::update() { }); } -float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace opt3001 } // namespace esphome diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h index ae3fde5c54..3bce9f0aeb 100644 --- a/esphome/components/opt3001/opt3001.h +++ b/esphome/components/opt3001/opt3001.h @@ -12,7 +12,6 @@ class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c public: void dump_config() override; void update() override; - float get_setup_priority() const override; protected: // checks if one-shot is complete before reading the result and returning it diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index f38b60b55d..03ed78654f 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -26,8 +26,6 @@ void PCF85063Component::dump_config() { RealTimeClock::dump_config(); } -float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } - void PCF85063Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h index b7034d4f00..697837f223 100644 --- a/esphome/components/pcf85063/pcf85063.h +++ b/esphome/components/pcf85063/pcf85063.h @@ -12,7 +12,6 @@ class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 2090936bb6..dc68807aef 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -26,8 +26,6 @@ void PCF8563Component::dump_config() { RealTimeClock::dump_config(); } -float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } - void PCF8563Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h index 81aa816b42..cd37d05816 100644 --- a/esphome/components/pcf8563/pcf8563.h +++ b/esphome/components/pcf8563/pcf8563.h @@ -12,7 +12,6 @@ class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp index c466c4bb25..fe8890e777 100644 --- a/esphome/components/pm1006/pm1006.cpp +++ b/esphome/components/pm1006/pm1006.cpp @@ -44,8 +44,6 @@ void PM1006Component::loop() { } } -float PM1006Component::get_setup_priority() const { return setup_priority::DATA; } - uint8_t PM1006Component::pm1006_checksum_(const uint8_t *command_data, uint8_t length) const { uint8_t sum = 0; for (uint8_t i = 0; i < length; i++) { diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h index 98637dad71..6b6332e1e3 100644 --- a/esphome/components/pm1006/pm1006.h +++ b/esphome/components/pm1006/pm1006.h @@ -18,8 +18,6 @@ class PM1006Component : public PollingComponent, public uart::UARTDevice { void loop() override; void update() override; - float get_setup_priority() const override; - protected: optional check_byte_() const; void parse_data_(); diff --git a/esphome/components/pm2005/pm2005.h b/esphome/components/pm2005/pm2005.h index 219fbae5cb..e788569b7e 100644 --- a/esphome/components/pm2005/pm2005.h +++ b/esphome/components/pm2005/pm2005.h @@ -14,8 +14,6 @@ enum SensorType { class PM2005Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - void set_sensor_type(SensorType sensor_type) { this->sensor_type_ = sensor_type; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { this->pm_1_0_sensor_ = pm_1_0_sensor; } diff --git a/esphome/components/pmwcs3/pmwcs3.cpp b/esphome/components/pmwcs3/pmwcs3.cpp index 95638851b5..2ed7789c53 100644 --- a/esphome/components/pmwcs3/pmwcs3.cpp +++ b/esphome/components/pmwcs3/pmwcs3.cpp @@ -53,8 +53,6 @@ void PMWCS3Component::water_calibration() { void PMWCS3Component::update() { this->read_data_(); } -float PMWCS3Component::get_setup_priority() const { return setup_priority::DATA; } - void PMWCS3Component::dump_config() { ESP_LOGCONFIG(TAG, "PMWCS3"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h index d63c516586..b1e26eec4f 100644 --- a/esphome/components/pmwcs3/pmwcs3.h +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -14,7 +14,6 @@ class PMWCS3Component : public PollingComponent, public i2c::I2CDevice { public: void update() override; void dump_config() override; - float get_setup_priority() const override; void set_e25_sensor(sensor::Sensor *e25_sensor) { e25_sensor_ = e25_sensor; } void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; } diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 733810c242..5366aab54e 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -426,8 +426,6 @@ bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) { return false; } -float PN532::get_setup_priority() const { return setup_priority::DATA; } - void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 488ec4af3b..73a6c15164 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -35,7 +35,6 @@ class PN532 : public PollingComponent { void dump_config() override; void update() override; - float get_setup_priority() const override; void loop() override; void on_powerdown() override { powerdown(); } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 9a7630a7be..007deb66e5 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -125,8 +125,6 @@ void PulseMeterSensor::loop() { } } -float PulseMeterSensor::get_setup_priority() const { return setup_priority::DATA; } - void PulseMeterSensor::dump_config() { LOG_SENSOR("", "Pulse Meter", this); LOG_PIN(" Pin: ", this->pin_); diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 748bab29ac..5800c4ec42 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -27,7 +27,6 @@ class PulseMeterSensor : public sensor::Sensor, public Component { void setup() override; void loop() override; - float get_setup_priority() const override; void dump_config() override; protected: diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 61b5356c90..1dc7caaf16 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -192,8 +192,6 @@ void PylontechComponent::process_line_(std::string &buffer) { } } -float PylontechComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace pylontech } // namespace esphome diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h index 10c669ad9a..5727928a60 100644 --- a/esphome/components/pylontech/pylontech.h +++ b/esphome/components/pylontech/pylontech.h @@ -34,8 +34,6 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - void register_listener(PylontechListener *listener) { this->listeners_.push_back(listener); } protected: diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 693614581c..bc2adb5cfe 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -86,8 +86,6 @@ void QMC5883LComponent::dump_config() { LOG_PIN(" DRDY Pin: ", this->drdy_pin_); } -float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } - void QMC5883LComponent::update() { i2c::ErrorCode err; uint8_t status = false; diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 5ba7180e23..21ef9c2a17 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -31,7 +31,6 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; } diff --git a/esphome/components/rd03d/rd03d.h b/esphome/components/rd03d/rd03d.h index 7413fe38f2..8bf7b423be 100644 --- a/esphome/components/rd03d/rd03d.h +++ b/esphome/components/rd03d/rd03d.h @@ -42,7 +42,6 @@ class RD03DComponent : public Component, public uart::UARTDevice { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_SENSOR void set_target_count_sensor(sensor::Sensor *sensor) { this->target_count_sensor_ = sensor; } diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index 51790069d2..810087ab7f 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -16,7 +16,6 @@ namespace resampler { class ResamplerSpeaker : public Component, public speaker::Speaker { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } void setup() override; void loop() override; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index c652944120..38fd14375d 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -235,7 +235,6 @@ void RotaryEncoderSensor::loop() { } } -float RotaryEncoderSensor::get_setup_priority() const { return setup_priority::DATA; } void RotaryEncoderSensor::set_restore_mode(RotaryEncoderRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 14442f0565..865554cd4d 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -82,8 +82,6 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { void dump_config() override; void loop() override; - float get_setup_priority() const override; - void add_on_clockwise_callback(std::function callback) { this->on_clockwise_callback_.add(std::move(callback)); } diff --git a/esphome/components/rx8130/rx8130.h b/esphome/components/rx8130/rx8130.h index 6694c763cd..979da3e19c 100644 --- a/esphome/components/rx8130/rx8130.h +++ b/esphome/components/rx8130/rx8130.h @@ -14,8 +14,6 @@ class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice { void dump_config() override; void read_time(); void write_time(); - /// Ensure RTC is initialized at the correct time in the setup sequence - float get_setup_priority() const override { return setup_priority::DATA; } protected: void stop_(bool stop); diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index d4ab04e7cd..6f6cc1ebd8 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -114,7 +114,5 @@ void SDP3XComponent::read_pressure_() { this->status_clear_warning(); } -float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace sdp3x } // namespace esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h index e3d3533c74..afb58d47c8 100644 --- a/esphome/components/sdp3x/sdp3x.h +++ b/esphome/components/sdp3x/sdp3x.h @@ -17,7 +17,6 @@ class SDP3XComponent : public PollingComponent, public sensirion_common::Sensiri void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } protected: diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 4e12c0e322..cdfd7544ad 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -108,8 +108,6 @@ void SDS011Component::loop() { } } -float SDS011Component::get_setup_priority() const { return setup_priority::DATA; } - void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } void SDS011Component::sds011_write_command_(const uint8_t *command_data) { diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h index 1f404601b1..3be74e66d1 100644 --- a/esphome/components/sds011/sds011.h +++ b/esphome/components/sds011/sds011.h @@ -21,8 +21,6 @@ class SDS011Component : public Component, public uart::UARTDevice { void dump_config() override; void loop() override; - float get_setup_priority() const override; - void set_update_interval(uint32_t val) { /* ignore */ } void set_update_interval_min(uint8_t update_interval_min); diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index d473df43c7..bd3dec6fb8 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -72,8 +72,6 @@ void SHT3XDComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; } - void SHT3XDComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 74f155121b..43f1a4d8e2 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -17,7 +17,6 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_heater_enabled(bool heater_enabled) { heater_enabled_ = heater_enabled; } diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index aca305b88d..ec12a5babd 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -63,8 +63,6 @@ void SHTCXComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float SHTCXComponent::get_setup_priority() const { return setup_priority::DATA; } - void SHTCXComponent::update() { if (this->status_has_warning()) { ESP_LOGW(TAG, "Retrying communication"); diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index 084d3bfc35..f9778dce8d 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -17,7 +17,6 @@ class SHTCXComponent : public PollingComponent, public sensirion_common::Sensiri void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void soft_reset(); void sleep(); diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index c8dfb4c7bd..1bcb964264 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -44,8 +44,6 @@ void SMT100Component::loop() { } } -float SMT100Component::get_setup_priority() const { return setup_priority::DATA; } - void SMT100Component::dump_config() { ESP_LOGCONFIG(TAG, "SMT100:"); diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index 86827607dc..df8803e1c6 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -17,8 +17,6 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { void loop() override; void update() override; - float get_setup_priority() const override; - void set_counts_sensor(sensor::Sensor *counts_sensor) { this->counts_sensor_ = counts_sensor; } void set_permittivity_sensor(sensor::Sensor *permittivity_sensor) { this->permittivity_sensor_ = permittivity_sensor; diff --git a/esphome/components/sonoff_d1/sonoff_d1.h b/esphome/components/sonoff_d1/sonoff_d1.h index 19ff83f378..20bea23287 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.h +++ b/esphome/components/sonoff_d1/sonoff_d1.h @@ -53,7 +53,6 @@ class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, publi void setup() override{}; void loop() override; void dump_config() override; - float get_setup_priority() const override { return esphome::setup_priority::DATA; } // Custom methods void set_use_rm433_remote(const bool use_rm433_remote) { this->use_rm433_remote_ = use_rm433_remote; } diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp index 4cc7286ba9..34f83027db 100644 --- a/esphome/components/spi_device/spi_device.cpp +++ b/esphome/components/spi_device/spi_device.cpp @@ -23,7 +23,5 @@ void SPIDeviceComponent::dump_config() { } } -float SPIDeviceComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace spi_device } // namespace esphome diff --git a/esphome/components/spi_device/spi_device.h b/esphome/components/spi_device/spi_device.h index d8aef440a7..e3aa74aaf0 100644 --- a/esphome/components/spi_device/spi_device.h +++ b/esphome/components/spi_device/spi_device.h @@ -13,8 +13,6 @@ class SPIDeviceComponent : public Component, void setup() override; void dump_config() override; - float get_setup_priority() const override; - protected: }; diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index eee2aca73e..8713b0b6b8 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -41,7 +41,7 @@ void STS3XComponent::dump_config() { LOG_SENSOR(" ", "STS3x", this); } -float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; } + void STS3XComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 8f806a3471..6c1dd2b244 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -14,7 +14,6 @@ class STS3XComponent : public sensor::Sensor, public PollingComponent, public se public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; }; diff --git a/esphome/components/sy6970/sy6970.h b/esphome/components/sy6970/sy6970.h index bacc072f9b..2225dd781b 100644 --- a/esphome/components/sy6970/sy6970.h +++ b/esphome/components/sy6970/sy6970.h @@ -87,7 +87,6 @@ class SY6970Component : 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; } // Listener registration void add_listener(SY6970Listener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index c2ac88ee2e..75f9ed108e 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -86,7 +86,6 @@ void T6615Component::query_ppm_() { this->send_ppm_command_(); } -float T6615Component::get_setup_priority() const { return setup_priority::DATA; } void T6615Component::dump_config() { ESP_LOGCONFIG(TAG, "T6615:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h index fb53032e8d..69c406a5ba 100644 --- a/esphome/components/t6615/t6615.h +++ b/esphome/components/t6615/t6615.h @@ -22,8 +22,6 @@ enum class T6615Command : uint8_t { class T6615Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void loop() override; void update() override; void dump_config() override; diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index abf3839e00..969ef3671e 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -61,7 +61,5 @@ void TC74Component::read_temperature_() { this->status_clear_warning(); } -float TC74Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace tc74 } // namespace esphome diff --git a/esphome/components/tc74/tc74.h b/esphome/components/tc74/tc74.h index 5d70c05420..f3ce225ff4 100644 --- a/esphome/components/tc74/tc74.h +++ b/esphome/components/tc74/tc74.h @@ -15,8 +15,6 @@ class TC74Component : public PollingComponent, public i2c::I2CDevice, public sen /// Update the sensor value (temperature). void update() override; - float get_setup_priority() const override; - protected: /// Internal method to read the temperature from the component after it has been scheduled. void read_temperature_(); diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index e4e5547595..4fe87de0ca 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -56,7 +56,6 @@ void TCS34725Component::dump_config() { LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_); } -float TCS34725Component::get_setup_priority() const { return setup_priority::DATA; } /*! * @brief Converts the raw R/G/B values to color temperature in degrees diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 23985e8221..85bb383e4b 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -52,7 +52,6 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { } void setup() override; - float get_setup_priority() const override; void update() override; void dump_config() override; diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 06481b628b..00a62247f9 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -43,7 +43,6 @@ void TEE501Component::dump_config() { LOG_SENSOR(" ", "TEE501", this); } -float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } void TEE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; this->write(address_1, 2); diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h index 2437ac92eb..62a6f1c944 100644 --- a/esphome/components/tee501/tee501.h +++ b/esphome/components/tee501/tee501.h @@ -12,7 +12,6 @@ class TEE501Component : public sensor::Sensor, public PollingComponent, public i public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/tem3200/tem3200.cpp b/esphome/components/tem3200/tem3200.cpp index b31496142c..9c305f8f6f 100644 --- a/esphome/components/tem3200/tem3200.cpp +++ b/esphome/components/tem3200/tem3200.cpp @@ -51,8 +51,6 @@ void TEM3200Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } -float TEM3200Component::get_setup_priority() const { return setup_priority::DATA; } - i2c::ErrorCode TEM3200Component::read_(uint8_t &status, uint16_t &raw_temperature, uint16_t &raw_pressure) { uint8_t response[4] = {0x00, 0x00, 0x00, 0x00}; diff --git a/esphome/components/tem3200/tem3200.h b/esphome/components/tem3200/tem3200.h index c84a2aba21..37589b2a06 100644 --- a/esphome/components/tem3200/tem3200.h +++ b/esphome/components/tem3200/tem3200.h @@ -15,7 +15,6 @@ class TEM3200Component : public PollingComponent, public i2c::I2CDevice { this->raw_pressure_sensor_ = raw_pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 0aef4b8e85..f6a3048bd4 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -51,7 +51,7 @@ void TimeBasedCover::loop() { this->last_publish_time_ = now; } } -float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; } + CoverTraits TimeBasedCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_stop(true); diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index d2457cae7a..0adc5cb370 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -12,7 +12,6 @@ class TimeBasedCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; Trigger<> *get_open_trigger() { return &this->open_trigger_; } Trigger<> *get_close_trigger() { return &this->close_trigger_; } diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index 7390d9fcc9..99f6753ddc 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -46,7 +46,5 @@ void TMP102Component::update() { }); } -float TMP102Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace tmp102 } // namespace esphome diff --git a/esphome/components/tmp102/tmp102.h b/esphome/components/tmp102/tmp102.h index 657b48c7cf..fe860a3819 100644 --- a/esphome/components/tmp102/tmp102.h +++ b/esphome/components/tmp102/tmp102.h @@ -11,8 +11,6 @@ class TMP102Component : public PollingComponent, public i2c::I2CDevice, public s public: void dump_config() override; void update() override; - - float get_setup_priority() const override; }; } // namespace tmp102 diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index c9eff41399..f8f52266e0 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -45,7 +45,7 @@ void TMP117Component::dump_config() { } LOG_SENSOR(" ", "Temperature", this); } -float TMP117Component::get_setup_priority() const { return setup_priority::DATA; } + bool TMP117Component::read_data_(int16_t *data) { if (!this->read_byte_16(0, (uint16_t *) data)) { ESP_LOGW(TAG, "Updating TMP117 failed!"); diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h index 162dbb64db..f501ee270c 100644 --- a/esphome/components/tmp117/tmp117.h +++ b/esphome/components/tmp117/tmp117.h @@ -11,7 +11,6 @@ class TMP117Component : public PollingComponent, public i2c::I2CDevice, public s public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_config(uint16_t config) { config_ = config; }; diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index 1442dd176c..cb4c38a83c 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -144,7 +144,7 @@ void TSL2561Sensor::set_integration_time(TSL2561IntegrationTime integration_time } void TSL2561Sensor::set_gain(TSL2561Gain gain) { this->gain_ = gain; } void TSL2561Sensor::set_is_cs_package(bool package_cs) { this->package_cs_ = package_cs; } -float TSL2561Sensor::get_setup_priority() const { return setup_priority::DATA; } + bool TSL2561Sensor::tsl2561_write_byte(uint8_t a_register, uint8_t value) { return this->write_byte(a_register | TSL2561_COMMAND_BIT, value); } diff --git a/esphome/components/tsl2561/tsl2561.h b/esphome/components/tsl2561/tsl2561.h index c54f41fb81..a8f0aef90f 100644 --- a/esphome/components/tsl2561/tsl2561.h +++ b/esphome/components/tsl2561/tsl2561.h @@ -67,7 +67,6 @@ class TSL2561Sensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; bool tsl2561_read_byte(uint8_t a_register, uint8_t *value); bool tsl2561_read_uint(uint8_t a_register, uint16_t *value); diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 999e42e949..42c524a074 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -247,8 +247,6 @@ void TSL2591Component::set_power_save_mode(bool enable) { this->power_save_mode_ void TSL2591Component::set_name(const char *name) { this->name_ = name; } -float TSL2591Component::get_setup_priority() const { return setup_priority::DATA; } - bool TSL2591Component::is_adc_valid() { uint8_t status; if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) { diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index fa302b14b0..84c92b6ba9 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -249,8 +249,6 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; /** Used by ESPHome framework. */ void update() override; - /** Used by ESPHome framework. */ - float get_setup_priority() const override; protected: const char *name_; diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index a6df61c053..6bc5f0bb51 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -38,8 +38,6 @@ void Tx20Component::loop() { } } -float Tx20Component::get_setup_priority() const { return setup_priority::DATA; } - std::string Tx20Component::get_wind_cardinal_direction() const { return this->wind_cardinal_direction_; } void Tx20Component::decode_and_publish_() { diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h index 95a9517227..d1673f99f2 100644 --- a/esphome/components/tx20/tx20.h +++ b/esphome/components/tx20/tx20.h @@ -35,7 +35,6 @@ class Tx20Component : public Component { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index a38737aff5..541f7d2b70 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -29,8 +29,6 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - /// Set the maximum time in µs to wait for the echo to return void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 584b8abfb2..2e5686008b 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -32,7 +32,6 @@ void VersionTextSensor::setup() { version_str[sizeof(version_str) - 1] = '\0'; this->publish_state(version_str); } -float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index 6813da7830..b7d8001120 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -11,7 +11,6 @@ class VersionTextSensor : public text_sensor::TextSensor, public Component { void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; - float get_setup_priority() const override; protected: bool hide_timestamp_{false}; diff --git a/esphome/components/wts01/wts01.h b/esphome/components/wts01/wts01.h index 298595a5d6..aae90c2c77 100644 --- a/esphome/components/wts01/wts01.h +++ b/esphome/components/wts01/wts01.h @@ -13,7 +13,6 @@ class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Compo public: void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: uint8_t buffer_[PACKET_SIZE]; From 4a579700a014f7beb2caebb93a21e64da23d5ff1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:52:14 +1100 Subject: [PATCH 40/95] [cover] Add operation-based triggers and fix repeated trigger firing (#13471) --- esphome/components/cover/__init__.py | 83 ++++++++++++++++------ esphome/components/cover/automation.h | 53 ++++++++------ esphome/components/cover/cover.cpp | 3 - esphome/components/cover/cover.h | 4 +- tests/components/template/common-base.yaml | 38 ++++++++++ 5 files changed, 133 insertions(+), 48 deletions(-) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 383daee083..41774f3d71 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg @@ -9,6 +11,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_MQTT_ID, + CONF_ON_IDLE, CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, @@ -32,9 +35,10 @@ from esphome.const import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass +from esphome.types import ConfigType, TemplateArgsType IS_PLATFORM_COMPONENT = True @@ -53,6 +57,8 @@ DEVICE_CLASSES = [ DEVICE_CLASS_WINDOW, ] +_LOGGER = logging.getLogger(__name__) + cover_ns = cg.esphome_ns.namespace("cover") Cover = cover_ns.class_("Cover", cg.EntityBase) @@ -83,14 +89,29 @@ ControlAction = cover_ns.class_("ControlAction", automation.Action) CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action) CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition) CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) - -# Triggers -CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template()) +CoverOpenedTrigger = cover_ns.class_( + "CoverOpenedTrigger", automation.Trigger.template() +) CoverClosedTrigger = cover_ns.class_( "CoverClosedTrigger", automation.Trigger.template() ) +CoverTrigger = cover_ns.class_("CoverTrigger", automation.Trigger.template()) +# Cover-specific constants CONF_ON_CLOSED = "on_closed" +CONF_ON_OPENED = "on_opened" +CONF_ON_OPENING = "on_opening" +CONF_ON_CLOSING = "on_closing" + +TRIGGERS = { + CONF_ON_OPEN: CoverOpenedTrigger, # Deprecated, use on_opened + CONF_ON_OPENED: CoverOpenedTrigger, + CONF_ON_CLOSED: CoverClosedTrigger, + CONF_ON_CLOSING: CoverTrigger.template(CoverOperation.COVER_OPERATION_CLOSING), + CONF_ON_OPENING: CoverTrigger.template(CoverOperation.COVER_OPERATION_OPENING), + CONF_ON_IDLE: CoverTrigger.template(CoverOperation.COVER_OPERATION_IDLE), +} + _COVER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) @@ -111,16 +132,14 @@ _COVER_SCHEMA = ( cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), - cv.Optional(CONF_ON_OPEN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), - } - ), - cv.Optional(CONF_ON_CLOSED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), - } - ), + **{ + cv.Optional(conf): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class), + } + ) + for conf, trigger_class in TRIGGERS.items() + }, } ) ) @@ -157,12 +176,14 @@ async def setup_cover_core_(var, config): if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) - for conf in config.get(CONF_ON_OPEN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_CLOSED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + if CONF_ON_OPEN in config: + _LOGGER.warning( + "'on_open' is deprecated, use 'on_opened'. Will be removed in 2026.8.0" + ) + for trigger_conf in TRIGGERS: + for conf in config.get(trigger_conf, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) @@ -258,6 +279,26 @@ async def cover_control_to_code(config, action_id, template_arg, args): return var +COVER_CONDITION_SCHEMA = cv.maybe_simple_value( + {cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID +) + + +async def cover_condition_to_code( + config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType +) -> MockObj: + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +automation.register_condition( + "cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) +automation.register_condition( + "cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index c0345a7cc6..12ec46725d 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -90,44 +90,53 @@ template class CoverPublishAction : public Action { Cover *cover_; }; -template class CoverIsOpenCondition : public Condition { +template class CoverPositionCondition : public Condition { public: - CoverIsOpenCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } + CoverPositionCondition(Cover *cover) : cover_(cover) {} + + bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); } protected: Cover *cover_; }; -template class CoverIsClosedCondition : public Condition { +template using CoverIsOpenCondition = CoverPositionCondition; +template using CoverIsClosedCondition = CoverPositionCondition; + +template class CoverPositionTrigger : public Trigger<> { public: - CoverIsClosedCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } + CoverPositionTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + if (a_cover->position != this->last_position_) { + this->last_position_ = a_cover->position; + if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + this->trigger(); + } + }); + } protected: - Cover *cover_; + float last_position_{NAN}; }; -class CoverOpenTrigger : public Trigger<> { +using CoverOpenedTrigger = CoverPositionTrigger; +using CoverClosedTrigger = CoverPositionTrigger; + +template class CoverTrigger : public Trigger<> { public: - CoverOpenTrigger(Cover *a_cover) { + CoverTrigger(Cover *a_cover) { a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_open()) { - this->trigger(); + auto current_op = a_cover->current_operation; + if (current_op == OP) { + if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { + this->trigger(); + } } + this->last_operation_ = current_op; }); } -}; -class CoverClosedTrigger : public Trigger<> { - public: - CoverClosedTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_closed()) { - this->trigger(); - } - }); - } + protected: + optional last_operation_{}; }; - } // namespace esphome::cover diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 0d9e7e8ffb..37cb908d9f 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -10,9 +10,6 @@ namespace esphome::cover { static const char *const TAG = "cover"; -const float COVER_OPEN = 1.0f; -const float COVER_CLOSED = 0.0f; - const LogString *cover_command_to_str(float pos) { if (pos == COVER_OPEN) { return LOG_STR("OPEN"); diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index e5427ceaa8..0af48f75de 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -10,8 +10,8 @@ namespace esphome::cover { -const extern float COVER_OPEN; -const extern float COVER_CLOSED; +static constexpr float COVER_OPEN = 1.0f; +static constexpr float COVER_CLOSED = 0.0f; #define LOG_COVER(prefix, type, obj) \ if ((obj) != nullptr) { \ diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 9dece7c3a5..d1849efaf7 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -245,6 +245,44 @@ cover: stop_action: - logger.log: stop_action optimistic: true + - platform: template + name: "Template Cover with Triggers" + id: template_cover_with_triggers + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + on_open: + - logger.log: "Cover on_open (deprecated)" + on_opened: + - logger.log: "Cover fully opened" + on_closed: + - logger.log: "Cover fully closed" + on_opening: + - logger.log: "Cover started opening" + on_closing: + - logger.log: "Cover started closing" + on_idle: + - logger.log: "Cover stopped moving" + - logger.log: "Cover stopped moving" + - if: + condition: + cover.is_open: template_cover_with_triggers + then: + logger.log: Cover is open + - if: + condition: + cover.is_closed: template_cover_with_triggers + then: + logger.log: Cover is closed number: - platform: template From 43d9d6fe641e2d9651f25429b1a147b2c811fe20 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:12:42 -0500 Subject: [PATCH 41/95] [esp32] Restore develop branch for dev platform version, bump platformio (#13759) Co-authored-by: Claude --- esphome/components/esp32/__init__.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4c53b42e6f..77753e277f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -517,7 +517,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = { "recommended": cv.Version(55, 3, 36), "latest": cv.Version(55, 3, 36), - "dev": cv.Version(55, 3, 36), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/requirements.txt b/requirements.txt index 821324262b..bb118c5f9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tornado==6.5.4 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile +platformio==6.1.19 esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 From 13ddf267bbb6e15e1d30c43f9a4511c964972d9f Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 4 Feb 2026 21:18:24 +0100 Subject: [PATCH 42/95] [nrf52,zigbee] update warnings (#13761) --- esphome/components/zigbee/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index c044148b32..7e917a9d70 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -84,8 +84,8 @@ def validate_number_of_ep(config: ConfigType) -> None: raise cv.Invalid("At least one zigbee device need to be included") count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if count == 1: - raise cv.Invalid( - "Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888" + _LOGGER.warning( + "Single endpoint requires ZHA or at leatst Zigbee2MQTT 2.8.0. For older versions of Zigbee2MQTT use multiple endpoints" ) if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode: raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}") @@ -151,7 +151,7 @@ def consume_endpoint(config: ConfigType) -> ConfigType: return config if CONF_NAME in config and " " in config[CONF_NAME]: _LOGGER.warning( - "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", + "Spaces in '%s' requires ZHA or at least Zigbee2MQTT 2.8.0. For older version of Zigbee2MQTT use '%s'", config[CONF_NAME], config[CONF_NAME].replace(" ", "_"), ) From 67dfa5e2bc837f98d8f58d613073881d96c0d7fd Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:39:03 +0100 Subject: [PATCH 43/95] [epaper_spi] Validate BUSY pin as input instead of output (#13764) --- esphome/components/epaper_spi/display.py | 78 +++++++++---------- tests/component_tests/epaper_spi/test_init.py | 53 +++++++++++++ 2 files changed, 88 insertions(+), 43 deletions(-) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 8cc7b2663c..13f66691b2 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -76,50 +76,42 @@ def model_schema(config): model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s") ) cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required - return ( - display.FULL_DISPLAY_SCHEMA.extend( - spi.spi_device_schema( - cs_pin_required=False, - default_mode="MODE0", - default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), - ) - ) - .extend( - { - model.option(pin): pins.gpio_output_pin_schema - for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) - } - ) - .extend( - { - cv.Optional(CONF_ROTATION, default=0): validate_rotation, - cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), - cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( - update_interval, cv.Range(min=minimum_update_interval) - ), - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - } - ), - cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), - model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, - cv.GenerateID(): cv.declare_id(class_name), - cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), - cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, - model.option(CONF_ENABLE_PIN): cv.ensure_list( - pins.gpio_output_pin_schema - ), - model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( - map_sequence - ), - model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(max=core.TimePeriod(milliseconds=500)), - ), - } + return display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), ) + ).extend( + { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( + update_interval, cv.Range(min=minimum_update_interval) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), + model.option(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + model.option(CONF_CS_PIN): pins.gpio_output_pin_schema, + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + model.option(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } ) diff --git a/tests/component_tests/epaper_spi/test_init.py b/tests/component_tests/epaper_spi/test_init.py index 71e66cd043..a9f5735fca 100644 --- a/tests/component_tests/epaper_spi/test_init.py +++ b/tests/component_tests/epaper_spi/test_init.py @@ -289,3 +289,56 @@ def test_model_with_full_update_every( "full_update_every": 10, } ) + + +def test_busy_pin_input_mode_ssd1677( + set_core_config: SetCoreConfigCallable, + set_component_config: Callable[[str, Any], None], +) -> None: + """Test that busy_pin has input mode and cs/dc/reset pins have output mode for ssd1677.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + # Configure SPI component which is required by epaper_spi + set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) + + result = run_schema_validation( + { + "id": "test_display", + "model": "ssd1677", + "dc_pin": 21, + "busy_pin": 22, + "reset_pin": 23, + "cs_pin": 5, + "dimensions": { + "width": 200, + "height": 200, + }, + } + ) + + # Verify that busy_pin has input mode set + assert CONF_BUSY_PIN in result + busy_pin_config = result[CONF_BUSY_PIN] + assert "mode" in busy_pin_config + assert busy_pin_config["mode"]["input"] is True + + # Verify that cs_pin has output mode set + assert CONF_CS_PIN in result + cs_pin_config = result[CONF_CS_PIN] + assert "mode" in cs_pin_config + assert cs_pin_config["mode"]["output"] is True + + # Verify that dc_pin has output mode set + assert CONF_DC_PIN in result + dc_pin_config = result[CONF_DC_PIN] + assert "mode" in dc_pin_config + assert dc_pin_config["mode"]["output"] is True + + # Verify that reset_pin has output mode set + assert CONF_RESET_PIN in result + reset_pin_config = result[CONF_RESET_PIN] + assert "mode" in reset_pin_config + assert reset_pin_config["mode"]["output"] is True From 89fc5ebc978325698052579a49c5af780f758b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:18:03 +0100 Subject: [PATCH 44/95] Fix bare hostname ping fallback in dashboard (#13760) --- esphome/dashboard/dns.py | 21 +++++++- tests/dashboard/status/test_dns.py | 82 +++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py index 58867f7bc1..eb4a87dbfb 100644 --- a/esphome/dashboard/dns.py +++ b/esphome/dashboard/dns.py @@ -3,11 +3,16 @@ from __future__ import annotations import asyncio from contextlib import suppress from ipaddress import ip_address +import logging from icmplib import NameLookupError, async_resolve RESOLVE_TIMEOUT = 3.0 +_LOGGER = logging.getLogger(__name__) + +_RESOLVE_EXCEPTIONS = (TimeoutError, NameLookupError, UnicodeError) + async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: """Wrap the icmplib async_resolve function.""" @@ -16,7 +21,21 @@ async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: try: async with asyncio.timeout(RESOLVE_TIMEOUT): return await async_resolve(hostname) - except (TimeoutError, NameLookupError, UnicodeError) as ex: + except _RESOLVE_EXCEPTIONS as ex: + # If the hostname ends with .local and resolution failed, + # try the bare hostname as a fallback since mDNS may not be + # working on the system but unicast DNS might resolve it + if hostname.endswith(".local"): + bare_hostname = hostname[:-6] # Remove ".local" + try: + async with asyncio.timeout(RESOLVE_TIMEOUT): + result = await async_resolve(bare_hostname) + _LOGGER.debug( + "Bare hostname %s resolved to %s", bare_hostname, result + ) + return result + except _RESOLVE_EXCEPTIONS: + _LOGGER.debug("Bare hostname %s also failed to resolve", bare_hostname) return ex diff --git a/tests/dashboard/status/test_dns.py b/tests/dashboard/status/test_dns.py index 9ca48ba2d8..f7c4992079 100644 --- a/tests/dashboard/status/test_dns.py +++ b/tests/dashboard/status/test_dns.py @@ -3,11 +3,12 @@ from __future__ import annotations import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch +from icmplib import NameLookupError import pytest -from esphome.dashboard.dns import DNSCache +from esphome.dashboard.dns import DNSCache, _async_resolve_wrapper @pytest.fixture @@ -119,3 +120,80 @@ def test_async_resolve_not_called(dns_cache_fixture: DNSCache) -> None: result = dns_cache_fixture.get_cached_addresses("valid.com", now) assert result == ["192.168.1.10"] mock_resolve.assert_not_called() + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_ip_address() -> None: + """Test _async_resolve_wrapper returns IP address directly.""" + result = await _async_resolve_wrapper("192.168.1.10") + assert result == ["192.168.1.10"] + + result = await _async_resolve_wrapper("2001:db8::1") + assert result == ["2001:db8::1"] + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_fallback_success() -> None: + """Test _async_resolve_wrapper falls back to bare hostname for .local.""" + mock_resolve = AsyncMock() + # First call (device.local) fails, second call (device) succeeds + mock_resolve.side_effect = [ + NameLookupError("device.local"), + ["192.168.1.50"], + ] + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + assert result == ["192.168.1.50"] + assert mock_resolve.call_count == 2 + mock_resolve.assert_any_call("device.local") + mock_resolve.assert_any_call("device") + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_fallback_both_fail() -> None: + """Test _async_resolve_wrapper returns exception when both fail.""" + mock_resolve = AsyncMock() + original_exception = NameLookupError("device.local") + mock_resolve.side_effect = [ + original_exception, + NameLookupError("device"), + ] + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + # Should return the original exception, not the fallback exception + assert result is original_exception + assert mock_resolve.call_count == 2 + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_non_local_no_fallback() -> None: + """Test _async_resolve_wrapper doesn't fallback for non-.local hostnames.""" + mock_resolve = AsyncMock() + original_exception = NameLookupError("device.example.com") + mock_resolve.side_effect = original_exception + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.example.com") + + assert result is original_exception + # Should only try the original hostname, no fallback + assert mock_resolve.call_count == 1 + mock_resolve.assert_called_once_with("device.example.com") + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_success_no_fallback() -> None: + """Test _async_resolve_wrapper doesn't fallback when .local succeeds.""" + mock_resolve = AsyncMock(return_value=["192.168.1.50"]) + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + assert result == ["192.168.1.50"] + # Should only try once since it succeeded + assert mock_resolve.call_count == 1 + mock_resolve.assert_called_once_with("device.local") From a5568248753345080b8e5524f7f886ddbaf9d8dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:19:13 +0100 Subject: [PATCH 45/95] [logger] Refactor to reduce code duplication and flash size (#13750) --- esphome/components/logger/logger.cpp | 55 +-- esphome/components/logger/logger.h | 407 +++++++++--------- esphome/components/logger/logger_esp32.cpp | 4 +- esphome/components/logger/logger_esp8266.cpp | 2 +- esphome/components/logger/logger_host.cpp | 4 +- .../components/logger/logger_libretiny.cpp | 2 +- esphome/components/logger/logger_rp2040.cpp | 2 +- esphome/components/logger/logger_zephyr.cpp | 4 +- 8 files changed, 234 insertions(+), 246 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 25243ff3f6..54b5670016 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -36,9 +36,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Fast path: main thread, no recursion (99.9% of all logs) if (is_main_task && !this->main_task_recursion_guard_) [[likely]] { - RecursionGuard guard(this->main_task_recursion_guard_); - // Format and send to both console and callbacks - this->log_message_to_buffer_and_send_(level, tag, line, format, args); + this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args); return; } @@ -101,12 +99,9 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; #endif char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety - uint16_t buffer_at = 0; // Initialize buffer position - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, - MAX_CONSOLE_LOG_MSG_SIZE); - // Add newline before writing to console - this->add_newline_to_buffer_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer, buffer_at); + LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + this->write_to_console_(buf); } // RAII guard automatically resets on return @@ -117,9 +112,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch if (level > this->level_for(tag) || global_recursion_guard_) return; - RecursionGuard guard(global_recursion_guard_); - // Format and send to both console and callbacks - this->log_message_to_buffer_and_send_(level, tag, line, format, args); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY @@ -135,28 +128,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas if (level > this->level_for(tag) || global_recursion_guard_) return; - RecursionGuard guard(global_recursion_guard_); - this->tx_buffer_at_ = 0; - - // Write header, format body directly from flash, and write footer - this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_, - reinterpret_cast(format), args); - this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - - // Ensure null termination - uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_; - this->tx_buffer_[null_pos] = '\0'; - - // Listeners get message first (before console write) -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); -#endif - - // Write to console - this->write_tx_buffer_to_console_(); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_STORE_LOG_STR_IN_FLASH @@ -215,10 +187,11 @@ void Logger::process_messages_() { logger::TaskLogBufferHost::LogMessage *message; while (this->log_buffer_->get_message_main_loop(&message)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text, - message->text_length); + message->text_length, buf); this->log_buffer_->release_message_main_loop(); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #elif defined(USE_ESP32) logger::TaskLogBuffer::LogMessage *message; @@ -226,22 +199,24 @@ void Logger::process_messages_() { void *received_token; while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length); + message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(received_token); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #elif defined(USE_LIBRETINY) logger::TaskLogBufferLibreTiny::LogMessage *message; const char *text; while (this->log_buffer_->borrow_message_main_loop(&message, &text)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length); + message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #endif } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 40ac9a38aa..1678fed5f5 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,6 +2,7 @@ #include #include +#include #if defined(USE_ESP32) || defined(USE_HOST) #include #endif @@ -123,6 +124,163 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Buffer wrapper for log formatting functions +struct LogBuffer { + char *data; + uint16_t size; + uint16_t pos{0}; + // Replaces the null terminator with a newline for console output. + // Must be called after notify_listeners_() since listeners need null-terminated strings. + // Console output uses length-based writes (buf.pos), so null terminator is not needed. + void terminate_with_newline() { + if (this->pos < this->size) { + this->data[this->pos++] = '\n'; + } else if (this->size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + this->data[this->size - 1] = '\n'; + this->pos = this->size; + } + } + void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) { + // Early return if insufficient space - intentionally don't update pos to prevent partial writes + if (this->pos + MAX_HEADER_SIZE > this->size) + return; + + char *p = this->current_(); + + // Write ANSI color + this->write_ansi_color_(p, level); + + // Construct: [LEVEL][tag:line] + *p++ = '['; + if (level != 0) { + if (level >= 7) { + *p++ = 'V'; // VERY_VERBOSE = "VV" + *p++ = 'V'; + } else { + *p++ = LOG_LEVEL_LETTER_CHARS[level]; + } + } + *p++ = ']'; + *p++ = '['; + + // Copy tag + this->copy_string_(p, tag); + + *p++ = ':'; + + // Format line number without modulo operations + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + *p++ = '0' + thousands; + line -= thousands * 1000; + } + int hundreds = line / 100; + int remainder = line - hundreds * 100; + int tens = remainder / 10; + *p++ = '0' + hundreds; + *p++ = '0' + tens; + *p++ = '0' + (remainder - tens * 10); + *p++ = ']'; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) + // Write thread name with bold red color + if (thread_name != nullptr) { + this->write_ansi_color_(p, 1); // Bold red for thread name + *p++ = '['; + this->copy_string_(p, thread_name); + *p++ = ']'; + this->write_ansi_color_(p, level); // Restore original color + } +#endif + + *p++ = ':'; + *p++ = ' '; + + this->pos = p - this->data; + } + void HOT format_body(const char *format, va_list args) { + this->format_vsnprintf_(format, args); + this->finalize_(); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void HOT format_body_P(PGM_P format, va_list args) { + this->format_vsnprintf_P_(format, args); + this->finalize_(); + } +#endif + void write_body(const char *text, uint16_t text_length) { + this->write_(text, text_length); + this->finalize_(); + } + + private: + bool full_() const { return this->pos >= this->size; } + uint16_t remaining_() const { return this->size - this->pos; } + char *current_() { return this->data + this->pos; } + void write_(const char *value, uint16_t length) { + const uint16_t available = this->remaining_(); + const uint16_t copy_len = (length < available) ? length : available; + if (copy_len > 0) { + memcpy(this->current_(), value, copy_len); + this->pos += copy_len; + } + } + void finalize_() { + // Write color reset sequence + static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; + this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); + // Null terminate + this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; + } + void strip_trailing_newlines_() { + while (this->pos > 0 && this->data[this->pos - 1] == '\n') + this->pos--; + } + void process_vsnprintf_result_(int ret) { + if (ret < 0) + return; + const uint16_t rem = this->remaining_(); + this->pos += (ret >= rem) ? (rem - 1) : static_cast(ret); + this->strip_trailing_newlines_(); + } + void format_vsnprintf_(const char *format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args)); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void format_vsnprintf_P_(PGM_P format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); + } +#endif + // Write ANSI color escape sequence to buffer, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void write_ansi_color_(char *&p, uint8_t level) { + if (level == 0) + return; + // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes) + *p++ = '\033'; + *p++ = '['; + *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold + *p++ = ';'; + *p++ = '3'; + *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = 'm'; + } + // Copy string without null terminator, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void copy_string_(char *&p, const char *str) { + const size_t len = strlen(str); + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by + // piece + memcpy(p, str, len); + p += len; + } +}; + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -260,114 +418,83 @@ class Logger : public Component { #endif #endif void process_messages_(); - void write_msg_(const char *msg, size_t len); + void write_msg_(const char *msg, uint16_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator - // It's the caller's responsibility to initialize buffer_at (typically to 0) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, - va_list args, char *buffer, uint16_t *buffer_at, - uint16_t buffer_size) { + va_list args, LogBuffer &buf) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) - this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); + buf.write_header(level, tag, line, this->get_thread_name_()); #elif defined(USE_ZEPHYR) - char buff[MAX_POINTER_REPRESENTATION]; - this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size); + char tmp[MAX_POINTER_REPRESENTATION]; + buf.write_header(level, tag, line, this->get_thread_name_(tmp)); #else - this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); + buf.write_header(level, tag, line, nullptr); #endif - this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, args); - this->write_footer_to_buffer_(buffer, buffer_at, buffer_size); - - // Always ensure the buffer has a null terminator, even if we need to - // overwrite the last character of the actual content - if (*buffer_at >= buffer_size) { - buffer[buffer_size - 1] = '\0'; // Truncate and ensure null termination - } else { - buffer[*buffer_at] = '\0'; // Normal case, append null terminator - } + buf.format_body(format, args); } - // Helper to add newline to buffer before writing to console - // Modifies buffer_at to include the newline - inline void HOT add_newline_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - // Add newline - don't need to maintain null termination - // write_msg_ receives explicit length, so we can safely overwrite the null terminator - // This is safe because: - // 1. Callbacks already received the message (before we add newline) - // 2. write_msg_ receives the length explicitly (doesn't need null terminator) - if (*buffer_at < buffer_size) { - buffer[(*buffer_at)++] = '\n'; - } else if (buffer_size > 0) { - // Buffer was full - replace last char with newline to ensure it's visible - buffer[buffer_size - 1] = '\n'; - *buffer_at = buffer_size; - } +#ifdef USE_STORE_LOG_STR_IN_FLASH + // Format a log message with flash string format and write it to a buffer with header, footer, and null terminator + inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line, + const __FlashStringHelper *format, va_list args, + LogBuffer &buf) { + buf.write_header(level, tag, line, nullptr); + buf.format_body_P(reinterpret_cast(format), args); + } +#endif + + // Helper to notify log listeners + inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { +#ifdef USE_LOG_LISTENERS + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, buf.data, buf.pos); +#endif } - // Helper to write tx_buffer_ to console if logging is enabled - // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null - inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { - if (this->baud_rate_ > 0) { - uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; - this->add_newline_to_buffer_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); - this->write_msg_(this->tx_buffer_ + offset, *len_ptr); - } + // Helper to write log buffer to console (replaces null terminator with newline and writes) + inline void HOT write_to_console_(LogBuffer &buf) { + buf.terminate_with_newline(); + this->write_msg_(buf.data, buf.pos); + } + + // Helper to write log buffer to console if logging is enabled + inline void HOT write_log_buffer_to_console_(LogBuffer &buf) { + if (this->baud_rate_ > 0) + this->write_to_console_(buf); } // Helper to format and send a log message to both console and listeners - inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, - va_list args) { - // Format to tx_buffer and prepare for output - this->tx_buffer_at_ = 0; // Initialize buffer position - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - - // Listeners get message WITHOUT newline (for API/MQTT/syslog) -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); + // Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings + template + inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, + FormatType format, va_list args) { + RecursionGuard guard(recursion_guard); + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; +#ifdef USE_STORE_LOG_STR_IN_FLASH + if constexpr (std::is_same_v) { + this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf); + } else #endif - - // Console gets message WITH newline (if platform needs it) - this->write_tx_buffer_to_console_(); + { + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + } + this->notify_listeners_(level, tag, buf); + this->write_log_buffer_to_console_(buf); } #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Helper to format a pre-formatted message from the task log buffer and notify listeners // Used by process_messages_ to avoid code duplication between ESP32 and host platforms inline void HOT format_buffered_message_and_notify_(uint8_t level, const char *tag, uint16_t line, - const char *thread_name, const char *text, size_t text_length) { - this->tx_buffer_at_ = 0; - this->write_header_to_buffer_(level, tag, line, thread_name, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - this->write_body_to_buffer_(text, text_length, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->tx_buffer_[this->tx_buffer_at_] = '\0'; -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); -#endif + const char *thread_name, const char *text, uint16_t text_length, + LogBuffer &buf) { + buf.write_header(level, tag, line, thread_name); + buf.write_body(text, text_length); + this->notify_listeners_(level, tag, buf); } #endif - // Write the body of the log message to the buffer - inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, - uint16_t buffer_size) { - // Calculate available space - if (*buffer_at >= buffer_size) - return; - const uint16_t available = buffer_size - *buffer_at; - - // Determine copy length (minimum of remaining capacity and string length) - const size_t copy_len = (length < static_cast(available)) ? length : available; - - // Copy the data - if (copy_len > 0) { - memcpy(buffer + *buffer_at, value, copy_len); - *buffer_at += copy_len; - } - } - #ifndef USE_HOST const LogString *get_uart_selection_(); #endif @@ -421,7 +548,6 @@ class Logger : public Component { #endif // Group smaller types together at the end - uint16_t tx_buffer_at_{0}; uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) @@ -525,117 +651,6 @@ class Logger : public Component { } #endif - static inline void copy_string(char *buffer, uint16_t &pos, const char *str) { - const size_t len = strlen(str); - // Intentionally no null terminator, building larger string - memcpy(buffer + pos, str, len); // NOLINT(bugprone-not-null-terminated-result) - pos += len; - } - - static inline void write_ansi_color_for_level(char *buffer, uint16_t &pos, uint8_t level) { - if (level == 0) - return; - // Construct ANSI escape sequence: "\033[{bold};3{color}m" - // Example: "\033[1;31m" for ERROR (bold red) - buffer[pos++] = '\033'; - buffer[pos++] = '['; - buffer[pos++] = (level == 1) ? '1' : '0'; // Only ERROR is bold - buffer[pos++] = ';'; - buffer[pos++] = '3'; - buffer[pos++] = LOG_LEVEL_COLOR_DIGIT[level]; - buffer[pos++] = 'm'; - } - - inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, - char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - uint16_t pos = *buffer_at; - // Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes - if (pos + MAX_HEADER_SIZE > buffer_size) - return; - - // Construct: [LEVEL][tag:line]: - write_ansi_color_for_level(buffer, pos, level); - buffer[pos++] = '['; - if (level != 0) { - if (level >= 7) { - buffer[pos++] = 'V'; // VERY_VERBOSE = "VV" - buffer[pos++] = 'V'; - } else { - buffer[pos++] = LOG_LEVEL_LETTER_CHARS[level]; - } - } - buffer[pos++] = ']'; - buffer[pos++] = '['; - copy_string(buffer, pos, tag); - buffer[pos++] = ':'; - // Format line number without modulo operations (passed by value, safe to mutate) - if (line > 999) [[unlikely]] { - int thousands = line / 1000; - buffer[pos++] = '0' + thousands; - line -= thousands * 1000; - } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - buffer[pos++] = '0' + hundreds; - buffer[pos++] = '0' + tens; - buffer[pos++] = '0' + (remainder - tens * 10); - buffer[pos++] = ']'; - -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) - if (thread_name != nullptr) { - write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name - buffer[pos++] = '['; - copy_string(buffer, pos, thread_name); - buffer[pos++] = ']'; - write_ansi_color_for_level(buffer, pos, level); // Restore original color - } -#endif - - buffer[pos++] = ':'; - buffer[pos++] = ' '; - *buffer_at = pos; - } - - // Helper to process vsnprintf return value and strip trailing newlines. - // Updates buffer_at with the formatted length, handling truncation: - // - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator - // - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator - __attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at, - uint16_t remaining, int ret) { - if (ret < 0) - return; // Encoding error, do not increment buffer_at - *buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast(ret); - // Remove all trailing newlines right after formatting - while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') - (*buffer_at)--; - } - - inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, - va_list args) { - // Check remaining capacity in the buffer - if (*buffer_at >= buffer_size) - return; - const uint16_t remaining = buffer_size - *buffer_at; - process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args)); - } - -#ifdef USE_STORE_LOG_STR_IN_FLASH - // ESP8266 variant that reads format string directly from flash using vsnprintf_P - inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format, - va_list args) { - if (*buffer_at >= buffer_size) - return; - const uint16_t remaining = buffer_size - *buffer_at; - process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args)); - } -#endif - - inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; - this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); - } - #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 9defb6c166..dfa643d5e9 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -118,9 +118,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { - // Length is now always passed explicitly - no strlen() fallback needed - +void HOT Logger::write_msg_(const char *msg, uint16_t len) { #if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) // USB CDC/JTAG - single write including newline (already in buffer) // Use fwrite to stdout which goes through VFS to USB console diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 6cee1baca5..0a3433d132 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -28,7 +28,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) this->hw_serial_->write(msg, len); } diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 874cdabd22..be12b6df6a 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { static constexpr size_t TIMESTAMP_LEN = 10; // "[HH:MM:SS]" // tx_buffer_size_ defaults to 512, so 768 covers default + headroom char buffer[TIMESTAMP_LEN + 768]; @@ -15,7 +15,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { size_t pos = strftime(buffer, TIMESTAMP_LEN + 1, "[%H:%M:%S]", &timeinfo); // Copy message (with newline already included by caller) - size_t copy_len = std::min(len, sizeof(buffer) - pos); + size_t copy_len = std::min(static_cast(len), sizeof(buffer) - pos); memcpy(buffer + pos, msg, copy_len); pos += copy_len; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index cdf55e710c..aab8a97abf 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } +void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index be8252f56a..1f435031f6 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) this->hw_serial_->write(msg, len); } diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 41f53beec0..ef1702c5c1 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -63,7 +63,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) #ifdef CONFIG_PRINTK // Requires the debug component and an active SWD connection. @@ -73,7 +73,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { if (this->uart_dev_ == nullptr) { return; } - for (size_t i = 0; i < len; ++i) { + for (uint16_t i = 0; i < len; ++i) { uart_poll_out(this->uart_dev_, msg[i]); } } From 25c0073b2d60e99012264a2149696cd50970af44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:20:04 +0100 Subject: [PATCH 46/95] [web_server] Fix ESP8266 watchdog panic by deferring actions to main loop (#13765) --- esphome/components/web_server/web_server.cpp | 15 +-------------- esphome/components/web_server/web_server.h | 11 +++++------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index d30cb524f4..a3bf1e6b02 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -721,11 +721,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } if (action != SWITCH_ACTION_NONE) { -#ifdef USE_ESP8266 - execute_switch_action(obj, action); -#else this->defer([obj, action]() { execute_switch_action(obj, action); }); -#endif request->send(200); } else { request->send(404); @@ -1645,11 +1641,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } if (action != LOCK_ACTION_NONE) { -#ifdef USE_ESP8266 - execute_lock_action(obj, action); -#else this->defer([obj, action]() { execute_lock_action(obj, action); }); -#endif request->send(200); } else { request->send(404); @@ -2015,19 +2007,14 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur return; } -#ifdef USE_ESP8266 - // ESP8266 is single-threaded, call directly - call.set_raw_timings_base64url(encoded); - call.perform(); -#else // Defer to main loop for thread safety. Move encoded string into lambda to ensure // it outlives the call - set_raw_timings_base64url stores a pointer, so the string // must remain valid until perform() completes. + // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. this->defer([call, encoded = std::move(encoded)]() mutable { call.set_raw_timings_base64url(encoded); call.perform(); }); -#endif request->send(200); return; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 92a5c7edee..224c051ece 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -42,13 +42,12 @@ using ParamNameType = const __FlashStringHelper *; using ParamNameType = const char *; #endif -// ESP8266 is single-threaded, so actions can execute directly in request context. -// Multi-core platforms need to defer to main loop thread for thread safety. -#ifdef USE_ESP8266 -#define DEFER_ACTION(capture, action) action -#else +// All platforms need to defer actions to main loop thread. +// Multi-core platforms need this for thread safety. +// ESP8266 needs this because ESPAsyncWebServer callbacks run in "sys" context +// (SDK system context), not "cont" context (continuation/main loop). Calling +// yield() from sys context causes a panic in the Arduino core. #define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; }) -#endif /// Result of matching a URL against an entity struct EntityMatchResult { From c27870b15d076e61632efc1637629104817d1064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:36:40 +0100 Subject: [PATCH 47/95] [web_server] Add some more missing ESPHOME_F macros (#13748) --- esphome/components/web_server/web_server.cpp | 10 +++++----- esphome/components/web_server/web_server_v1.cpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a3bf1e6b02..42219f3aac 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -454,7 +454,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse(200, ""); + AsyncWebServerResponse *response = request->beginResponse(200, ESPHOME_F("")); response->addHeader(ESPHOME_F("Access-Control-Allow-Private-Network"), ESPHOME_F("true")); response->addHeader(ESPHOME_F("Private-Network-Access-Name"), App.get_name().c_str()); char mac_s[18]; @@ -1967,7 +1967,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Only allow transmit if the device supports it if (!obj->has_transmitter()) { - request->send(400, ESPHOME_F("text/plain"), "Device does not support transmission"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Device does not support transmission")); return; } @@ -1993,7 +1993,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) if (!request->hasParam(ESPHOME_F("data"))) { - request->send(400, ESPHOME_F("text/plain"), "Missing 'data' parameter"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); return; } @@ -2003,7 +2003,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Validate base64url is not empty if (encoded.empty()) { - request->send(400, ESPHOME_F("text/plain"), "Empty 'data' parameter"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter")); return; } @@ -2472,7 +2472,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { else { // No matching handler found - send 404 ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); - request->send(404, "text/plain", "Not Found"); + request->send(404, ESPHOME_F("text/plain"), ESPHOME_F("Not Found")); } } diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index ae4bbfa557..f7b90018dc 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -74,7 +74,7 @@ void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("text/html"); + AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); const std::string &title = App.get_name(); stream->print(ESPHOME_F("")); From 7bd8b08e16493259df06d0c3d5e4ac035bb44e25 Mon Sep 17 00:00:00 2001 From: Jas Strong <jasmine@electronpusher.org> Date: Thu, 5 Feb 2026 00:06:52 -0800 Subject: [PATCH 48/95] [rd03d] Revert incorrect field order swap (#13769) Co-authored-by: jas <jas@asspa.in> --- esphome/components/rd03d/rd03d.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index ba05abe8e0..090e4dcf32 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -132,18 +132,15 @@ void RD03DComponent::process_frame_() { // Header is 4 bytes, each target is 8 bytes uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); - // Extract raw bytes for this target - // Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution, - // actual radar output has Resolution before Speed (verified empirically - - // stationary targets were showing non-zero speed with original field order) + // Extract raw bytes for this target (per datasheet Table 5-2: X, Y, Speed, Resolution) uint8_t x_low = this->buffer_[offset + 0]; uint8_t x_high = this->buffer_[offset + 1]; uint8_t y_low = this->buffer_[offset + 2]; uint8_t y_high = this->buffer_[offset + 3]; - uint8_t res_low = this->buffer_[offset + 4]; - uint8_t res_high = this->buffer_[offset + 5]; - uint8_t speed_low = this->buffer_[offset + 6]; - uint8_t speed_high = this->buffer_[offset + 7]; + uint8_t speed_low = this->buffer_[offset + 4]; + uint8_t speed_high = this->buffer_[offset + 5]; + uint8_t res_low = this->buffer_[offset + 6]; + uint8_t res_high = this->buffer_[offset + 7]; // Decode values per RD-03D format int16_t x = decode_value(x_low, x_high); From be44d4801f578aeb4d6561f3a075872f4241724a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 5 Feb 2026 10:52:43 +0100 Subject: [PATCH 49/95] [esp32] Reduce Arduino build size by 44% and build time by 36% (#13623) --- esphome/components/bme680_bsec/__init__.py | 3 +- esphome/components/esp32/__init__.py | 266 +++++++++++++++++- esphome/components/esp32/const.py | 1 + esphome/components/espnow/__init__.py | 5 +- esphome/components/ethernet/__init__.py | 3 - .../i2s_audio/media_player/__init__.py | 1 + esphome/components/midea/climate.py | 5 +- esphome/components/network/__init__.py | 3 +- .../components/web_server_base/__init__.py | 5 +- esphome/components/wled/__init__.py | 3 + esphome/core/__init__.py | 10 + esphome/writer.py | 15 +- .../components/i2s_audio/test.esp32-ard.yaml | 16 ++ tests/unit_tests/test_core.py | 75 +++++ 14 files changed, 384 insertions(+), 27 deletions(-) create mode 100644 tests/components/i2s_audio/test.esp32-ard.yaml diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 06e641d34d..a86e061cd4 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -89,8 +89,9 @@ async def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) - # Although this component does not use SPI, the BSEC library requires the SPI library + # Although this component does not use SPI/Wire directly, the BSEC library requires them cg.add_library("SPI", None) + cg.add_library("Wire", None) cg.add_define("USE_BSEC") cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 77753e277f..e1e3164b68 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -46,10 +46,11 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache +from esphome.writer import clean_cmake_cache, rmtree from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa + KEY_ARDUINO_LIBRARIES, KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, @@ -152,6 +153,168 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation ) +# Additional IDF managed components to exclude for Arduino framework builds +# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome +# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because +# that's how managed components are registered in the IDF build system +# List includes direct dependencies from arduino-esp32/idf_component.yml +# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need) +ARDUINO_EXCLUDED_IDF_COMPONENTS = ( + "chmorgan__esp-libhelix-mp3", # MP3 decoder - not used + "espressif__cbor", # CBOR library - only used by RainMaker/Insights + "espressif__esp-dsp", # DSP library - not used + "espressif__esp-modbus", # Modbus - ESPHome has its own + "espressif__esp-sr", # Speech recognition - not used + "espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used + "espressif__esp-zigbee-lib", # Zigbee library - not used + "espressif__esp_diag_data_store", # Diagnostics - not used + "espressif__esp_diagnostics", # Diagnostics - not used + "espressif__esp_hosted", # ESP hosted - only for ESP32-P4 + "espressif__esp_insights", # ESP Insights - not used + "espressif__esp_modem", # Modem library - not used + "espressif__esp_rainmaker", # RainMaker - not used + "espressif__esp_rcp_update", # RCP update - RainMaker transitive dep + "espressif__esp_schedule", # Schedule - RainMaker transitive dep + "espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep + "espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4 + "espressif__json_generator", # JSON generator - RainMaker transitive dep + "espressif__json_parser", # JSON parser - RainMaker transitive dep + "espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly + "espressif__libsodium", # Crypto - ESPHome uses its own noise-c library + "espressif__network_provisioning", # Network provisioning - not used + "espressif__qrcode", # QR code - not used + "espressif__rmaker_common", # RainMaker common - not used + "joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem +) + +# Mapping of Arduino libraries to IDF managed components they require +# When an Arduino library is enabled via cg.add_library(), these components +# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS. +# +# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have +# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle +# their managed component dependencies. Our mapping is primarily needed for libraries +# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.) +# and to ensure the stubs are removed from our idf_component.yml overrides. +ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { + "BLE": ("esp_driver_gptimer",), + "BluetoothSerial": ("esp_driver_gptimer",), + "ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"), + "ESP_SR": ("espressif__esp-sr",), + "Ethernet": ("espressif__lan867x",), + "FFat": ("fatfs",), + "Insights": ( + "espressif__cbor", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", # Transitive dep from esp_insights + ), + "LittleFS": ("joltwallet__littlefs",), + "Matter": ("espressif__esp_matter",), + "PPP": ("espressif__esp_modem",), + "RainMaker": ( + # Direct deps from idf_component.yml + "espressif__cbor", + "espressif__esp_rainmaker", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", + "espressif__qrcode", + # Transitive deps from esp_rainmaker + "espressif__esp_rcp_update", + "espressif__esp_schedule", + "espressif__esp_secure_cert_mgr", + "espressif__json_generator", + "espressif__json_parser", + "espressif__network_provisioning", + ), + "SD": ("fatfs",), + "SD_MMC": ("fatfs",), + "SPIFFS": ("spiffs",), + "WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"), + "Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"), +} + +# Arduino library to Arduino library dependencies +# When enabling one library, also enable its dependencies +# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION +ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = { + "Ethernet": ("Network",), + "WiFi": ("Network",), +} + + +def _idf_component_stub_name(component: str) -> str: + """Get stub directory name from IDF component name. + + Component names are typically namespace__name (e.g., espressif__cbor). + Returns just the name part (e.g., cbor). If no namespace is present, + returns the original component name. + """ + _prefix, sep, suffix = component.partition("__") + return suffix if sep else component + + +def _idf_component_dep_name(component: str) -> str: + """Convert IDF component name to dependency format. + + Converts espressif__cbor to espressif/cbor. + """ + return component.replace("__", "/") + + +# Arduino libraries to disable by default when using Arduino framework +# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core +# (HardwareSerial, Print, Stream, GPIO functions which are always compiled) +# Components use cg.add_library() which auto-enables any they need +# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt +ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset( + { + "ArduinoOTA", + "AsyncUDP", + "BLE", + "BluetoothSerial", + "DNSServer", + "EEPROM", + "ESP_HostedOTA", + "ESP_I2S", + "ESP_NOW", + "ESP_SR", + "ESPmDNS", + "Ethernet", + "FFat", + "FS", + "Hash", + "HTTPClient", + "HTTPUpdate", + "Insights", + "LittleFS", + "Matter", + "NetBIOS", + "Network", + "NetworkClientSecure", + "OpenThread", + "PPP", + "Preferences", + "RainMaker", + "SD", + "SD_MMC", + "SimpleBLE", + "SPI", + "SPIFFS", + "Ticker", + "Update", + "USB", + "WebServer", + "WiFi", + "WiFiProv", + "Wire", + "Zigbee", + } +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -243,7 +406,13 @@ def set_core_data(config): CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} # Initialize with default exclusions - components can call include_builtin_idf_component() # to re-enable any they need - CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + # Add Arduino-specific managed component exclusions when using Arduino framework + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded + # Initialize Arduino library tracking - cg.add_library() auto-enables libraries + CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set() CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -391,6 +560,26 @@ def include_builtin_idf_component(name: str) -> None: CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) +def _enable_arduino_library(name: str) -> None: + """Enable an Arduino library that is disabled by default. + + This is called automatically by CORE.add_library() when a component adds + an Arduino library via cg.add_library(). Components should not call this + directly - just use cg.add_library("LibName", None). + + Args: + name: The library name (e.g., "Wire", "SPI", "WiFi") + """ + enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] + enabled_libs.add(name) + # Also enable any required Arduino library dependencies + for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()): + enabled_libs.add(dep_lib) + # Also enable any required IDF components + for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()): + include_builtin_idf_component(idf_component) + + def add_extra_script(stage: str, filename: str, path: Path): """Add an extra script to the project.""" key = f"{stage}:{filename}" @@ -1132,6 +1321,27 @@ async def _write_exclude_components() -> None: ) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libraries_sdkconfig() -> None: + """Write Arduino selective compilation sdkconfig after all components have added libraries. + + This must run at FINAL priority so that all components have had a chance to call + cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library(). + """ + if KEY_ESP32 not in CORE.data: + return + # Enable Arduino selective compilation to disable unused Arduino libraries + # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core + # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) + # cg.add_library() auto-enables needed libraries; users can also add + # libraries via esphome: libraries: config which calls cg.add_library() + add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + for lib in ARDUINO_DISABLED_LIBRARIES: + # Enable if explicitly requested, disable otherwise + add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs) + + @coroutine_with_priority(CoroPriority.FINAL) async def _add_yaml_idf_components(components: list[ConfigType]): """Add IDF components from YAML config with final priority to override code-added components.""" @@ -1550,6 +1760,11 @@ async def to_code(config): # Default exclusions are added in set_core_data() during config validation. CORE.add_job(_write_exclude_components) + # Write Arduino selective compilation sdkconfig at FINAL priority after all + # components have had a chance to call cg.add_library() to enable libraries they need. + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + CORE.add_job(_write_arduino_libraries_sdkconfig) + APP_PARTITION_SIZES = { "2MB": 0x0C0000, # 768 KB @@ -1630,11 +1845,49 @@ def _write_sdkconfig(): def _write_idf_component_yml(): yml_path = CORE.relative_build_path("src/idf_component.yml") + dependencies: dict[str, dict] = {} + + # For Arduino builds, override unused managed components from the Arduino framework + # by pointing them to empty stub directories using override_path + # This prevents the IDF component manager from downloading the real components + if CORE.using_arduino: + # Determine which IDF components are needed by enabled Arduino libraries + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + required_idf_components = { + comp + for lib in enabled_libs + for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ()) + } + + # Only stub components that are not required by any enabled Arduino library + components_to_stub = ( + set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components + ) + + stubs_dir = CORE.relative_build_path("component_stubs") + stubs_dir.mkdir(exist_ok=True) + for component_name in components_to_stub: + # Create stub directory with minimal CMakeLists.txt + stub_path = stubs_dir / _idf_component_stub_name(component_name) + stub_path.mkdir(exist_ok=True) + stub_cmake = stub_path / "CMakeLists.txt" + if not stub_cmake.exists(): + stub_cmake.write_text("idf_component_register()\n") + dependencies[_idf_component_dep_name(component_name)] = { + "version": "*", + "override_path": str(stub_path), + } + + # Remove stubs for components that are now required by enabled libraries + for component_name in required_idf_components: + stub_path = stubs_dir / _idf_component_stub_name(component_name) + if stub_path.exists(): + rmtree(stub_path) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - dependencies = {} for name, component in components.items(): - dependency = {} + dependency: dict[str, str] = {} if component[KEY_REF]: dependency["version"] = component[KEY_REF] if component[KEY_REPO]: @@ -1642,9 +1895,8 @@ def _write_idf_component_yml(): if component[KEY_PATH]: dependency["path"] = component[KEY_PATH] dependencies[name] = dependency - contents = yaml_util.dump({"dependencies": dependencies}) - else: - contents = "" + + contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else "" if write_file_if_changed(yml_path, contents): dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index db3eddebd5..7874c1c759 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -7,6 +7,7 @@ KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_COMPONENTS = "components" KEY_EXCLUDE_COMPONENTS = "exclude_components" +KEY_ARDUINO_LIBRARIES = "arduino_libraries" KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 1f5ca1104a..faeccd910e 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WIFI, ) -from esphome.core import CORE, HexInt +from esphome.core import HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -124,9 +124,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.using_arduino: - cg.add_library("WiFi", None) - # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks # This enables low-latency event processing instead of waiting for select() timeout socket.require_wake_loop_threadsafe() diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 23436cc5be..38489ceb2b 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -431,9 +431,6 @@ async def to_code(config): # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") - if CORE.using_arduino: - cg.add_library("WiFi", None) - if on_connect_config := config.get(CONF_ON_CONNECT): cg.add_define("USE_ETHERNET_CONNECT_TRIGGER") await automation.build_automation( diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 35c42e1b06..426b211f47 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -114,6 +114,7 @@ async def to_code(config): cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) + cg.add_library("WiFi", None) cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) cg.add_library("esphome/ESP32-audioI2S", "2.3.0") diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index b08a47afa9..8a3d4f22ba 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -30,7 +30,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_WATT, ) -from esphome.core import coroutine +from esphome.core import CORE, coroutine CODEOWNERS = ["@dudanov"] DEPENDENCIES = ["climate", "uart"] @@ -290,4 +290,7 @@ async def to_code(config): if CONF_HUMIDITY_SETPOINT in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) cg.add(var.set_humidity_setpoint_sensor(sens)) + # MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping) + if CORE.is_esp32: + cg.add_library("WiFi", None) cg.add_library("dudanov/MideaUART", "1.1.9") diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 5b63bbfce9..1f75b12178 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -137,8 +137,7 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") - if CORE.using_arduino and CORE.is_esp32: - cg.add_library("Networking", None) + # ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed # Apply high performance networking settings # Config can explicitly enable/disable, or default to component-driven behavior diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 6326b4d6ff..11408ae260 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -38,11 +38,8 @@ async def to_code(config): cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) return + # ESP32 uses IDF web server (early return above), so this is for other Arduino platforms if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("WiFi", None) - cg.add_library("FS", None) - cg.add_library("Update", None) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) if CORE.is_libretiny: diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index fb20a03010..49eb15dad6 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -3,6 +3,7 @@ from esphome.components.light.effects import register_addressable_effect from esphome.components.light.types import AddressableLightEffect import esphome.config_validation as cv from esphome.const import CONF_NAME, CONF_PORT +from esphome.core import CORE wled_ns = cg.esphome_ns.namespace("wled") WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect) @@ -27,4 +28,6 @@ async def wled_light_effect_to_code(config, effect_id): cg.add(effect.set_port(config[CONF_PORT])) cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK])) cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START])) + if CORE.is_esp32: + cg.add_library("WiFi", None) return effect diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 5308ad241e..484f679369 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -892,6 +892,16 @@ class EsphomeCore: library.name if "/" not in library.name else library.name.split("/")[-1] ) + # Auto-enable Arduino libraries on ESP32 Arduino builds + if self.is_esp32 and self.using_arduino: + from esphome.components.esp32 import ( + ARDUINO_DISABLED_LIBRARIES, + _enable_arduino_library, + ) + + if short_name in ARDUINO_DISABLED_LIBRARIES: + _enable_arduino_library(short_name) + if short_name not in self.platformio_libraries: _LOGGER.debug("Adding library: %s", library) self.platformio_libraries[short_name] = library diff --git a/esphome/writer.py b/esphome/writer.py index cb9c921693..661118e518 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -421,6 +421,11 @@ def _rmtree_error_handler( func(path) +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows.""" + shutil.rmtree(path, onerror=_rmtree_error_handler) + + def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): @@ -430,11 +435,11 @@ def clean_build(clear_pio_cache: bool = True): pioenvs = CORE.relative_pioenvs_path() if pioenvs.is_dir(): _LOGGER.info("Deleting %s", pioenvs) - shutil.rmtree(pioenvs, onerror=_rmtree_error_handler) + rmtree(pioenvs) piolibdeps = CORE.relative_piolibdeps_path() if piolibdeps.is_dir(): _LOGGER.info("Deleting %s", piolibdeps) - shutil.rmtree(piolibdeps, onerror=_rmtree_error_handler) + rmtree(piolibdeps) dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): _LOGGER.info("Deleting %s", dependencies_lock) @@ -455,7 +460,7 @@ def clean_build(clear_pio_cache: bool = True): cache_dir = Path(config.get("platformio", "cache_dir")) if cache_dir.is_dir(): _LOGGER.info("Deleting PlatformIO cache %s", cache_dir) - shutil.rmtree(cache_dir, onerror=_rmtree_error_handler) + rmtree(cache_dir) def clean_all(configuration: list[str]): @@ -480,7 +485,7 @@ def clean_all(configuration: list[str]): if item.is_file() and not item.name.endswith(".json"): item.unlink() elif item.is_dir() and item.name != "storage": - shutil.rmtree(item, onerror=_rmtree_error_handler) + rmtree(item) # Clean PlatformIO project files try: @@ -494,7 +499,7 @@ def clean_all(configuration: list[str]): path = Path(config.get("platformio", pio_dir)) if path.is_dir(): _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) - shutil.rmtree(path, onerror=_rmtree_error_handler) + rmtree(path) GITIGNORE_CONTENT = """# Gitignore settings for ESPHome diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml new file mode 100644 index 0000000000..4276b4f922 --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +substitutions: + i2s_bclk_pin: GPIO15 + i2s_lrclk_pin: GPIO4 + i2s_mclk_pin: GPIO5 + +<<: !include common.yaml + +wifi: + ssid: test + password: test1234 + +media_player: + - platform: i2s_audio + name: Test Media Player + dac_type: internal + mode: stereo diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 1fc8dab358..174b3fec85 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -780,3 +780,78 @@ class TestEsphomeCore: target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}} assert target.has_networking is False + + def test_add_library__esp32_arduino_enables_disabled_library(self, target): + """Test add_library auto-enables Arduino libraries on ESP32 Arduino builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("WiFi") + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp32_arduino_ignores_non_arduino_library(self, target): + """Test add_library doesn't enable libraries not in ARDUINO_DISABLED_LIBRARIES.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("SomeOtherLib", "1.0.0") + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "SomeOtherLib" in target.platformio_libraries + + def test_add_library__esp32_idf_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP32 IDF builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "esp-idf", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp8266_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP8266.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp8266", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__extracts_short_name_from_path(self, target): + """Test add_library extracts short name from library paths like owner/lib.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("arduino/Wire", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("Wire") + + assert "Wire" in target.platformio_libraries From ed8c0dc99d519f492e51d1f054f5ea9d5e07fe5b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Feb 2026 05:55:08 -0500 Subject: [PATCH 50/95] [esp32] Skip downloading precompiled Arduino libs (#13775) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 33 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index e1e3164b68..f5a8bc8994 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -95,6 +95,11 @@ CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" +ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" +ARDUINO_LIBS_NAME = f"{ARDUINO_FRAMEWORK_NAME}-libs" +ARDUINO_LIBS_PKG = f"pioarduino/{ARDUINO_LIBS_NAME}" + LOG_LEVELS_IDF = [ "NONE", "ERROR", @@ -599,14 +604,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool: def _format_framework_arduino_version(ver: cv.Version) -> str: - # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to - # a PIO pioarduino/framework-arduinoespressif32 value # 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz if ver >= cv.Version(3, 3, 6): filename = f"esp32-core-{ver}.tar.xz" else: filename = f"esp32-{ver}.zip" - return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" + return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: @@ -741,9 +744,7 @@ def _check_versions(config): CONF_SOURCE, _format_framework_arduino_version(version) ) if _is_framework_url(value[CONF_SOURCE]): - value[CONF_SOURCE] = ( - f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" - ) + value[CONF_SOURCE] = f"{ARDUINO_FRAMEWORK_PKG}@{value[CONF_SOURCE]}" else: if version < cv.Version(5, 0, 0): raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") @@ -1321,6 +1322,20 @@ async def _write_exclude_components() -> None: ) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libs_stub(stubs_dir: Path, idf_ver: cv.Version) -> None: + """Write stub package to skip downloading precompiled Arduino libs.""" + stubs_dir.mkdir(parents=True, exist_ok=True) + write_file_if_changed( + stubs_dir / "package.json", + f'{{"name":"{ARDUINO_LIBS_NAME}","version":"{idf_ver.major}.{idf_ver.minor}.{idf_ver.patch}"}}', + ) + write_file_if_changed( + stubs_dir / "tools.json", + '{"packages":[{"platforms":[{"toolsDependencies":[]}],"tools":[]}]}', + ) + + @coroutine_with_priority(CoroPriority.FINAL) async def _write_arduino_libraries_sdkconfig() -> None: """Write Arduino selective compilation sdkconfig after all components have added libraries. @@ -1451,6 +1466,12 @@ async def to_code(config): "platform_packages", [_format_framework_espidf_version(idf_ver, None)], ) + # Use stub package to skip downloading precompiled libs + stubs_dir = CORE.relative_build_path("arduino-libs-stub") + cg.add_platformio_option( + "platform_packages", [f"{ARDUINO_LIBS_PKG}@file://{stubs_dir}"] + ) + CORE.add_job(_write_arduino_libs_stub, stubs_dir, idf_ver) # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency if get_esp32_variant() == VARIANT_ESP32S2: From 9ea846144008ff78f082e0987132047b7092a726 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:41:17 -0500 Subject: [PATCH 51/95] [esp32] Remove specific claims from framework migration message (#13777) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- 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 f5a8bc8994..0c02f6fbee 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1169,8 +1169,8 @@ def _show_framework_migration_message(name: str, variant: str) -> None: + "(We've been warning about this change since ESPHome 2025.8.0)\n" + "\n" + "Why we made this change:\n" - + color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n") - + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") + + color(AnsiFore.GREEN, " ✨ Smaller firmware binaries\n") + + color(AnsiFore.GREEN, " ⚡ Faster compile times\n") + color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n") + color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n") + "\n" From bbdb202e2c53358dbd20076a748bc743500371e6 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:26:47 +0100 Subject: [PATCH 52/95] [epaper_spi] Refactor initialise for future use (#13774) --- esphome/components/epaper_spi/epaper_spi.cpp | 13 +++++++++---- esphome/components/epaper_spi/epaper_spi.h | 3 ++- esphome/components/epaper_spi/epaper_waveshare.cpp | 3 ++- esphome/components/epaper_spi/epaper_waveshare.h | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index db803305a5..ae1923a916 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -182,7 +182,9 @@ void EPaperBase::process_state_() { this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: - this->initialise(this->update_count_ != 0); + if (!this->initialise(this->update_count_ != 0)) { + return; // Not done yet, come back next loop + } this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: @@ -239,11 +241,9 @@ void EPaperBase::start_data_() { void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } -void EPaperBase::initialise(bool partial) { +void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) { size_t index = 0; - auto *sequence = this->init_sequence_; - auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { this->mark_failed(LOG_STR("Malformed init sequence")); @@ -266,6 +266,11 @@ void EPaperBase::initialise(bool partial) { } } +bool EPaperBase::initialise(bool partial) { + this->send_init_sequence_(this->init_sequence_, this->init_sequence_length_); + return true; +} + /** * Check and rotate coordinates based on the transform flags. * @param x diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 521543f026..a8c2fe9b56 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -115,7 +115,8 @@ class EPaperBase : public Display, bool is_idle_() const; void setup_pins_() const; virtual bool reset(); - virtual void initialise(bool partial); + virtual bool initialise(bool partial); + void send_init_sequence_(const uint8_t *sequence, size_t length); void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); bool rotate_coordinates_(int &x, int &y); diff --git a/esphome/components/epaper_spi/epaper_waveshare.cpp b/esphome/components/epaper_spi/epaper_waveshare.cpp index 8d382d86e7..7a7b4e22d3 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.cpp +++ b/esphome/components/epaper_spi/epaper_waveshare.cpp @@ -4,7 +4,7 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi.waveshare"; -void EpaperWaveshare::initialise(bool partial) { +bool EpaperWaveshare::initialise(bool partial) { EPaperBase::initialise(partial); if (partial) { this->cmd_data(0x32, this->partial_lut_, this->partial_lut_length_); @@ -17,6 +17,7 @@ void EpaperWaveshare::initialise(bool partial) { this->cmd_data(0x3C, {0x05}); } this->send_red_ = true; + return true; } void EpaperWaveshare::set_window() { diff --git a/esphome/components/epaper_spi/epaper_waveshare.h b/esphome/components/epaper_spi/epaper_waveshare.h index 6b894cfd09..15fb575ca0 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.h +++ b/esphome/components/epaper_spi/epaper_waveshare.h @@ -18,7 +18,7 @@ class EpaperWaveshare : public EPaperMono { partial_lut_length_(partial_lut_length) {} protected: - void initialise(bool partial) override; + bool initialise(bool partial) override; void set_window() override; void refresh_screen(bool partial) override; void deep_sleep() override; From f4e410f47f92199def4494525d121528b4504bc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 5 Feb 2026 14:56:43 +0100 Subject: [PATCH 53/95] [ci] Block new scanf() usage to prevent ~9.8KB flash bloat (#13657) --- script/ci-custom.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index b146c9867e..b5bec74fa7 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -756,6 +756,28 @@ def lint_no_sprintf(fname, match): ) +@lint_re_check( + # Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf + # Also match std:: prefixed versions + # [^\w] ensures we match function calls, not substrings + r"[^\w]((?:std::)?v?[fs]?scanf)\s*\(" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_scanf(fname, match): + func = match.group(1) + return ( + f"{highlight(func + '()')} is not allowed in new ESPHome code. The scanf family " + f"pulls in ~7KB flash on ESP8266 and ~9KB on ESP32, and ESPHome doesn't otherwise " + f"need this code.\n" + f"Please use alternatives:\n" + f" - {highlight('parse_number<T>(str)')} for parsing integers/floats from strings\n" + f" - {highlight('strtol()/strtof()')} for C-style number parsing with error checking\n" + f" - {highlight('parse_hex()')} for hex string parsing\n" + f" - Manual parsing for simple fixed formats\n" + f"(If strictly necessary, add `// NOLINT` to the end of the line)" + ) + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 081f953dc330cdf22e09c3286b9d957477f68c95 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:00:16 -0500 Subject: [PATCH 54/95] [core] Add capacity check to register_component_ (#13778) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/core/application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0e77be9ee4..0daabc0282 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -81,6 +81,10 @@ void Application::register_component_(Component *comp) { return; } } + if (this->components_.size() >= ESPHOME_COMPONENT_COUNT) { + ESP_LOGE(TAG, "Cannot register component %s - at capacity!", LOG_STR_ARG(comp->get_component_log_str())); + return; + } this->components_.push_back(comp); } void Application::setup() { From 55ef8393afd40baf100424c0ef0a01efa25ffc08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 5 Feb 2026 15:19:03 +0100 Subject: [PATCH 55/95] [api] Remove is_single parameter and fix batch buffer preparation (#13773) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 354 +++++++++------------- esphome/components/api/api_connection.h | 176 ++++------- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 32 +- script/api_protobuf/api_protobuf.py | 2 +- 5 files changed, 221 insertions(+), 345 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 839de29de7..2aa5956f24 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -300,7 +300,7 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { // Encodes a message to the buffer and returns the total number of bytes used, // including header and footer overhead. Returns 0 if the message doesn't fit. uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { #ifdef HAS_PROTO_MESSAGE_DUMP // If in log-only mode, just log and return if (conn->flags_.log_only_mode) { @@ -330,12 +330,9 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess // Get buffer size after allocation (which includes header padding) std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); - if (is_single || conn->flags_.batch_first_message) { - // Single message or first batch message - conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size); - if (conn->flags_.batch_first_message) { - conn->flags_.batch_first_message = false; - } + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller, just clear flag + conn->flags_.batch_first_message = false; } else { // Batch message second or later // Add padding for previous message footer + this message header @@ -365,24 +362,22 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary BinarySensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } -uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); ListEntitiesBinarySensorResponse msg; msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } #endif @@ -390,8 +385,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool APIConnection::send_cover_state(cover::Cover *cover) { return this->send_message_smart_(cover, CoverStateResponse::MESSAGE_TYPE, CoverStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast<cover::Cover *>(entity); CoverStateResponse msg; auto traits = cover->get_traits(); @@ -399,10 +393,9 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); - return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast<cover::Cover *>(entity); ListEntitiesCoverResponse msg; auto traits = cover->get_traits(); @@ -411,8 +404,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); msg.device_class = cover->get_device_class_ref(); - return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::cover_command(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -430,8 +422,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { bool APIConnection::send_fan_state(fan::Fan *fan) { return this->send_message_smart_(fan, FanStateResponse::MESSAGE_TYPE, FanStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast<fan::Fan *>(entity); FanStateResponse msg; auto traits = fan->get_traits(); @@ -445,10 +436,9 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast<enums::FanDirection>(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) msg.preset_mode = fan->get_preset_mode(); - return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast<fan::Fan *>(entity); ListEntitiesFanResponse msg; auto traits = fan->get_traits(); @@ -457,7 +447,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); msg.supported_preset_modes = &traits.supported_preset_modes(); - return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::fan_command(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) @@ -481,8 +471,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { bool APIConnection::send_light_state(light::LightState *light) { return this->send_message_smart_(light, LightStateResponse::MESSAGE_TYPE, LightStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast<light::LightState *>(entity); LightStateResponse resp; auto values = light->remote_values; @@ -501,10 +490,9 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * if (light->supports_effects()) { resp.effect = light->get_effect_name(); } - return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast<light::LightState *>(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); @@ -527,8 +515,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c } } msg.effects = &effects_list; - return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::light_command(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -568,17 +555,15 @@ bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { return this->send_message_smart_(sensor, SensorStateResponse::MESSAGE_TYPE, SensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast<sensor::Sensor *>(entity); SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast<sensor::Sensor *>(entity); ListEntitiesSensorResponse msg; msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); @@ -586,8 +571,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); - return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -596,23 +580,19 @@ bool APIConnection::send_switch_state(switch_::Switch *a_switch) { return this->send_message_smart_(a_switch, SwitchStateResponse::MESSAGE_TYPE, SwitchStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast<switch_::Switch *>(entity); SwitchStateResponse resp; resp.state = a_switch->state; - return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast<switch_::Switch *>(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); msg.device_class = a_switch->get_device_class_ref(); - return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -631,22 +611,19 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) TextSensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); TextSensorStateResponse resp; resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); - return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); ListEntitiesTextSensorResponse msg; msg.device_class = text_sensor->get_device_class_ref(); return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } #endif @@ -654,8 +631,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect bool APIConnection::send_climate_state(climate::Climate *climate) { return this->send_message_smart_(climate, ClimateStateResponse::MESSAGE_TYPE, ClimateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast<climate::Climate *>(entity); ClimateStateResponse resp; auto traits = climate->get_traits(); @@ -687,11 +663,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; - return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast<climate::Climate *>(entity); ListEntitiesClimateResponse msg; auto traits = climate->get_traits(); @@ -716,8 +690,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_presets = &traits.get_supported_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_swing_modes = &traits.get_supported_swing_modes(); - return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -750,17 +723,15 @@ bool APIConnection::send_number_state(number::Number *number) { return this->send_message_smart_(number, NumberStateResponse::MESSAGE_TYPE, NumberStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast<number::Number *>(entity); NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast<number::Number *>(entity); ListEntitiesNumberResponse msg; msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); @@ -769,8 +740,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::number_command(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -783,22 +753,19 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { bool APIConnection::send_date_state(datetime::DateEntity *date) { return this->send_message_smart_(date, DateStateResponse::MESSAGE_TYPE, DateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast<datetime::DateEntity *>(entity); DateStateResponse resp; resp.missing_state = !date->has_state(); resp.year = date->year; resp.month = date->month; resp.day = date->day; - return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast<datetime::DateEntity *>(entity); ListEntitiesDateResponse msg; - return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::date_command(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -811,22 +778,19 @@ void APIConnection::date_command(const DateCommandRequest &msg) { bool APIConnection::send_time_state(datetime::TimeEntity *time) { return this->send_message_smart_(time, TimeStateResponse::MESSAGE_TYPE, TimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast<datetime::TimeEntity *>(entity); TimeStateResponse resp; resp.missing_state = !time->has_state(); resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast<datetime::TimeEntity *>(entity); ListEntitiesTimeResponse msg; - return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::time_command(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -840,8 +804,7 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { return this->send_message_smart_(datetime, DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); DateTimeStateResponse resp; resp.missing_state = !datetime->has_state(); @@ -849,15 +812,12 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); ListEntitiesDateTimeResponse msg; - return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) @@ -871,25 +831,22 @@ bool APIConnection::send_text_state(text::Text *text) { return this->send_message_smart_(text, TextStateResponse::MESSAGE_TYPE, TextStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast<text::Text *>(entity); TextStateResponse resp; resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); - return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast<text::Text *>(entity); ListEntitiesTextResponse msg; msg.mode = static_cast<enums::TextMode>(text->traits.get_mode()); msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern_ref(); - return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::text_command(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) @@ -903,22 +860,19 @@ bool APIConnection::send_select_state(select::Select *select) { return this->send_message_smart_(select, SelectStateResponse::MESSAGE_TYPE, SelectStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast<select::Select *>(entity); SelectStateResponse resp; resp.state = select->current_option(); resp.missing_state = !select->has_state(); - return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast<select::Select *>(entity); ListEntitiesSelectResponse msg; msg.options = &select->traits.get_options(); - return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::select_command(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) @@ -928,13 +882,11 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast<button::Button *>(entity); ListEntitiesButtonResponse msg; msg.device_class = button->get_device_class_ref(); - return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -947,23 +899,20 @@ bool APIConnection::send_lock_state(lock::Lock *a_lock) { return this->send_message_smart_(a_lock, LockStateResponse::MESSAGE_TYPE, LockStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast<lock::Lock *>(entity); LockStateResponse resp; resp.state = static_cast<enums::LockState>(a_lock->state); - return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast<lock::Lock *>(entity); ListEntitiesLockResponse msg; msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::lock_command(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -986,16 +935,14 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { bool APIConnection::send_valve_state(valve::Valve *valve) { return this->send_message_smart_(valve, ValveStateResponse::MESSAGE_TYPE, ValveStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast<valve::Valve *>(entity); ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); - return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast<valve::Valve *>(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); @@ -1003,8 +950,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::valve_command(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1021,8 +967,7 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla return this->send_message_smart_(media_player, MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast<media_player::MediaPlayer *>(entity); MediaPlayerStateResponse resp; media_player::MediaPlayerState report_state = media_player->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING @@ -1031,11 +976,9 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast<enums::MediaPlayerState>(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast<media_player::MediaPlayer *>(entity); ListEntitiesMediaPlayerResponse msg; auto traits = media_player->get_traits(); @@ -1051,7 +994,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.sample_bytes = supported_format.sample_bytes; } return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1092,7 +1035,7 @@ void APIConnection::try_send_camera_image_() { msg.device_id = camera::Camera::instance()->get_device_id(); #endif - if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { + if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) { return; // Send failed, try again later } this->image_reader_->consume_data(to_send); @@ -1115,12 +1058,10 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) this->try_send_camera_image_(); } } -uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *camera = static_cast<camera::Camera *>(entity); ListEntitiesCameraResponse msg; - return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::camera_image(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) @@ -1305,22 +1246,22 @@ bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmCon AlarmControlPanelStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); ListEntitiesAlarmControlPanelResponse msg; msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, - conn, remaining_size, is_single); + conn, remaining_size); } void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) @@ -1357,8 +1298,7 @@ bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_hea return this->send_message_smart_(water_heater, WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast<water_heater::WaterHeater *>(entity); WaterHeaterStateResponse resp; resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode()); @@ -1369,10 +1309,9 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.state = wh->get_state(); resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast<water_heater::WaterHeater *>(entity); ListEntitiesWaterHeaterResponse msg; auto traits = wh->get_traits(); @@ -1381,8 +1320,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); - return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) { @@ -1411,20 +1349,18 @@ void APIConnection::send_event(event::Event *event) { event->get_last_event_type_index()); } uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { EventResponse resp; resp.event_type = event_type; - return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast<event::Event *>(entity); ListEntitiesEventResponse msg; msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1447,13 +1383,11 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent #endif #ifdef USE_INFRARED -uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *infrared = static_cast<infrared::Infrared *>(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); - return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1461,8 +1395,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection bool APIConnection::send_update_state(update::UpdateEntity *update) { return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); UpdateStateResponse resp; resp.missing_state = !update->has_state(); @@ -1478,15 +1411,13 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = StringRef(update->update_info.summary); resp.release_url = StringRef(update->update_info.release_url); } - return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); ListEntitiesUpdateResponse msg; msg.device_class = update->get_device_class_ref(); - return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::update_command(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1512,7 +1443,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char SubscribeLogsResponse msg; msg.level = static_cast<enums::LogLevel>(level); msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len); - return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE); + return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE); } void APIConnection::complete_authentication_() { @@ -1837,6 +1768,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } +bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { + ProtoSize size; + msg.calculate_size(size); + std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, size.get_size()); + msg.encode({&shared_buf}); + return this->send_buffer({&shared_buf}, message_type); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -1897,6 +1836,23 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me } } +bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, + uint8_t aux_data_index) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + auto &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, estimated_size); + DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; + if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && + this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_batch_item_(item); +#endif + return true; + } + } + return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); +} + bool APIConnection::schedule_batch_() { if (!this->flags_.batch_scheduled) { this->flags_.batch_scheduled = true; @@ -1925,10 +1881,21 @@ void APIConnection::process_batch_() { auto &shared_buf = this->parent_->get_shared_buffer_ref(); size_t num_items = this->deferred_batch_.size(); - // Fast path for single message - allocate exact size needed + // Cache these values to avoid repeated virtual calls + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + + // Pre-calculate exact buffer size needed based on message types + uint32_t total_estimated_size = num_items * (header_padding + footer_size); + for (size_t i = 0; i < num_items; i++) { + total_estimated_size += this->deferred_batch_[i].estimated_size; + } + + this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); + + // Fast path for single message - buffer already allocated above if (num_items == 1) { const auto &item = this->deferred_batch_[0]; - // Let dispatch_message_ calculate size and encode if it fits uint16_t payload_size = this->dispatch_message_(item, std::numeric_limits<uint16_t>::max(), true); @@ -1951,30 +1918,8 @@ void APIConnection::process_batch_() { // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage); - size_t message_count = 0; - - // Cache these values to avoid repeated virtual calls - const uint8_t header_padding = this->helper_->frame_header_padding(); - const uint8_t footer_size = this->helper_->frame_footer_size(); - - // Initialize buffer and tracking variables - shared_buf.clear(); - - // Pre-calculate exact buffer size needed based on message types - uint32_t total_estimated_size = num_items * (header_padding + footer_size); - for (size_t i = 0; i < this->deferred_batch_.size(); i++) { - const auto &item = this->deferred_batch_[i]; - total_estimated_size += item.estimated_size; - } - - // Calculate total overhead for all messages - // Reserve based on estimated size (much more accurate than 24-byte worst-case) - shared_buf.reserve(total_estimated_size); - this->flags_.batch_first_message = true; - size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits<uint16_t>::max(); - // Track where each message's header padding begins in the buffer // For plaintext: this is where the 6-byte header padding starts // For noise: this is where the 7-byte header padding starts @@ -1986,7 +1931,7 @@ void APIConnection::process_batch_() { const auto &item = this->deferred_batch_[i]; // Try to encode message via dispatch // The dispatch function calculates overhead to determine if the message fits - uint16_t payload_size = this->dispatch_message_(item, remaining_size, false); + uint16_t payload_size = this->dispatch_message_(item, remaining_size, i == 0); if (payload_size == 0) { // Message won't fit, stop processing @@ -2000,10 +1945,7 @@ void APIConnection::process_batch_() { // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, // as ensured by the static_assert in its definition. - new (&message_info[message_count++]) MessageInfo(item.message_type, current_offset, proto_payload_size); - - // Update tracking variables - items_processed++; + new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size); // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation if (items_processed == 1) { remaining_size = MAX_BATCH_PACKET_SIZE; @@ -2026,7 +1968,7 @@ void APIConnection::process_batch_() { // Send all collected messages APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, - std::span<const MessageInfo>(message_info, message_count)); + std::span<const MessageInfo>(message_info, items_processed)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); } @@ -2055,7 +1997,8 @@ void APIConnection::process_batch_() { // Dispatch message encoding based on message_type // Switch assigns function pointer, single call site for smaller code size uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, - bool is_single) { + bool batch_first) { + this->flags_.batch_first_message = batch_first; #ifdef USE_EVENT // Events need aux_data_index to look up event type from entity if (item.message_type == EventResponse::MESSAGE_TYPE) { @@ -2064,7 +2007,7 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, return 0; auto *event = static_cast<event::Event *>(item.entity); return try_send_event_response(event, StringRef::from_maybe_nullptr(event->get_event_type(item.aux_data_index)), - this, remaining_size, is_single); + this, remaining_size); } #endif @@ -2174,25 +2117,22 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, #undef CASE_STATE_INFO #undef CASE_INFO_ONLY - return func(item.entity, this, remaining_size, is_single); + return func(item.entity, this, remaining_size); } -uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { ListEntitiesDoneResponse resp; - return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { DisconnectRequest req; - return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { PingRequest req; - return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size); } #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b839a2a97b..40e4fd61c1 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -255,17 +255,7 @@ class APIConnection final : public APIServerConnection { void on_fatal_error() override; void on_no_setup_connection() override; - ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { - // FIXME: ensure no recursive writes can happen - - // Get header padding size - used for both reserve and insert - uint8_t header_padding = this->helper_->frame_header_padding(); - // Get shared buffer from parent server - std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, header_padding, - reserve_size + header_padding + this->helper_->frame_footer_size()); - return {&shared_buf}; - } + bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override; void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); @@ -277,6 +267,13 @@ class APIConnection final : public APIServerConnection { shared_buf.resize(header_padding); } + // Convenience overload - computes frame overhead internally + void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) { + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); + } + bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; @@ -298,21 +295,21 @@ class APIConnection final : public APIServerConnection { // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); + uint32_t remaining_size); // Helper to fill entity state base and encode message static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { + APIConnection *conn, uint32_t remaining_size) { msg.key = entity->get_object_id_hash(); #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + return encode_message_to_buffer(msg, message_type, conn, remaining_size); } // Helper to fill entity info base and encode message static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { + APIConnection *conn, uint32_t remaining_size) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); @@ -339,7 +336,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + return encode_message_to_buffer(msg, message_type, conn, remaining_size); } #ifdef USE_VOICE_ASSISTANT @@ -370,141 +367,108 @@ class APIConnection final : public APIServerConnection { } #ifdef USE_BINARY_SENSOR - static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_COVER - static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_FAN - static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LIGHT - static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SENSOR - static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SWITCH - static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT_SENSOR - static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CLIMATE - static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_NUMBER - static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATE - static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_TIME - static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATETIME - static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT - static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SELECT - static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_BUTTON - static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LOCK - static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_VALVE - static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_MEDIA_PLAYER - static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_ALARM_CONTROL_PANEL - static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_WATER_HEATER - static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_INFRARED - static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_EVENT static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); - static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + uint32_t remaining_size); + static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_UPDATE - static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CAMERA - static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif // Method for ListEntitiesDone batching - static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Method for DisconnectRequest batching - static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Batch message method for ping requests - static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // === Optimal member ordering for 32-bit systems === @@ -539,7 +503,7 @@ class APIConnection final : public APIServerConnection { #endif // Function pointer type for message encoding - using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); + using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size); // Generic batching mechanism for both state updates and entity info struct DeferredBatch { @@ -652,7 +616,7 @@ class APIConnection final : public APIServerConnection { // Dispatch message encoding based on message_type - replaces function pointer storage // Switch assigns pointer, single call site for smaller code size - uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool is_single); + uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool batch_first); #ifdef HAS_PROTO_MESSAGE_DUMP void log_batch_item_(const DeferredBatch::BatchItem &item) { @@ -684,19 +648,7 @@ class APIConnection final : public APIServerConnection { // Tries immediate send if should_send_immediately_() returns true and buffer has space // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) { - if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { - DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; - if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && - this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { -#ifdef HAS_PROTO_MESSAGE_DUMP - this->log_batch_item_(item); -#endif - return true; - } - } - return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); - } + uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED); // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 80a61c1041..e2bc1609ed 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -23,7 +23,7 @@ class APIServerConnectionBase : public ProtoService { DumpBuffer dump_buf; this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); #endif - return this->send_message_(msg, message_type); + return this->send_message_impl(msg, message_type); } virtual void on_hello_request(const HelloRequest &value){}; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 552b4a4625..92978f765f 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -957,32 +957,16 @@ class ProtoService { virtual bool is_connection_setup() = 0; virtual void on_fatal_error() = 0; virtual void on_no_setup_connection() = 0; - /** - * Create a buffer with a reserved size. - * @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint - * to optimize memory usage and avoid reallocations during encoding. - * Implementations should aim to allocate at least this size. - * @return A ProtoWriteBuffer object with the reserved size. - */ - virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - - // Optimized method that pre-allocates buffer based on message size - bool send_message_(const ProtoMessage &msg, uint8_t message_type) { - ProtoSize size; - msg.calculate_size(size); - uint32_t msg_size = size.get_size(); - - // Create a pre-sized buffer - auto buffer = this->create_buffer(msg_size); - - // Encode message into the buffer - msg.encode(buffer); - - // Send the buffer - return this->send_buffer(buffer, message_type); - } + /** + * Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending. + * This is the implementation method - callers should use send_message() which adds logging. + * @param msg The protobuf message to send. + * @param message_type The message type identifier. + * @return True if the message was sent successfully, false otherwise. + */ + virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0; // Authentication helper methods inline bool check_connection_setup_() { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 8baf6acf11..4021a062ca 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2848,7 +2848,7 @@ static const char *const TAG = "api.service"; hpp += " DumpBuffer dump_buf;\n" hpp += " this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));\n" hpp += "#endif\n" - hpp += " return this->send_message_(msg, message_type);\n" + hpp += " return this->send_message_impl(msg, message_type);\n" hpp += " }\n\n" # Add logging helper method implementations to cpp From ed4f00d4a3c80c54f6084305a640377f4f575105 Mon Sep 17 00:00:00 2001 From: Marek Beran <95686867+Bercek71@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:11:14 +0100 Subject: [PATCH 56/95] [vbus] Add DeltaSol BS/2 support with sensors and binary sensors (#13762) --- esphome/components/vbus/__init__.py | 1 + .../components/vbus/binary_sensor/__init__.py | 41 +++++++ .../vbus/binary_sensor/vbus_binary_sensor.cpp | 19 +++ .../vbus/binary_sensor/vbus_binary_sensor.h | 17 +++ esphome/components/vbus/sensor/__init__.py | 110 ++++++++++++++++++ .../components/vbus/sensor/vbus_sensor.cpp | 39 +++++++ esphome/components/vbus/sensor/vbus_sensor.h | 30 +++++ tests/components/vbus/common.yaml | 50 ++++++-- 8 files changed, 298 insertions(+), 9 deletions(-) diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index d916d7c064..5790a9cce0 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -16,6 +16,7 @@ CONF_VBUS_ID = "vbus_id" CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" CONF_DELTASOL_BS_2009 = "deltasol_bs_2009" +CONF_DELTASOL_BS2 = "deltasol_bs2" CONF_DELTASOL_C = "deltasol_c" CONF_DELTASOL_CS2 = "deltasol_cs2" CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index ae927656c0..70dda94300 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -27,6 +28,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) @@ -118,6 +120,28 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -275,6 +299,23 @@ async def to_code(config): ) cg.add(var.set_frost_protection_active_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index 4ccd149935..c1d7bc1b18 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -129,6 +129,25 @@ void DeltaSolCSPlusBSensor::handle_message(std::vector<uint8_t> &message) { this->s4_error_bsensor_->publish_state(message[20] & 8); } +void DeltaSolBS2BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolBS2BSensor::handle_message(std::vector<uint8_t> &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[10] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[10] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[10] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[10] & 8); +} + void VBusCustomBSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Binary Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index 146aa1b673..2decdde602 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -111,6 +111,23 @@ class DeltaSolCSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector<uint8_t> &message) override; }; +class DeltaSolBS2BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector<uint8_t> &message) override; +}; + class VBusCustomSubBSensor; class VBusCustomBSensor : public VBusListener, public Component { diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index fcff698ac0..ff8ef98a1a 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -43,6 +44,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2Sensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) @@ -227,6 +229,79 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -560,6 +635,41 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_VERSION]) cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index e81c0486d4..75c9ea1aee 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -214,6 +214,45 @@ void DeltaSolCSPlusSensor::handle_message(std::vector<uint8_t> &message) { this->flow_rate_sensor_->publish_state(get_u16(message, 38)); } +void DeltaSolBS2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBS2Sensor::handle_message(std::vector<uint8_t> &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 12)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) { + float heat_wh = get_u16(message, 16) + get_u16(message, 18) * 1000.0f + get_u16(message, 20) * 1000000.0f; + this->heat_quantity_sensor_->publish_state(heat_wh); + } + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 24) * 0.01f); +} + void VBusCustomSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index d5535b2019..cea2ee1c86 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -157,6 +157,36 @@ class DeltaSolCSPlusSensor : public VBusListener, public Component { void handle_message(std::vector<uint8_t> &message) override; }; +class DeltaSolBS2Sensor : public VBusListener, public Component { + public: + void dump_config() override; + + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector<uint8_t> &message) override; +}; + class VBusCustomSubSensor; class VBusCustomSensor : public VBusListener, public Component { diff --git a/tests/components/vbus/common.yaml b/tests/components/vbus/common.yaml index 33d9e2935d..5c771be922 100644 --- a/tests/components/vbus/common.yaml +++ b/tests/components/vbus/common.yaml @@ -4,11 +4,21 @@ binary_sensor: - platform: vbus model: deltasol_bs_plus relay1: - name: Relay 1 On + name: BS Plus Relay 1 On relay2: - name: Relay 2 On + name: BS Plus Relay 2 On sensor1_error: - name: Sensor 1 Error + name: BS Plus Sensor 1 Error + - platform: vbus + model: deltasol_bs2 + sensor1_error: + name: BS2 Sensor 1 Error + sensor2_error: + name: BS2 Sensor 2 Error + sensor3_error: + name: BS2 Sensor 3 Error + sensor4_error: + name: BS2 Sensor 4 Error - platform: vbus model: custom command: 0x100 @@ -23,14 +33,36 @@ sensor: - platform: vbus model: deltasol c temperature_1: - name: Temperature 1 + name: DeltaSol C Temperature 1 temperature_2: - name: Temperature 2 + name: DeltaSol C Temperature 2 temperature_3: - name: Temperature 3 + name: DeltaSol C Temperature 3 operating_hours_1: - name: Operating Hours 1 + name: DeltaSol C Operating Hours 1 heat_quantity: - name: Heat Quantity + name: DeltaSol C Heat Quantity time: - name: System Time + name: DeltaSol C System Time + - platform: vbus + model: deltasol_bs2 + temperature_1: + name: BS2 Temperature 1 + temperature_2: + name: BS2 Temperature 2 + temperature_3: + name: BS2 Temperature 3 + temperature_4: + name: BS2 Temperature 4 + pump_speed_1: + name: BS2 Pump Speed 1 + pump_speed_2: + name: BS2 Pump Speed 2 + operating_hours_1: + name: BS2 Operating Hours 1 + operating_hours_2: + name: BS2 Operating Hours 2 + heat_quantity: + name: BS2 Heat Quantity + version: + name: BS2 Firmware Version From c7729cb019021d332fa414e7e6f1a183127b6f0a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 03:51:13 -0500 Subject: [PATCH 57/95] [esp32] Use underscores in arduino_libs_stub folder name (#13785) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c02f6fbee..90f6035aba 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1467,7 +1467,7 @@ async def to_code(config): [_format_framework_espidf_version(idf_ver, None)], ) # Use stub package to skip downloading precompiled libs - stubs_dir = CORE.relative_build_path("arduino-libs-stub") + stubs_dir = CORE.relative_build_path("arduino_libs_stub") cg.add_platformio_option( "platform_packages", [f"{ARDUINO_LIBS_PKG}@file://{stubs_dir}"] ) From 6decdfad26772115bfddc4525ae9624c1ac75101 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:05:10 +0100 Subject: [PATCH 58/95] Bump github/codeql-action from 4.32.1 to 4.32.2 (#13781) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 817ea1d2be..51ea4085e0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 + uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 + uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: category: "/language:${{matrix.language}}" From 8e461db301dcb00b04147c57d6a62f8639109a18 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:09:48 -0500 Subject: [PATCH 59/95] [ota] Fix CLI upload option shown when only http_request platform configured (#13784) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/__main__.py | 9 +++- tests/unit_tests/test_main.py | 85 ++++++++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 55297e8d9b..c86b5604e1 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -294,8 +294,13 @@ def has_api() -> bool: def has_ota() -> bool: - """Check if OTA is available.""" - return CONF_OTA in CORE.config + """Check if OTA upload is available (requires platform: esphome).""" + if CONF_OTA not in CORE.config: + return False + return any( + ota_item.get(CONF_PLATFORM) == CONF_ESPHOME + for ota_item in CORE.config[CONF_OTA] + ) def has_mqtt_ip_lookup() -> bool: diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3268f7ee87..c9aa446323 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -32,6 +32,7 @@ from esphome.__main__ import ( has_mqtt_ip_lookup, has_mqtt_logging, has_non_ip_address, + has_ota, has_resolvable_address, mqtt_get_ip, run_esphome, @@ -332,7 +333,9 @@ def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None: def test_choose_upload_log_host_with_ota_list() -> None: """Test with OTA as the only item in the list.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=["OTA"], @@ -345,7 +348,7 @@ def test_choose_upload_log_host_with_ota_list() -> None: @pytest.mark.usefixtures("mock_has_mqtt_logging") def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None: """Test with OTA list falling back to MQTT when no address.""" - setup_core(config={CONF_OTA: {}, "mqtt": {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], "mqtt": {}}) result = choose_upload_log_host( default=["OTA"], @@ -408,7 +411,9 @@ def test_choose_upload_log_host_with_serial_device_with_ports( def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None: """Test OTA device when OTA is configured.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default="OTA", @@ -475,7 +480,9 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None: @pytest.mark.usefixtures("mock_choose_prompt") def test_choose_upload_log_host_multiple_devices() -> None: """Test with multiple devices including special identifiers.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")] @@ -514,7 +521,9 @@ def test_choose_upload_log_host_no_defaults_with_serial_ports( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_no_defaults_with_ota() -> None: """Test interactive mode with OTA option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) with patch( "esphome.__main__.choose_prompt", return_value="192.168.1.100" @@ -575,7 +584,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -604,7 +617,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -632,7 +649,9 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_check_default_matches() -> None: """Test when check_default matches an available option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=None, @@ -704,7 +723,10 @@ def test_choose_upload_log_host_mixed_resolved_unresolved() -> None: def test_choose_upload_log_host_ota_both_conditions() -> None: """Test OTA device when both OTA and API are configured and enabled.""" - setup_core(config={CONF_OTA: {}, CONF_API: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}}, + address="192.168.1.100", + ) result = choose_upload_log_host( default="OTA", @@ -719,7 +741,7 @@ def test_choose_upload_log_host_ota_ip_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -744,7 +766,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -769,7 +791,7 @@ def test_choose_upload_log_host_ota_ip_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -794,7 +816,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -817,7 +839,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: @pytest.mark.usefixtures("mock_no_mqtt_logging") def test_choose_upload_log_host_no_address_with_ota_config() -> None: """Test OTA device when OTA is configured but no address is set.""" - setup_core(config={CONF_OTA: {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) with pytest.raises( EsphomeError, match="All specified devices .* could not be resolved" @@ -1532,10 +1554,43 @@ def test_has_mqtt() -> None: assert has_mqtt() is False # Test with other components but no MQTT - setup_core(config={CONF_API: {}, CONF_OTA: {}}) + setup_core(config={CONF_API: {}, CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) assert has_mqtt() is False +def test_has_ota() -> None: + """Test has_ota function. + + The has_ota function should only return True when OTA is configured + with platform: esphome, not when only platform: http_request is configured. + This is because CLI OTA upload only works with the esphome platform. + """ + # Test with OTA esphome platform configured + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) + assert has_ota() is True + + # Test with OTA http_request platform only (should return False) + # This is the bug scenario from issue #13783 + setup_core(config={CONF_OTA: [{CONF_PLATFORM: "http_request"}]}) + assert has_ota() is False + + # Test without OTA configured + setup_core(config={}) + assert has_ota() is False + + # Test with multiple OTA platforms including esphome + setup_core( + config={ + CONF_OTA: [{CONF_PLATFORM: "http_request"}, {CONF_PLATFORM: CONF_ESPHOME}] + } + ) + assert has_ota() is True + + # Test with empty OTA list + setup_core(config={CONF_OTA: []}) + assert has_ota() is False + + def test_get_port_type() -> None: """Test get_port_type function.""" From fef5d3f88f3f0fff512e39ac3e956bf84c2e958f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:10:22 -0500 Subject: [PATCH 60/95] [rdm6300] Add ID-20LA compatibility by skipping CR/LF bytes (#13779) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/components/rdm6300/rdm6300.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/rdm6300/rdm6300.cpp b/esphome/components/rdm6300/rdm6300.cpp index f9b6126c4b..04065982f6 100644 --- a/esphome/components/rdm6300/rdm6300.cpp +++ b/esphome/components/rdm6300/rdm6300.cpp @@ -34,6 +34,8 @@ void rdm6300::RDM6300Component::loop() { this->buffer_[this->read_state_ / 2] += value; } this->read_state_++; + } else if (data == 0x0D || data == 0x0A) { + // Skip CR/LF bytes (ID-20LA compatibility) } else if (data != RDM6300_END_BYTE) { ESP_LOGW(TAG, "Invalid end byte from RDM6300!"); this->read_state_ = RDM6300_STATE_WAITING_FOR_START; From 112a2c5d9276cdf61743ccfd18c404e9c9658092 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:11:08 +1100 Subject: [PATCH 61/95] [const] Move some constants to common (#13788) --- esphome/components/const/__init__.py | 6 ++++++ esphome/components/daly_bms/sensor.py | 4 +--- esphome/components/ina2xx_base/__init__.py | 4 ++-- esphome/components/pipsolar/sensor/__init__.py | 4 +--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index fcfafa0c1a..3201db5dfd 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -17,3 +17,9 @@ CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" + +ICON_CURRENT_DC = "mdi:current-dc" +ICON_SOLAR_PANEL = "mdi:solar-panel" +ICON_SOLAR_POWER = "mdi:solar-power" + +UNIT_AMPERE_HOUR = "Ah" diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 560bcef64e..aa92cfa86a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import ICON_CURRENT_DC, UNIT_AMPERE_HOUR import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_LEVEL, @@ -55,14 +56,11 @@ CONF_CELL_15_VOLTAGE = "cell_15_voltage" CONF_CELL_16_VOLTAGE = "cell_16_voltage" CONF_CELL_17_VOLTAGE = "cell_17_voltage" CONF_CELL_18_VOLTAGE = "cell_18_voltage" -ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" ICON_THERMOMETER_CHEVRON_DOWN = "mdi:thermometer-chevron-down" ICON_CAR_BATTERY = "mdi:car-battery" -UNIT_AMPERE_HOUR = "Ah" - TYPES = [ CONF_VOLTAGE, CONF_CURRENT, diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index ce68ad2726..15e2faba07 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import UNIT_AMPERE_HOUR import esphome.config_validation as cv from esphome.const import ( CONF_BUS_VOLTAGE, @@ -36,7 +37,6 @@ CONF_CHARGE_COULOMBS = "charge_coulombs" CONF_ENERGY_JOULES = "energy_joules" CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" CONF_RESET_ON_BOOT = "reset_on_boot" -UNIT_AMPERE_HOURS = "Ah" UNIT_COULOMB = "C" UNIT_JOULE = "J" UNIT_MILLIVOLT = "mV" @@ -180,7 +180,7 @@ INA2XX_SCHEMA = cv.Schema( ), cv.Optional(CONF_CHARGE): cv.maybe_simple_value( sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE_HOURS, + unit_of_measurement=UNIT_AMPERE_HOUR, accuracy_decimals=8, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index d08a877b55..8d3ba10d62 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import ICON_CURRENT_DC, ICON_SOLAR_PANEL, ICON_SOLAR_POWER import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_VOLTAGE, @@ -29,9 +30,6 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA DEPENDENCIES = ["uart"] -ICON_SOLAR_POWER = "mdi:solar-power" -ICON_SOLAR_PANEL = "mdi:solar-panel" -ICON_CURRENT_DC = "mdi:current-dc" # QPIRI sensors CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" From 7afd0eb1aa947cc5b081849eb6bc090763068127 Mon Sep 17 00:00:00 2001 From: Andrew Rankin <andrew@eiknet.com> Date: Fri, 6 Feb 2026 06:36:55 -0500 Subject: [PATCH 62/95] [esp32_ble] include sdkconfig.h before ESP-Hosted preprocessor guards (#13787) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- .../components/esp32_ble/ble_advertising.h | 9 ----- .../components/esp32_ble_beacon/__init__.py | 11 +++-- .../esp32_ble_beacon/esp32_ble_beacon.cpp | 10 ++++- .../esp32_ble_beacon/esp32_ble_beacon.h | 4 ++ esphome/config_validation.py | 11 +++++ .../config_validation/test_config.py | 40 ++++++++++++++++++- .../esp32_ble/test.esp32-p4-idf.yaml | 8 ++++ .../esp32_ble_beacon/test.esp32-p4-idf.yaml | 7 ++++ .../esp32_ble_client/test.esp32-p4-idf.yaml | 6 +++ .../esp32_ble_server/test.esp32-p4-idf.yaml | 4 ++ .../esp32_ble_tracker/test.esp32-p4-idf.yaml | 7 ++++ .../common/ble/esp32-p4-idf.yaml | 21 ++++++++++ 12 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 tests/components/esp32_ble/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_client/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_server/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml create mode 100644 tests/test_build_components/common/ble/esp32-p4-idf.yaml diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index d7f1eeac9d..3cfa6f548a 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -10,20 +10,11 @@ #ifdef USE_ESP32 #ifdef USE_ESP32_BLE_ADVERTISING -#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#include <esp_bt.h> -#endif #include <esp_gap_ble_api.h> #include <esp_gatts_api.h> namespace esphome::esp32_ble { -using raw_adv_data_t = struct { - uint8_t *data; - size_t length; - esp_power_level_t power_level; -}; - class ESPBTUUID; class BLEAdvertising { diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index ba5ae4331c..04c783980d 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -53,8 +53,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range( min=-128, max=0 ), - cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( - cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True) + cv.OnlyWithout(CONF_TX_POWER, "esp32_hosted", default="3dBm"): cv.All( + cv.conflicts_with_component("esp32_hosted"), + cv.decibel, + cv.enum(esp32_ble.TX_POWER_LEVELS, int=True), ), } ).extend(cv.COMPONENT_SCHEMA), @@ -82,7 +84,10 @@ async def to_code(config): cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL])) cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL])) cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) - cg.add(var.set_tx_power(config[CONF_TX_POWER])) + + # TX power control only available on native Bluetooth (not ESP-Hosted) + if CONF_TX_POWER in config: + cg.add(var.set_tx_power(config[CONF_TX_POWER])) cg.add_define("USE_ESP32_BLE_ADVERTISING") diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index f2aa7e762e..093273b399 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -36,11 +36,16 @@ void ESP32BLEBeacon::dump_config() { } } *bpos = '\0'; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" ", TX Power: %ddBm", uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, (this->tx_power_ * 3) - 12); +#else + ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d", + uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_); +#endif } float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } @@ -74,11 +79,14 @@ void ESP32BLEBeacon::on_advertise_() { ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_); ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(this->measured_power_); + esp_err_t err; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGD(TAG, "Setting BLE TX power"); - esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); } +#endif err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err)); diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 05afdc7379..7a0424f3aa 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -48,7 +48,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented void set_min_interval(uint16_t val) { this->min_interval_ = val; } void set_max_interval(uint16_t val) { this->max_interval_ = val; } void set_measured_power(int8_t val) { this->measured_power_ = val; } +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } +#endif void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; protected: @@ -60,7 +62,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented uint16_t min_interval_{}; uint16_t max_interval_{}; int8_t measured_power_{}; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID esp_power_level_t tx_power_{}; +#endif esp_ble_adv_params_t ble_adv_params_; bool advertising_{false}; }; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 55e13a7050..a9d1a72e5a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1403,6 +1403,17 @@ def requires_component(comp): return validator +def conflicts_with_component(comp): + """Validate that this option cannot be specified when the component `comp` is loaded.""" + + def validator(value): + if comp in CORE.loaded_integrations: + raise Invalid(f"This option is not compatible with component {comp}") + return value + + return validator + + uint8_t = int_range(min=0, max=255) uint16_t = int_range(min=0, max=65535) uint32_t = int_range(min=0, max=4294967295) diff --git a/tests/component_tests/config_validation/test_config.py b/tests/component_tests/config_validation/test_config.py index 1a9b9bc1f3..25d85d333b 100644 --- a/tests/component_tests/config_validation/test_config.py +++ b/tests/component_tests/config_validation/test_config.py @@ -1,10 +1,14 @@ """ -Test schema.extend functionality in esphome.config_validation. +Test config_validation functionality in esphome.config_validation. """ from typing import Any +import pytest +from voluptuous import Invalid + import esphome.config_validation as cv +from esphome.core import CORE def test_config_extend() -> None: @@ -49,3 +53,37 @@ def test_config_extend() -> None: assert validated["key2"] == "initial_value2" assert validated["extra_1"] == "value1" assert validated["extra_2"] == "value2" + + +def test_requires_component_passes_when_loaded() -> None: + """Test requires_component passes when the required component is loaded.""" + CORE.loaded_integrations.update({"wifi", "logger"}) + validator = cv.requires_component("wifi") + result = validator("test_value") + assert result == "test_value" + + +def test_requires_component_fails_when_not_loaded() -> None: + """Test requires_component raises Invalid when the required component is not loaded.""" + CORE.loaded_integrations.add("logger") + validator = cv.requires_component("wifi") + with pytest.raises(Invalid) as exc_info: + validator("test_value") + assert "requires component wifi" in str(exc_info.value) + + +def test_conflicts_with_component_passes_when_not_loaded() -> None: + """Test conflicts_with_component passes when the conflicting component is not loaded.""" + CORE.loaded_integrations.update({"wifi", "logger"}) + validator = cv.conflicts_with_component("esp32_hosted") + result = validator("test_value") + assert result == "test_value" + + +def test_conflicts_with_component_fails_when_loaded() -> None: + """Test conflicts_with_component raises Invalid when the conflicting component is loaded.""" + CORE.loaded_integrations.update({"wifi", "esp32_hosted"}) + validator = cv.conflicts_with_component("esp32_hosted") + with pytest.raises(Invalid) as exc_info: + validator("test_value") + assert "not compatible with component esp32_hosted" in str(exc_info.value) diff --git a/tests/components/esp32_ble/test.esp32-p4-idf.yaml b/tests/components/esp32_ble/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..4eeb7c2f18 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-p4-idf.yaml @@ -0,0 +1,8 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false diff --git a/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..b6e1845c50 --- /dev/null +++ b/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml @@ -0,0 +1,7 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +# tx_power is not supported on ESP-Hosted platforms +esp32_ble_beacon: + type: iBeacon + uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' diff --git a/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..e2496dd1ce --- /dev/null +++ b/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml @@ -0,0 +1,6 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: blec diff --git a/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..f202161cf3 --- /dev/null +++ b/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..d0f1e94a97 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml @@ -0,0 +1,7 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9 diff --git a/tests/test_build_components/common/ble/esp32-p4-idf.yaml b/tests/test_build_components/common/ble/esp32-p4-idf.yaml new file mode 100644 index 0000000000..dce923078a --- /dev/null +++ b/tests/test_build_components/common/ble/esp32-p4-idf.yaml @@ -0,0 +1,21 @@ +# Common BLE tracker configuration for ESP32-P4 IDF tests +# ESP32-P4 requires ESP-Hosted for Bluetooth via external coprocessor +# BLE client components share this tracker infrastructure +# Each component defines its own ble_client with unique MAC address + +esp32_hosted: + active_high: true + variant: ESP32C6 + reset_pin: GPIO54 + cmd_pin: GPIO19 + clk_pin: GPIO18 + d0_pin: GPIO14 + d1_pin: GPIO15 + d2_pin: GPIO16 + d3_pin: GPIO17 + +esp32_ble_tracker: + scan_parameters: + interval: 1100ms + window: 1100ms + active: true From e4ad2082bcc22d60244426e6b9fbf0dde51e73bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 15:26:16 +0100 Subject: [PATCH 63/95] [core] Add PROGMEM_STRING_TABLE macro for flash-optimized string lookups (#13659) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/cover/cover.cpp | 14 +--- .../components/light/light_json_schema.cpp | 37 +++------ esphome/components/logger/logger.cpp | 34 +++----- esphome/components/sensor/sensor.cpp | 20 ++--- esphome/components/sensor/sensor.h | 1 + esphome/components/valve/valve.cpp | 14 +--- .../wifi/wifi_component_esp8266.cpp | 43 ++++------ esphome/core/progmem.h | 83 +++++++++++++++++++ 8 files changed, 137 insertions(+), 109 deletions(-) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 37cb908d9f..0589aa2379 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -19,17 +19,11 @@ const LogString *cover_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *cover_operation_to_str(CoverOperation op) { - switch (op) { - case COVER_OPERATION_IDLE: - return LOG_STR("IDLE"); - case COVER_OPERATION_OPENING: - return LOG_STR("OPENING"); - case COVER_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + return CoverOperationStrings::get_log_str(static_cast<uint8_t>(op), CoverOperationStrings::LAST_INDEX); } Cover::Cover() : position{COVER_OPEN} {} diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 631f59221f..aaa1176f9f 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -9,32 +9,19 @@ namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Get JSON string for color mode. -// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would -// generate a large jump table. Converting to bit index (0-9) allows a compact switch. +// Color mode JSON strings - packed into flash with compile-time generated offsets. +// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff"). +PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", + "rgbww"); + +// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0). +// Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266. static ProgmemStr get_color_mode_json_str(ColorMode mode) { - switch (ColorModeBitPolicy::to_bit(mode)) { - case 1: - return ESPHOME_F("onoff"); - case 2: - return ESPHOME_F("brightness"); - case 3: - return ESPHOME_F("white"); - case 4: - return ESPHOME_F("color_temp"); - case 5: - return ESPHOME_F("cwww"); - case 6: - return ESPHOME_F("rgb"); - case 7: - return ESPHOME_F("rgbw"); - case 8: - return ESPHOME_F("rgbct"); - case 9: - return ESPHOME_F("rgbww"); - default: - return nullptr; - } + unsigned bit = ColorModeBitPolicy::to_bit(mode); + if (bit == 0) + return nullptr; + // bit is 1-9 for valid modes, so bit-1 is always valid (0-8). LAST_INDEX fallback never used. + return ColorModeStrings::get_progmem_str(bit - 1, ColorModeStrings::LAST_INDEX); } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 54b5670016..4cbd4f1bf1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -4,6 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::logger { @@ -241,34 +242,20 @@ UARTSelection Logger::get_uart() const { return this->uart_; } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } -#ifdef USE_STORE_LOG_STR_IN_FLASH -// ESP8266: PSTR() cannot be used in array initializers, so we need to declare -// each string separately as a global constant first -static const char LOG_LEVEL_NONE[] PROGMEM = "NONE"; -static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR"; -static const char LOG_LEVEL_WARN[] PROGMEM = "WARN"; -static const char LOG_LEVEL_INFO[] PROGMEM = "INFO"; -static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG"; -static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG"; -static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE"; -static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE"; +// Log level strings - packed into flash on ESP8266, indexed by log level (0-7) +PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); -static const LogString *const LOG_LEVELS[] = { - reinterpret_cast<const LogString *>(LOG_LEVEL_NONE), reinterpret_cast<const LogString *>(LOG_LEVEL_ERROR), - reinterpret_cast<const LogString *>(LOG_LEVEL_WARN), reinterpret_cast<const LogString *>(LOG_LEVEL_INFO), - reinterpret_cast<const LogString *>(LOG_LEVEL_CONFIG), reinterpret_cast<const LogString *>(LOG_LEVEL_DEBUG), - reinterpret_cast<const LogString *>(LOG_LEVEL_VERBOSE), reinterpret_cast<const LogString *>(LOG_LEVEL_VERY_VERBOSE), -}; -#else -static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; -#endif +static const LogString *get_log_level_str(uint8_t level) { + return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX); +} void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_])); + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)), + LOG_STR_ARG(get_log_level_str(this->current_level_))); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -287,7 +274,7 @@ void Logger::dump_config() { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif } @@ -295,7 +282,8 @@ void Logger::dump_config() { void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL))); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 3f2be02af2..ae2ee3e3d1 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::sensor { @@ -30,20 +31,13 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o } } +// State class strings indexed by StateClass enum (0-4): NONE, MEASUREMENT, TOTAL_INCREASING, TOTAL, MEASUREMENT_ANGLE +PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", "total", "measurement_angle"); +static_assert(StateClassStrings::COUNT == STATE_CLASS_LAST + 1, "StateClassStrings must match StateClass enum"); + const LogString *state_class_to_string(StateClass state_class) { - switch (state_class) { - case STATE_CLASS_MEASUREMENT: - return LOG_STR("measurement"); - case STATE_CLASS_TOTAL_INCREASING: - return LOG_STR("total_increasing"); - case STATE_CLASS_TOTAL: - return LOG_STR("total"); - case STATE_CLASS_MEASUREMENT_ANGLE: - return LOG_STR("measurement_angle"); - case STATE_CLASS_NONE: - default: - return LOG_STR(""); - } + // Fallback to index 0 (empty string for STATE_CLASS_NONE) if out of range + return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0); } Sensor::Sensor() : state(NAN), raw_state(NAN) {} diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index d9046020f6..f9a45cb1d0 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -32,6 +32,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, STATE_CLASS_MEASUREMENT_ANGLE = 4 }; +constexpr uint8_t STATE_CLASS_LAST = static_cast<uint8_t>(STATE_CLASS_MEASUREMENT_ANGLE); const LogString *state_class_to_string(StateClass state_class); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 607f614ef7..493ffd8da2 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -23,17 +23,11 @@ const LogString *valve_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Valve operation strings indexed by ValveOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(ValveOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *valve_operation_to_str(ValveOperation op) { - switch (op) { - case VALVE_OPERATION_IDLE: - return LOG_STR("IDLE"); - case VALVE_OPERATION_OPENING: - return LOG_STR("OPENING"); - case VALVE_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + return ValveOperationStrings::get_log_str(static_cast<uint8_t>(op), ValveOperationStrings::LAST_INDEX); } Valve::Valve() : position{VALVE_OPEN} {} diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c6bd40037d..0765fdc03b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -36,6 +36,7 @@ extern "C" { #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/util.h" namespace esphome::wifi { @@ -398,37 +399,23 @@ class WiFiMockClass : public ESP8266WiFiGenericClass { static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT }; +// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at last index +// Static asserts verify the SDK constants are contiguous as expected +static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4, + "AUTH_* constants are not contiguous"); +PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN"); + const LogString *get_auth_mode_str(uint8_t mode) { - switch (mode) { - case AUTH_OPEN: - return LOG_STR("OPEN"); - case AUTH_WEP: - return LOG_STR("WEP"); - case AUTH_WPA_PSK: - return LOG_STR("WPA PSK"); - case AUTH_WPA2_PSK: - return LOG_STR("WPA2 PSK"); - case AUTH_WPA_WPA2_PSK: - return LOG_STR("WPA/WPA2 PSK"); - default: - return LOG_STR("UNKNOWN"); - } -} -const LogString *get_op_mode_str(uint8_t mode) { - switch (mode) { - case WIFI_OFF: - return LOG_STR("OFF"); - case WIFI_STA: - return LOG_STR("STA"); - case WIFI_AP: - return LOG_STR("AP"); - case WIFI_AP_STA: - return LOG_STR("AP+STA"); - default: - return LOG_STR("UNKNOWN"); - } + return AuthModeStrings::get_log_str(mode, AuthModeStrings::LAST_INDEX); } +// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at last index +static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3, + "WIFI_* op mode constants are not contiguous"); +PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN"); + +const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); } + const LogString *get_disconnect_reason_str(uint8_t reason) { /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 4b897fb2de..6c6a5252cf 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -1,5 +1,11 @@ #pragma once +#include <array> +#include <cstddef> +#include <cstdint> + +#include "esphome/core/hal.h" // For PROGMEM definition + // Platform-agnostic macros for PROGMEM string handling // On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings // On other platforms: Use plain strings (no PROGMEM) @@ -32,3 +38,80 @@ using ProgmemStr = const __FlashStringHelper *; // Type for pointers to strings (no PROGMEM on non-ESP8266 platforms) using ProgmemStr = const char *; #endif + +namespace esphome { + +/// Helper for C++20 string literal template arguments +template<size_t N> struct FixedString { + char data[N]{}; + constexpr FixedString(const char (&str)[N]) { + for (size_t i = 0; i < N; ++i) + data[i] = str[i]; + } + constexpr size_t size() const { return N - 1; } // exclude null terminator +}; + +/// Compile-time string table that packs strings into a single blob with offset lookup. +/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266. +/// +/// Example: +/// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz"); +/// ProgmemStr str = MyStrings::get_progmem_str(idx, MyStrings::LAST_INDEX); // For ArduinoJson +/// const LogString *log_str = MyStrings::get_log_str(idx, MyStrings::LAST_INDEX); // For logging +/// +template<FixedString... Strs> struct ProgmemStringTable { + static constexpr size_t COUNT = sizeof...(Strs); + static constexpr size_t BLOB_SIZE = (0 + ... + (Strs.size() + 1)); + + /// Generate packed string blob at compile time + static constexpr auto make_blob() { + std::array<char, BLOB_SIZE> result{}; + size_t pos = 0; + auto copy = [&](const auto &str) { + for (size_t i = 0; i <= str.size(); ++i) + result[pos++] = str.data[i]; + }; + (copy(Strs), ...); + return result; + } + + /// Generate offset table at compile time (uint8_t limits blob to 255 bytes) + static constexpr auto make_offsets() { + static_assert(COUNT > 0, "PROGMEM_STRING_TABLE must contain at least one string"); + static_assert(COUNT <= 255, "PROGMEM_STRING_TABLE supports at most 255 strings with uint8_t indices"); + static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings"); + std::array<uint8_t, COUNT> result{}; + size_t pos = 0, idx = 0; + ((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...); + return result; + } +}; + +// Forward declaration for LogString (defined in log.h) +struct LogString; + +/// Instantiate a ProgmemStringTable with PROGMEM storage. +/// Creates: Name::get_progmem_str(idx, fallback), Name::get_log_str(idx, fallback) +/// If idx >= COUNT, returns string at fallback. Use LAST_INDEX for common patterns. +#define PROGMEM_STRING_TABLE(Name, ...) \ + struct Name { \ + using Table = ::esphome::ProgmemStringTable<__VA_ARGS__>; \ + static constexpr size_t COUNT = Table::COUNT; \ + static constexpr uint8_t LAST_INDEX = COUNT - 1; \ + static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ + static constexpr auto BLOB PROGMEM = Table::make_blob(); \ + static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ + static const char *get_(uint8_t idx, uint8_t fallback) { \ + if (idx >= COUNT) \ + idx = fallback; \ + return &BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]; \ + } \ + static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \ + return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \ + } \ + static const ::esphome::LogString *get_log_str(uint8_t idx, uint8_t fallback) { \ + return reinterpret_cast<const ::esphome::LogString *>(get_(idx, fallback)); \ + } \ + } + +} // namespace esphome From c3622ef7fb18aed84c1b59357a37945955bb7086 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 15:52:41 +0100 Subject: [PATCH 64/95] [http_request] Fix chunked transfer encoding on Arduino platforms (#13790) --- .../http_request/http_request_arduino.cpp | 193 ++++++++++++++++-- .../http_request/http_request_arduino.h | 18 ++ .../http_request/ota/ota_http_request.cpp | 6 +- esphome/core/helpers.cpp | 2 +- esphome/core/helpers.h | 5 +- 5 files changed, 198 insertions(+), 26 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 2f12b58766..aee1f651bf 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -133,20 +133,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur // HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length). // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). - // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the - // early return check (bytes_read_ >= content_length) will never trigger. - // - // TODO: Chunked transfer encoding is NOT properly supported on Arduino. - // The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where - // esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr() - // returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead - // of decoded content. This wasn't noticed because requests would complete and payloads - // were only examined on IDF. The long transfer times were also masked by the misleading - // "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues: - // 1. Response body is corrupted - contains chunk size headers mixed with data - // 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout - // The proper fix would be to use getString() for chunked responses, which decodes chunks - // internally, but this buffers the entire response in memory. + // The read() method uses a chunked transfer encoding decoder (read_chunked_) to strip + // chunk framing and deliver only decoded content. When the final 0-size chunk is received, + // is_chunked_ is cleared and content_length is set to the actual decoded size, so + // is_read_complete() returns true and callers exit their read loops correctly. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; @@ -174,6 +164,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur // > 0: bytes read // 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF! // < 0: error/connection closed <-- connection closed returns -1, not 0 +// +// For chunked transfer encoding, read_chunked_() decodes chunk framing and delivers +// only the payload data. When the final 0-size chunk is received, it clears is_chunked_ +// and sets content_length = bytes_read_ so is_read_complete() returns true. int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); @@ -184,24 +178,42 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return HTTP_ERROR_CONNECTION_CLOSED; } + if (this->is_chunked_) { + int result = this->read_chunked_(buf, max_len, stream_ptr); + this->duration_ms += (millis() - start); + if (result > 0) { + return result; + } + // result <= 0: check for completion or errors + if (this->is_read_complete()) { + return 0; // Chunked transfer complete (final 0-size chunk received) + } + if (result < 0) { + return result; // Stream error during chunk decoding + } + // read_chunked_ returned 0: no data was available (available() was 0). + // This happens when the TCP buffer is empty - either more data is in flight, + // or the connection dropped. Arduino's connected() returns false only when + // both the remote has closed AND the receive buffer is empty, so any buffered + // data is fully drained before we report the drop. + if (!stream_ptr->connected()) { + return HTTP_ERROR_CONNECTION_CLOSED; + } + return 0; // No data yet, caller should retry + } + + // Non-chunked path int available_data = stream_ptr->available(); - // For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when - // cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read. size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data)); if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (non-chunked only) - // For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false if (this->is_read_complete()) { return 0; // All content read successfully } - // No data available - check if connection is still open - // For chunked encoding, !connected() after reading means EOF (all chunks received) - // For known content_length with bytes_read_ < content_length, it means connection dropped if (!stream_ptr->connected()) { - return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked + return HTTP_ERROR_CONNECTION_CLOSED; } return 0; // No data yet, caller should retry } @@ -215,6 +227,143 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return read_len; } +void HttpContainerArduino::chunk_header_complete_() { + if (this->chunk_remaining_ == 0) { + this->chunk_state_ = ChunkedState::CHUNK_TRAILER; + this->chunk_remaining_ = 1; // repurpose as at-start-of-line flag + } else { + this->chunk_state_ = ChunkedState::CHUNK_DATA; + } +} + +// Chunked transfer encoding decoder +// +// On Arduino, getStreamPtr() returns raw TCP data. For chunked responses, this includes +// chunk framing (size headers, CRLF delimiters) mixed with payload data. This decoder +// strips the framing and delivers only decoded content to the caller. +// +// Chunk format (RFC 9112 Section 7.1): +// <hex-size>[;extension]\r\n +// <data bytes>\r\n +// ... +// 0\r\n +// [trailer-field\r\n]* +// \r\n +// +// Non-blocking: only processes bytes already in the TCP receive buffer. +// State (chunk_state_, chunk_remaining_) is preserved between calls, so partial +// chunk headers or split \r\n sequences resume correctly on the next call. +// Framing bytes (hex sizes, \r\n) may be consumed without producing output; +// the caller sees 0 and retries via the normal read timeout logic. +// +// WiFiClient::read() returns -1 on error despite available() > 0 (connection reset +// between check and read). On any stream error (c < 0 or readBytes <= 0), we return +// already-decoded data if any; otherwise HTTP_ERROR_CONNECTION_CLOSED. The error +// will surface again on the next call since the stream stays broken. +// +// Returns: > 0 decoded bytes, 0 no data available, < 0 error +int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream) { + int total_decoded = 0; + + while (total_decoded < (int) max_len && this->chunk_state_ != ChunkedState::COMPLETE) { + // Non-blocking: only process what's already buffered + if (stream->available() == 0) + break; + + // CHUNK_DATA reads multiple bytes; handle before the single-byte switch + if (this->chunk_state_ == ChunkedState::CHUNK_DATA) { + // Only read what's available, what fits in buf, and what remains in this chunk + size_t to_read = + std::min({max_len - (size_t) total_decoded, this->chunk_remaining_, (size_t) stream->available()}); + if (to_read == 0) + break; + App.feed_wdt(); + int read_len = stream->readBytes(buf + total_decoded, to_read); + if (read_len <= 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + total_decoded += read_len; + this->chunk_remaining_ -= read_len; + this->bytes_read_ += read_len; + if (this->chunk_remaining_ == 0) + this->chunk_state_ = ChunkedState::CHUNK_DATA_TRAIL; + continue; + } + + // All other states consume a single byte + int c = stream->read(); + if (c < 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + + switch (this->chunk_state_) { + // Parse hex chunk size, one byte at a time: "<hex>[;ext]\r\n" + // Note: if no hex digits are parsed (e.g., bare \r\n), chunk_remaining_ stays 0 + // and is treated as the final chunk. This is intentionally lenient — on embedded + // devices, rejecting malformed framing is less useful than terminating cleanly. + // Overflow of chunk_remaining_ from extremely long hex strings (>8 digits on + // 32-bit) is not checked; >4GB chunks are unrealistic on embedded targets and + // would simply cause fewer bytes to be read from that chunk. + case ChunkedState::CHUNK_HEADER: + if (c == '\n') { + // \n terminates the size line; chunk_remaining_ == 0 means last chunk + this->chunk_header_complete_(); + } else { + uint8_t hex = parse_hex_char(c); + if (hex != INVALID_HEX_CHAR) { + this->chunk_remaining_ = (this->chunk_remaining_ << 4) | hex; + } else if (c != '\r') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER_EXT; // ';' starts extension, skip to \n + } + } + break; + + // Skip chunk extension bytes until \n (e.g., ";name=value\r\n") + case ChunkedState::CHUNK_HEADER_EXT: + if (c == '\n') { + this->chunk_header_complete_(); + } + break; + + // Consume \r\n trailing each chunk's data + case ChunkedState::CHUNK_DATA_TRAIL: + if (c == '\n') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER; + this->chunk_remaining_ = 0; // reset for next chunk's hex accumulation + } + // else: \r is consumed silently, next iteration gets \n + break; + + // Consume optional trailer headers and terminating empty line after final chunk. + // Per RFC 9112 Section 7.1: "0\r\n" is followed by optional "field\r\n" lines + // and a final "\r\n". chunk_remaining_ is repurposed as a flag: 1 = at start + // of line (may be the empty terminator), 0 = mid-line (reading a trailer field). + case ChunkedState::CHUNK_TRAILER: + if (c == '\n') { + if (this->chunk_remaining_ != 0) { + this->chunk_state_ = ChunkedState::COMPLETE; // Empty line terminates trailers + } else { + this->chunk_remaining_ = 1; // End of trailer field, at start of next line + } + } else if (c != '\r') { + this->chunk_remaining_ = 0; // Non-CRLF char: reading a trailer field + } + // \r doesn't change the flag — it's part of \r\n line endings + break; + + default: + break; + } + + if (this->chunk_state_ == ChunkedState::COMPLETE) { + // Clear chunked flag and set content_length to actual decoded size so + // is_read_complete() returns true and callers exit their read loops + this->is_chunked_ = false; + this->content_length = this->bytes_read_; + } + } + + return total_decoded; +} + void HttpContainerArduino::end() { watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); this->client_.end(); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d9b5af9d81..a1084b12d5 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -18,6 +18,17 @@ namespace esphome::http_request { class HttpRequestArduino; + +/// State machine for decoding chunked transfer encoding on Arduino +enum class ChunkedState : uint8_t { + CHUNK_HEADER, ///< Reading hex digits of chunk size + CHUNK_HEADER_EXT, ///< Skipping chunk extensions until \n + CHUNK_DATA, ///< Reading chunk data bytes + CHUNK_DATA_TRAIL, ///< Skipping \r\n after chunk data + CHUNK_TRAILER, ///< Consuming trailer headers after final 0-size chunk + COMPLETE, ///< Finished: final chunk and trailers consumed +}; + class HttpContainerArduino : public HttpContainer { public: int read(uint8_t *buf, size_t max_len) override; @@ -26,6 +37,13 @@ class HttpContainerArduino : public HttpContainer { protected: friend class HttpRequestArduino; HTTPClient client_{}; + + /// Decode chunked transfer encoding from the raw stream + int read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream); + /// Transition from chunk header to data or trailer based on parsed size + void chunk_header_complete_(); + ChunkedState chunk_state_{ChunkedState::CHUNK_HEADER}; + size_t chunk_remaining_{0}; ///< Bytes remaining in current chunk }; class HttpRequestArduino : public HttpRequestComponent { diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8f4ecfab2d..882def4d7f 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -133,8 +133,10 @@ uint8_t OtaHttpRequestComponent::do_ota_() { auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; - // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, - // but this is defensive code in case chunked transfer encoding support is added for OTA in the future. + // For non-chunked responses, COMPLETE is unreachable (loop condition checks bytes_read < content_length). + // For chunked responses, the decoder sets content_length = bytes_read when the final chunk arrives, + // which causes the loop condition to terminate. But COMPLETE can still be returned if the decoder + // finishes mid-read, so this is needed for correctness. if (result == HttpReadLoopResult::COMPLETE) break; if (result != HttpReadLoopResult::DATA) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1a5d22f8d8..c2f7f67d9a 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -295,7 +295,7 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { size_t chars = std::min(length, 2 * count); for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) { uint8_t val = parse_hex_char(*str); - if (val > 15) + if (val == INVALID_HEX_CHAR) return 0; data[i >> 1] = (i & 1) ? data[i >> 1] | val : val << 4; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9c7060cd1d..f7de34b6d5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -874,6 +874,9 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> optional< } /// Parse a hex character to its nibble value (0-15), returns 255 on invalid input +/// Returned by parse_hex_char() for non-hex characters. +static constexpr uint8_t INVALID_HEX_CHAR = 255; + constexpr uint8_t parse_hex_char(char c) { if (c >= '0' && c <= '9') return c - '0'; @@ -881,7 +884,7 @@ constexpr uint8_t parse_hex_char(char c) { return c - 'A' + 10; if (c >= 'a' && c <= 'f') return c - 'a' + 10; - return 255; + return INVALID_HEX_CHAR; } /// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) From ec477801cab5ac6d9b91a0359af640eeeab34086 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:23:19 +0100 Subject: [PATCH 65/95] [wifi] Defer ESP8266 WiFi listener callbacks from system context to main loop (#13789) --- esphome/components/wifi/wifi_component.cpp | 35 ++++++++ esphome/components/wifi/wifi_component.h | 79 ++++++++++++------- .../wifi/wifi_component_esp8266.cpp | 60 +++++++------- .../wifi/wifi_component_esp_idf.cpp | 21 ++--- .../wifi/wifi_component_libretiny.cpp | 21 ++--- .../components/wifi/wifi_component_pico_w.cpp | 17 +--- 6 files changed, 132 insertions(+), 101 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e9b78c9225..0fe98162f3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1470,6 +1470,14 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->notify_connect_state_listeners_(); #endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) + // On ESP8266, GOT_IP event may not fire for static IP configurations, + // so notify IP state listeners here as a fallback. + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + this->notify_ip_state_listeners_(); + } +#endif + return; } @@ -1481,7 +1489,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { } if (this->error_from_callback_) { + // ESP8266: logging done in callback, listeners deferred via pending_.disconnect + // Other platforms: just log generic failure message +#ifndef USE_ESP8266 ESP_LOGW(TAG, "Connecting to network failed (callback)"); +#endif this->retry_connect(); return; } @@ -2202,8 +2214,31 @@ void WiFiComponent::notify_connect_state_listeners_() { listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); } } + +void WiFiComponent::notify_disconnect_state_listeners_() { + constexpr uint8_t empty_bssid[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), empty_bssid); + } +} #endif // USE_WIFI_CONNECT_STATE_LISTENERS +#ifdef USE_WIFI_IP_STATE_LISTENERS +void WiFiComponent::notify_ip_state_listeners_() { + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +} +#endif // USE_WIFI_IP_STATE_LISTENERS + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS +void WiFiComponent::notify_scan_results_listeners_() { + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +} +#endif // USE_WIFI_SCAN_RESULTS_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 98f339809a..58f19c184a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -594,6 +594,9 @@ class WiFiComponent : public Component { void connect_soon_(); void wifi_loop_(); +#ifdef USE_ESP8266 + void process_pending_callbacks_(); +#endif bool wifi_mode_(optional<bool> sta, optional<bool> ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); @@ -635,6 +638,16 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) void notify_connect_state_listeners_(); + /// Notify connect state listeners of disconnection + void notify_disconnect_state_listeners_(); +#endif +#ifdef USE_WIFI_IP_STATE_LISTENERS + /// Notify IP state listeners with current addresses + void notify_ip_state_listeners_(); +#endif +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + /// Notify scan results listeners with current scan results + void notify_scan_results_listeners_(); #endif #ifdef USE_ESP8266 @@ -658,13 +671,13 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif + // Large/pointer-aligned members first FixedVector<WiFiAP> sta_; std::vector<WiFiSTAPriority> sta_priorities_; wifi_scan_vector_t<WiFiScanResult> scan_result_; #ifdef USE_WIFI_AP WiFiAP ap_; #endif - float output_power_{NAN}; #ifdef USE_WIFI_IP_STATE_LISTENERS StaticVector<WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS> ip_state_listeners_; #endif @@ -681,6 +694,15 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; #endif +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes @@ -688,7 +710,8 @@ class WiFiComponent : public Component { static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; - // Group all 32-bit integers together + // 4-byte members + float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; @@ -697,7 +720,7 @@ class WiFiComponent : public Component { uint32_t ap_timeout_{}; #endif - // Group all 8-bit values together + // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; @@ -708,17 +731,39 @@ class WiFiComponent : public Component { // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; uint8_t roaming_attempts_{0}; - #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ + bool error_from_callback_{false}; + RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; + RoamingState roaming_state_{RoamingState::IDLE}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; +#endif - // Group all boolean values together + // Bools and bitfields + // Pending listener callbacks deferred from platform callbacks to main loop. + struct { +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Deferred until state machine reaches STA_CONNECTED so wifi.connected + // condition returns true in listener automations. + bool connect_state : 1; +#ifdef USE_ESP8266 + // ESP8266: also defer disconnect notification to main loop + bool disconnect : 1; +#endif +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) + bool got_ip : 1; +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_SCAN_RESULTS_LISTENERS) + bool scan_complete : 1; +#endif + } pending_{}; bool has_ap_{false}; #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; #endif - bool error_from_callback_{false}; bool scan_done_{false}; bool ap_setup_{false}; bool ap_started_{false}; @@ -733,32 +778,10 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started - RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; bool skip_cooldown_next_cycle_{false}; bool post_connect_roaming_{true}; // Enabled by default - RoamingState roaming_state_{RoamingState::IDLE}; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) - WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; bool is_high_performance_mode_{false}; - - SemaphoreHandle_t high_performance_semaphore_{nullptr}; -#endif - -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Pending listener notifications deferred until state machine reaches appropriate state. - // Listeners are notified after state transitions complete so conditions like - // wifi.connected return correct values in automations. - // Uses bitfields to minimize memory; more flags may be added as needed. - struct { - bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED - } pending_{}; -#endif - -#ifdef USE_WIFI_CONNECT_TRIGGER - Trigger<> connect_trigger_; -#endif -#ifdef USE_WIFI_DISCONNECT_TRIGGER - Trigger<> disconnect_trigger_; #endif private: diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0765fdc03b..6e2adcbf04 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -506,16 +506,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // Defer listener notification until state machine reaches STA_CONNECTED // This ensures wifi.connected condition returns true in listener automations global_wifi_component->pending_.connect_state = true; -#endif - // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here -#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) - if (const WiFiAP *config = global_wifi_component->get_selected_sta_(); - config && config->get_manual_ip().has_value()) { - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), - global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1)); - } - } #endif break; } @@ -534,16 +524,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; - // IMPORTANT: Set error flag BEFORE notifying listeners. - // This ensures is_connected() returns false during listener callbacks, - // which is critical for proper reconnection logic (e.g., roaming). global_wifi_component->error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Notify listeners AFTER setting error flag so they see correct state - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + global_wifi_component->pending_.disconnect = true; #endif break; } @@ -555,8 +538,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); - // we can't call retry_connect() from this context, so disconnect immediately - // and notify main thread with error_from_callback_ wifi_station_disconnect(); global_wifi_component->error_from_callback_ = true; } @@ -570,10 +551,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), - global_wifi_component->get_dns_address(1)); - } + // Defer listener callbacks to main loop - system context has limited stack + global_wifi_component->pending_.got_ip = true; #endif break; } @@ -785,9 +764,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { needs_full ? "" : " (filtered)"); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : global_wifi_component->scan_results_listeners_) { - listener->on_wifi_scan_results(global_wifi_component->scan_result_); - } + this->pending_.scan_complete = true; // Defer listener callbacks to main loop #endif } @@ -974,7 +951,34 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(&ip.gw); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); } -void WiFiComponent::wifi_loop_() {} +void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } + +void WiFiComponent::process_pending_callbacks_() { + // Process callbacks deferred from ESP8266 SDK system context (~2KB stack) + // to main loop context (full stack). Connect state listeners are handled + // by notify_connect_state_listeners_() in the shared state machine code. + +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + if (this->pending_.disconnect) { + this->pending_.disconnect = false; + this->notify_disconnect_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_IP_STATE_LISTENERS + if (this->pending_.got_ip) { + this->pending_.got_ip = false; + this->notify_ip_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + if (this->pending_.scan_complete) { + this->pending_.scan_complete = false; + this->notify_scan_results_listeners_(); + } +#endif +} } // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 22bf4be483..d74d083954 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -753,9 +753,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif @@ -779,10 +777,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connecting = false; error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -793,9 +788,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #if USE_NETWORK_IPV6 @@ -804,9 +797,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #endif /* USE_NETWORK_IPV6 */ @@ -883,9 +874,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 285a520ef5..5fd9d7663b 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -468,9 +468,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } #endif @@ -527,10 +525,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif break; } @@ -553,18 +548,14 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { ESP_LOGV(TAG, "Got IPv6"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } @@ -708,9 +699,7 @@ void WiFiComponent::wifi_scan_done_callback_() { needs_full ? "" : " (filtered)"); WiFi.scanDelete(); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1ce36c2d93..818ad1059c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -264,9 +264,7 @@ void WiFiComponent::wifi_loop_() { ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } @@ -290,9 +288,7 @@ void WiFiComponent::wifi_loop_() { #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_had_ip = true; - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif } else if (!is_connected && s_sta_was_connected) { @@ -301,10 +297,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } @@ -322,9 +315,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } } From 44f308502e130c1bc8f7c9643c4e8a3e270f17bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:37:02 +0100 Subject: [PATCH 66/95] [gpio] Convert interrupt_type_to_string to PROGMEM_STRING_TABLE (#13795) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../gpio/binary_sensor/gpio_binary_sensor.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 7a35596194..38ebbc90e4 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -1,5 +1,6 @@ #include "gpio_binary_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace gpio { @@ -7,17 +8,12 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +// Interrupt type strings indexed by edge-triggered InterruptType values: +// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0). +PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE"); + static const LogString *interrupt_type_to_string(gpio::InterruptType type) { - switch (type) { - case gpio::INTERRUPT_RISING_EDGE: - return LOG_STR("RISING_EDGE"); - case gpio::INTERRUPT_FALLING_EDGE: - return LOG_STR("FALLING_EDGE"); - case gpio::INTERRUPT_ANY_EDGE: - return LOG_STR("ANY_EDGE"); - default: - return LOG_STR("UNKNOWN"); - } + return InterruptTypeStrings::get_log_str(static_cast<uint8_t>(type), 0); } static const LogString *gpio_mode_to_string(bool use_interrupt) { From b7dc975331f944c14452542a1ed95c019872e856 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:37:19 +0100 Subject: [PATCH 67/95] [core] Convert entity string lookups to PROGMEM_STRING_TABLE (#13794) --- .../alarm_control_panel_state.cpp | 31 ++--- esphome/components/climate/climate_mode.cpp | 115 ++++-------------- esphome/components/fan/fan.cpp | 13 +- esphome/components/lock/lock.cpp | 20 +-- .../components/water_heater/water_heater.cpp | 24 +--- 5 files changed, 48 insertions(+), 155 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp index 862c620497..b8d246c861 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -1,32 +1,15 @@ #include "alarm_control_panel_state.h" +#include "esphome/core/progmem.h" namespace esphome::alarm_control_panel { +// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9) +PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT", + "ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN"); + const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { - switch (state) { - case ACP_STATE_DISARMED: - return LOG_STR("DISARMED"); - case ACP_STATE_ARMED_HOME: - return LOG_STR("ARMED_HOME"); - case ACP_STATE_ARMED_AWAY: - return LOG_STR("ARMED_AWAY"); - case ACP_STATE_ARMED_NIGHT: - return LOG_STR("ARMED_NIGHT"); - case ACP_STATE_ARMED_VACATION: - return LOG_STR("ARMED_VACATION"); - case ACP_STATE_ARMED_CUSTOM_BYPASS: - return LOG_STR("ARMED_CUSTOM_BYPASS"); - case ACP_STATE_PENDING: - return LOG_STR("PENDING"); - case ACP_STATE_ARMING: - return LOG_STR("ARMING"); - case ACP_STATE_DISARMING: - return LOG_STR("DISARMING"); - case ACP_STATE_TRIGGERED: - return LOG_STR("TRIGGERED"); - default: - return LOG_STR("UNKNOWN"); - } + return AlarmControlPanelStateStrings::get_log_str(static_cast<uint8_t>(state), + AlarmControlPanelStateStrings::LAST_INDEX); } } // namespace esphome::alarm_control_panel diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index b153ee0424..c4dd19d503 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -1,109 +1,44 @@ #include "climate_mode.h" +#include "esphome/core/progmem.h" namespace esphome::climate { +// Climate mode strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO +PROGMEM_STRING_TABLE(ClimateModeStrings, "OFF", "HEAT_COOL", "COOL", "HEAT", "FAN_ONLY", "DRY", "AUTO", "UNKNOWN"); + const LogString *climate_mode_to_string(ClimateMode mode) { - switch (mode) { - case CLIMATE_MODE_OFF: - return LOG_STR("OFF"); - case CLIMATE_MODE_HEAT_COOL: - return LOG_STR("HEAT_COOL"); - case CLIMATE_MODE_AUTO: - return LOG_STR("AUTO"); - case CLIMATE_MODE_COOL: - return LOG_STR("COOL"); - case CLIMATE_MODE_HEAT: - return LOG_STR("HEAT"); - case CLIMATE_MODE_FAN_ONLY: - return LOG_STR("FAN_ONLY"); - case CLIMATE_MODE_DRY: - return LOG_STR("DRY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateModeStrings::get_log_str(static_cast<uint8_t>(mode), ClimateModeStrings::LAST_INDEX); } + +// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN +PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN"); + const LogString *climate_action_to_string(ClimateAction action) { - switch (action) { - case CLIMATE_ACTION_OFF: - return LOG_STR("OFF"); - case CLIMATE_ACTION_COOLING: - return LOG_STR("COOLING"); - case CLIMATE_ACTION_HEATING: - return LOG_STR("HEATING"); - case CLIMATE_ACTION_IDLE: - return LOG_STR("IDLE"); - case CLIMATE_ACTION_DRYING: - return LOG_STR("DRYING"); - case CLIMATE_ACTION_FAN: - return LOG_STR("FAN"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateActionStrings::get_log_str(static_cast<uint8_t>(action), ClimateActionStrings::LAST_INDEX); } +// Climate fan mode strings indexed by ClimateFanMode enum (0-9): ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, +// DIFFUSE, QUIET +PROGMEM_STRING_TABLE(ClimateFanModeStrings, "ON", "OFF", "AUTO", "LOW", "MEDIUM", "HIGH", "MIDDLE", "FOCUS", "DIFFUSE", + "QUIET", "UNKNOWN"); + const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - return LOG_STR("ON"); - case climate::CLIMATE_FAN_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_FAN_AUTO: - return LOG_STR("AUTO"); - case climate::CLIMATE_FAN_LOW: - return LOG_STR("LOW"); - case climate::CLIMATE_FAN_MEDIUM: - return LOG_STR("MEDIUM"); - case climate::CLIMATE_FAN_HIGH: - return LOG_STR("HIGH"); - case climate::CLIMATE_FAN_MIDDLE: - return LOG_STR("MIDDLE"); - case climate::CLIMATE_FAN_FOCUS: - return LOG_STR("FOCUS"); - case climate::CLIMATE_FAN_DIFFUSE: - return LOG_STR("DIFFUSE"); - case climate::CLIMATE_FAN_QUIET: - return LOG_STR("QUIET"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateFanModeStrings::get_log_str(static_cast<uint8_t>(fan_mode), ClimateFanModeStrings::LAST_INDEX); } +// Climate swing mode strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL +PROGMEM_STRING_TABLE(ClimateSwingModeStrings, "OFF", "BOTH", "VERTICAL", "HORIZONTAL", "UNKNOWN"); + const LogString *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_SWING_BOTH: - return LOG_STR("BOTH"); - case climate::CLIMATE_SWING_VERTICAL: - return LOG_STR("VERTICAL"); - case climate::CLIMATE_SWING_HORIZONTAL: - return LOG_STR("HORIZONTAL"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateSwingModeStrings::get_log_str(static_cast<uint8_t>(swing_mode), ClimateSwingModeStrings::LAST_INDEX); } +// Climate preset strings indexed by ClimatePreset enum (0-7): NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY +PROGMEM_STRING_TABLE(ClimatePresetStrings, "NONE", "HOME", "AWAY", "BOOST", "COMFORT", "ECO", "SLEEP", "ACTIVITY", + "UNKNOWN"); + const LogString *climate_preset_to_string(ClimatePreset preset) { - switch (preset) { - case climate::CLIMATE_PRESET_NONE: - return LOG_STR("NONE"); - case climate::CLIMATE_PRESET_HOME: - return LOG_STR("HOME"); - case climate::CLIMATE_PRESET_ECO: - return LOG_STR("ECO"); - case climate::CLIMATE_PRESET_AWAY: - return LOG_STR("AWAY"); - case climate::CLIMATE_PRESET_BOOST: - return LOG_STR("BOOST"); - case climate::CLIMATE_PRESET_COMFORT: - return LOG_STR("COMFORT"); - case climate::CLIMATE_PRESET_SLEEP: - return LOG_STR("SLEEP"); - case climate::CLIMATE_PRESET_ACTIVITY: - return LOG_STR("ACTIVITY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimatePresetStrings::get_log_str(static_cast<uint8_t>(preset), ClimatePresetStrings::LAST_INDEX); } } // namespace esphome::climate diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index a983babe1c..d70a2940bc 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -2,21 +2,18 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace fan { static const char *const TAG = "fan"; +// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN +PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN"); + const LogString *fan_direction_to_string(FanDirection direction) { - switch (direction) { - case FanDirection::FORWARD: - return LOG_STR("FORWARD"); - case FanDirection::REVERSE: - return LOG_STR("REVERSE"); - default: - return LOG_STR("UNKNOWN"); - } + return FanDirectionStrings::get_log_str(static_cast<uint8_t>(direction), FanDirectionStrings::LAST_INDEX); } FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 4c61418256..939c84720b 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -8,22 +8,12 @@ namespace esphome::lock { static const char *const TAG = "lock"; +// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING +// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range +PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING"); + const LogString *lock_state_to_string(LockState state) { - switch (state) { - case LOCK_STATE_LOCKED: - return LOG_STR("LOCKED"); - case LOCK_STATE_UNLOCKED: - return LOG_STR("UNLOCKED"); - case LOCK_STATE_JAMMED: - return LOG_STR("JAMMED"); - case LOCK_STATE_LOCKING: - return LOG_STR("LOCKING"); - case LOCK_STATE_UNLOCKING: - return LOG_STR("UNLOCKING"); - case LOCK_STATE_NONE: - default: - return LOG_STR("UNKNOWN"); - } + return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0); } Lock::Lock() : state(LOCK_STATE_NONE) {} diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 286addf7db..e6d1562352 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -233,25 +233,13 @@ void WaterHeater::set_visual_target_temperature_step_override(float visual_targe } #endif +// Water heater mode strings indexed by WaterHeaterMode enum (0-6): OFF, ECO, ELECTRIC, PERFORMANCE, HIGH_DEMAND, +// HEAT_PUMP, GAS +PROGMEM_STRING_TABLE(WaterHeaterModeStrings, "OFF", "ECO", "ELECTRIC", "PERFORMANCE", "HIGH_DEMAND", "HEAT_PUMP", "GAS", + "UNKNOWN"); + const LogString *water_heater_mode_to_string(WaterHeaterMode mode) { - switch (mode) { - case WATER_HEATER_MODE_OFF: - return LOG_STR("OFF"); - case WATER_HEATER_MODE_ECO: - return LOG_STR("ECO"); - case WATER_HEATER_MODE_ELECTRIC: - return LOG_STR("ELECTRIC"); - case WATER_HEATER_MODE_PERFORMANCE: - return LOG_STR("PERFORMANCE"); - case WATER_HEATER_MODE_HIGH_DEMAND: - return LOG_STR("HIGH_DEMAND"); - case WATER_HEATER_MODE_HEAT_PUMP: - return LOG_STR("HEAT_PUMP"); - case WATER_HEATER_MODE_GAS: - return LOG_STR("GAS"); - default: - return LOG_STR("UNKNOWN"); - } + return WaterHeaterModeStrings::get_log_str(static_cast<uint8_t>(mode), WaterHeaterModeStrings::LAST_INDEX); } void WaterHeater::dump_traits_(const char *tag) { From 368ef5687b95476c48659d6d3618772992d729fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:37:41 +0100 Subject: [PATCH 68/95] [update] Move update_state_to_string to update component and convert to PROGMEM_STRING_TABLE (#13796) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/update/update_entity.cpp | 9 +++++++++ esphome/components/update/update_entity.h | 2 ++ esphome/components/web_server/web_server.cpp | 19 +++++-------------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 515e4c2c18..7edea2fe22 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -2,12 +2,21 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace update { static const char *const TAG = "update"; +// Update state strings indexed by UpdateState enum (0-3): UNKNOWN, NO UPDATE, UPDATE AVAILABLE, INSTALLING +PROGMEM_STRING_TABLE(UpdateStateStrings, "UNKNOWN", "NO UPDATE", "UPDATE AVAILABLE", "INSTALLING"); + +const LogString *update_state_to_string(UpdateState state) { + return UpdateStateStrings::get_log_str(static_cast<uint8_t>(state), + static_cast<uint8_t>(UpdateState::UPDATE_STATE_UNKNOWN)); +} + void UpdateEntity::publish_state() { ESP_LOGD(TAG, "'%s' >>\n" diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 8eba78b44b..405346bee4 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -27,6 +27,8 @@ enum UpdateState : uint8_t { UPDATE_STATE_INSTALLING, }; +const LogString *update_state_to_string(UpdateState state); + class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { public: void publish_state(); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 42219f3aac..dfd602be6b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -29,6 +29,10 @@ #include "esphome/components/climate/climate.h" #endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif + #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif @@ -2104,19 +2108,6 @@ std::string WebServer::event_json_(event::Event *obj, StringRef event_type, Json #endif #ifdef USE_UPDATE -static const LogString *update_state_to_string(update::UpdateState state) { - switch (state) { - case update::UPDATE_STATE_NO_UPDATE: - return LOG_STR("NO UPDATE"); - case update::UPDATE_STATE_AVAILABLE: - return LOG_STR("UPDATE AVAILABLE"); - case update::UPDATE_STATE_INSTALLING: - return LOG_STR("INSTALLING"); - default: - return LOG_STR("UNKNOWN"); - } -} - void WebServer::on_update(update::UpdateEntity *obj) { this->events_.deferrable_send_state(obj, "state", update_state_json_generator); } @@ -2158,7 +2149,7 @@ std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_ JsonObject root = builder.root(); char buf[PSTR_LOCAL_SIZE]; - set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update_state_to_string(obj->state)), + set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update::update_state_to_string(obj->state)), obj->update_info.latest_version, start_config); if (start_config == DETAIL_ALL) { root[ESPHOME_F("current_version")] = obj->update_info.current_version; From c7c9ffe7e15b27a8c117ada0e8d9f3310d861b4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:38:03 +0100 Subject: [PATCH 69/95] [light] Convert color_mode_to_human to PROGMEM_STRING_TABLE using to_bit() (#13797) --- esphome/components/light/light_call.cpp | 26 +++++++------------------ 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 6d42dd1513..f4bb0c5d11 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,6 +4,7 @@ #include "light_state.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" +#include "esphome/core/progmem.h" namespace esphome::light { @@ -51,26 +52,13 @@ static void log_invalid_parameter(const char *name, const LogString *message) { return *this; \ } +// Color mode human-readable strings indexed by ColorModeBitPolicy::to_bit() (0-9) +// Index 0 is Unknown (for ColorMode::UNKNOWN), also used as fallback for out-of-range +PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature", + "Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white"); + static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::ON_OFF) - return LOG_STR("On/Off"); - if (color_mode == ColorMode::BRIGHTNESS) - return LOG_STR("Brightness"); - if (color_mode == ColorMode::WHITE) - return LOG_STR("White"); - if (color_mode == ColorMode::COLOR_TEMPERATURE) - return LOG_STR("Color temperature"); - if (color_mode == ColorMode::COLD_WARM_WHITE) - return LOG_STR("Cold/warm white"); - if (color_mode == ColorMode::RGB) - return LOG_STR("RGB"); - if (color_mode == ColorMode::RGB_WHITE) - return LOG_STR("RGBW"); - if (color_mode == ColorMode::RGB_COLD_WARM_WHITE) - return LOG_STR("RGB + cold/warm white"); - if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) - return LOG_STR("RGB + color temperature"); - return LOG_STR("Unknown"); + return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0); } // Helper to log percentage values From 2917057da81c7d815551c82c9d220f15bef4a53e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 18:08:30 +0100 Subject: [PATCH 70/95] [analyze-memory] Trace CSWTCH switch table symbols to source components (#13798) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/analyze_memory/__init__.py | 217 ++++++++++++++++++++++++++++- esphome/analyze_memory/cli.py | 50 +++++++ esphome/analyze_memory/const.py | 13 +- 3 files changed, 267 insertions(+), 13 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 63ef0e74ed..d8c941e76f 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -12,7 +12,6 @@ from .const import ( CORE_SUBCATEGORY_PATTERNS, DEMANGLED_PATTERNS, ESPHOME_COMPONENT_PATTERN, - SECTION_TO_ATTR, SYMBOL_PATTERNS, ) from .demangle import batch_demangle @@ -91,6 +90,17 @@ class ComponentMemory: bss_size: int = 0 # Uninitialized data (ram only) symbol_count: int = 0 + def add_section_size(self, section_name: str, size: int) -> None: + """Add size to the appropriate attribute for a section.""" + if section_name == ".text": + self.text_size += size + elif section_name == ".rodata": + self.rodata_size += size + elif section_name == ".data": + self.data_size += size + elif section_name == ".bss": + self.bss_size += size + @property def flash_total(self) -> int: """Total flash usage (text + rodata + data).""" @@ -167,12 +177,15 @@ class MemoryAnalyzer: self._elf_symbol_names: set[str] = set() # SDK symbols not in ELF (static/local symbols from closed-source libs) self._sdk_symbols: list[SDKSymbol] = [] + # CSWTCH symbols: list of (name, size, source_file, component) + self._cswtch_symbols: list[tuple[str, int, str, str]] = [] def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" self._parse_sections() self._parse_symbols() self._categorize_symbols() + self._analyze_cswtch_symbols() self._analyze_sdk_libraries() return dict(self.components) @@ -255,8 +268,7 @@ class MemoryAnalyzer: comp_mem.symbol_count += 1 # Update the appropriate size attribute based on section - if attr_name := SECTION_TO_ATTR.get(section_name): - setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size) + comp_mem.add_section_size(section_name, size) # Track uncategorized symbols if component == "other" and size > 0: @@ -372,6 +384,205 @@ class MemoryAnalyzer: return "Other Core" + def _find_object_files_dir(self) -> Path | None: + """Find the directory containing object files for this build. + + Returns: + Path to the directory containing .o files, or None if not found. + """ + # The ELF is typically at .pioenvs/<env>/firmware.elf + # Object files are in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/ + pioenvs_dir = self.elf_path.parent + if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")): + return pioenvs_dir + return None + + def _scan_cswtch_in_objects( + self, obj_dir: Path + ) -> dict[str, list[tuple[str, int]]]: + """Scan object files for CSWTCH symbols using a single nm invocation. + + Uses ``nm --print-file-name -S`` on all ``.o`` files at once. + Output format: ``/path/to/file.o:address size type name`` + + Args: + obj_dir: Directory containing object files (.pioenvs/<env>/) + + Returns: + Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples. + """ + cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + + if not self.nm_path: + return cswtch_map + + # Find all .o files recursively, sorted for deterministic output + obj_files = sorted(obj_dir.rglob("*.o")) + if not obj_files: + return cswtch_map + + _LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files)) + + # Single nm call with --print-file-name for all object files + result = run_tool( + [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files], + timeout=30, + ) + if result is None or result.returncode != 0: + return cswtch_map + + for line in result.stdout.splitlines(): + if "CSWTCH$" not in line: + continue + + # Split on last ":" that precedes a hex address. + # nm --print-file-name format: filepath:hex_addr hex_size type name + # We split from the right: find the last colon followed by hex digits. + parts_after_colon = line.rsplit(":", 1) + if len(parts_after_colon) != 2: + continue + + file_path = parts_after_colon[0] + fields = parts_after_colon[1].split() + # fields: [address, size, type, name] + if len(fields) < 4: + continue + + sym_name = fields[3] + if not sym_name.startswith("CSWTCH$"): + continue + + try: + size = int(fields[1], 16) + except ValueError: + continue + + # Get relative path from obj_dir for readability + try: + rel_path = str(Path(file_path).relative_to(obj_dir)) + except ValueError: + rel_path = file_path + + key = f"{sym_name}:{size}" + cswtch_map[key].append((rel_path, size)) + + return cswtch_map + + def _source_file_to_component(self, source_file: str) -> str: + """Map a source object file path to its component name. + + Args: + source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o' + + Returns: + Component name like '[esphome]wifi' or the source file if unknown. + """ + parts = Path(source_file).parts + + # ESPHome component: src/esphome/components/<name>/... + if "components" in parts: + idx = parts.index("components") + if idx + 1 < len(parts): + component_name = parts[idx + 1] + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + + # ESPHome core: src/esphome/core/... or src/esphome/... + if "core" in parts and "esphome" in parts: + return _COMPONENT_CORE + if "esphome" in parts and "components" not in parts: + return _COMPONENT_CORE + + # Framework/library files - return the first path component + # e.g., lib65b/ESPAsyncTCP/... -> lib65b + # FrameworkArduino/... -> FrameworkArduino + return parts[0] if parts else source_file + + def _analyze_cswtch_symbols(self) -> None: + """Analyze CSWTCH (GCC switch table) symbols by tracing to source objects. + + CSWTCH symbols are compiler-generated lookup tables for switch statements. + They are local symbols, so the same name can appear in different object files. + This method scans .o files to attribute them to their source components. + """ + obj_dir = self._find_object_files_dir() + if obj_dir is None: + _LOGGER.debug("No object files directory found, skipping CSWTCH analysis") + return + + # Scan object files for CSWTCH symbols + cswtch_map = self._scan_cswtch_in_objects(obj_dir) + if not cswtch_map: + _LOGGER.debug("No CSWTCH symbols found in object files") + return + + # Collect CSWTCH symbols from the ELF (already parsed in sections) + # Include section_name for re-attribution of component totals + elf_cswtch = [ + (symbol_name, size, section_name) + for section_name, section in self.sections.items() + for symbol_name, size, _ in section.symbols + if symbol_name.startswith("CSWTCH$") + ] + + _LOGGER.debug( + "Found %d CSWTCH symbols in ELF, %d unique in object files", + len(elf_cswtch), + len(cswtch_map), + ) + + # Match ELF CSWTCH symbols to source files and re-attribute component totals. + # _categorize_symbols() already ran and put these into "other" since CSWTCH$ + # names don't match any component pattern. We move the bytes to the correct + # component based on the object file mapping. + other_mem = self.components.get("other") + + for sym_name, size, section_name in elf_cswtch: + key = f"{sym_name}:{size}" + sources = cswtch_map.get(key, []) + + if len(sources) == 1: + source_file = sources[0][0] + component = self._source_file_to_component(source_file) + elif len(sources) > 1: + # Ambiguous - multiple object files have same CSWTCH name+size + source_file = "ambiguous" + component = "ambiguous" + _LOGGER.debug( + "Ambiguous CSWTCH %s (%d B) found in %d files: %s", + sym_name, + size, + len(sources), + ", ".join(src for src, _ in sources), + ) + else: + source_file = "unknown" + component = "unknown" + + self._cswtch_symbols.append((sym_name, size, source_file, component)) + + # Re-attribute from "other" to the correct component + if ( + component not in ("other", "unknown", "ambiguous") + and other_mem is not None + ): + other_mem.add_section_size(section_name, -size) + if component not in self.components: + self.components[component] = ComponentMemory(component) + self.components[component].add_section_size(section_name, size) + + # Sort by size descending + self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True) + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + _LOGGER.debug( + "CSWTCH analysis: %d symbols, %d bytes total", + len(self._cswtch_symbols), + total_size, + ) + def get_unattributed_ram(self) -> tuple[int, int, int]: """Get unattributed RAM sizes (SDK/framework overhead). diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 72a73dbdd4..bb0eb7723e 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -184,6 +184,52 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" ) + def _add_cswtch_analysis(self, lines: list[str]) -> None: + """Add CSWTCH (GCC switch table lookup) analysis section.""" + self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)") + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + lines.append( + f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B" + ) + lines.append("") + + # Group by component + by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list) + for sym_name, size, source_file, component in self._cswtch_symbols: + by_component[component].append((sym_name, size, source_file)) + + # Sort components by total size descending + sorted_components = sorted( + by_component.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ) + + for component, symbols in sorted_components: + comp_total = sum(s[1] for s in symbols) + lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):") + + # Group by source file within component + by_file: dict[str, list[tuple[str, int]]] = defaultdict(list) + for sym_name, size, source_file in symbols: + by_file[source_file].append((sym_name, size)) + + for source_file, file_symbols in sorted( + by_file.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ): + file_total = sum(s[1] for s in file_symbols) + lines.append( + f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)" + ) + for sym_name, size in sorted( + file_symbols, key=lambda x: x[1], reverse=True + ): + lines.append(f" {size:>6,} B {sym_name}") + lines.append("") + def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" components = sorted( @@ -471,6 +517,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append("") + # CSWTCH (GCC switch table) analysis + if self._cswtch_symbols: + self._add_cswtch_analysis(lines) + lines.append( "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." ) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 83547b1eb5..66866615a6 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -66,15 +66,6 @@ SECTION_MAPPING = { ), } -# Section to ComponentMemory attribute mapping -# Maps section names to the attribute name in ComponentMemory dataclass -SECTION_TO_ATTR = { - ".text": "text_size", - ".rodata": "rodata_size", - ".data": "data_size", - ".bss": "bss_size", -} - # Component identification rules # Symbol patterns: patterns found in raw symbol names SYMBOL_PATTERNS = { @@ -513,7 +504,9 @@ SYMBOL_PATTERNS = { "__FUNCTION__$", "DAYS_IN_MONTH", "_DAYS_BEFORE_MONTH", - "CSWTCH$", + # Note: CSWTCH$ symbols are GCC switch table lookup tables. + # They are attributed to their source object files via _analyze_cswtch_symbols() + # rather than being lumped into libc. "dst$", "sulp", "_strtol_l", # String to long with locale From f9192b5f75af343a41eb00b1cf82dd9b8380c90a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 18:20:46 +0100 Subject: [PATCH 71/95] [wifi] Avoid jump tables in LOG_STR switch statements to save ESP8266 RAM (#13799) --- esphome/components/wifi/wifi_component.cpp | 30 ++-- .../wifi/wifi_component_esp8266.cpp | 150 ++++++++---------- 2 files changed, 81 insertions(+), 99 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0fe98162f3..e350f990af 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -236,25 +236,23 @@ static const char *const TAG = "wifi"; /// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// └──────────────────────────────────────────────────────────────────────┘ +// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { - switch (phase) { - case WiFiRetryPhase::INITIAL_CONNECT: - return LOG_STR("INITIAL_CONNECT"); + if (phase == WiFiRetryPhase::INITIAL_CONNECT) + return LOG_STR("INITIAL_CONNECT"); #ifdef USE_WIFI_FAST_CONNECT - case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: - return LOG_STR("FAST_CONNECT_CYCLING"); + if (phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) + return LOG_STR("FAST_CONNECT_CYCLING"); #endif - case WiFiRetryPhase::EXPLICIT_HIDDEN: - return LOG_STR("EXPLICIT_HIDDEN"); - case WiFiRetryPhase::SCAN_CONNECTING: - return LOG_STR("SCAN_CONNECTING"); - case WiFiRetryPhase::RETRY_HIDDEN: - return LOG_STR("RETRY_HIDDEN"); - case WiFiRetryPhase::RESTARTING_ADAPTER: - return LOG_STR("RESTARTING"); - default: - return LOG_STR("UNKNOWN"); - } + if (phase == WiFiRetryPhase::EXPLICIT_HIDDEN) + return LOG_STR("EXPLICIT_HIDDEN"); + if (phase == WiFiRetryPhase::SCAN_CONNECTING) + return LOG_STR("SCAN_CONNECTING"); + if (phase == WiFiRetryPhase::RETRY_HIDDEN) + return LOG_STR("RETRY_HIDDEN"); + if (phase == WiFiRetryPhase::RESTARTING_ADAPTER) + return LOG_STR("RESTARTING"); + return LOG_STR("UNKNOWN"); } bool WiFiComponent::went_through_explicit_hidden_phase_() const { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 6e2adcbf04..6488de8dae 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -416,75 +416,65 @@ PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN"); const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); } +// Use if-chain instead of switch to avoid jump tables in RODATA (wastes RAM on ESP8266). +// A single switch would generate a sparse lookup table with ~175 default entries, wasting 700 bytes of RAM. +// Even split switches still generate smaller jump tables in RODATA. const LogString *get_disconnect_reason_str(uint8_t reason) { - /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the - * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM - * per entry. As there's ~175 default entries, this wastes 700 bytes of RAM. - */ - if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200 - switch (reason) { - case REASON_AUTH_EXPIRE: - return LOG_STR("Auth Expired"); - case REASON_AUTH_LEAVE: - return LOG_STR("Auth Leave"); - case REASON_ASSOC_EXPIRE: - return LOG_STR("Association Expired"); - case REASON_ASSOC_TOOMANY: - return LOG_STR("Too Many Associations"); - case REASON_NOT_AUTHED: - return LOG_STR("Not Authenticated"); - case REASON_NOT_ASSOCED: - return LOG_STR("Not Associated"); - case REASON_ASSOC_LEAVE: - return LOG_STR("Association Leave"); - case REASON_ASSOC_NOT_AUTHED: - return LOG_STR("Association not Authenticated"); - case REASON_DISASSOC_PWRCAP_BAD: - return LOG_STR("Disassociate Power Cap Bad"); - case REASON_DISASSOC_SUPCHAN_BAD: - return LOG_STR("Disassociate Supported Channel Bad"); - case REASON_IE_INVALID: - return LOG_STR("IE Invalid"); - case REASON_MIC_FAILURE: - return LOG_STR("Mic Failure"); - case REASON_4WAY_HANDSHAKE_TIMEOUT: - return LOG_STR("4-Way Handshake Timeout"); - case REASON_GROUP_KEY_UPDATE_TIMEOUT: - return LOG_STR("Group Key Update Timeout"); - case REASON_IE_IN_4WAY_DIFFERS: - return LOG_STR("IE In 4-Way Handshake Differs"); - case REASON_GROUP_CIPHER_INVALID: - return LOG_STR("Group Cipher Invalid"); - case REASON_PAIRWISE_CIPHER_INVALID: - return LOG_STR("Pairwise Cipher Invalid"); - case REASON_AKMP_INVALID: - return LOG_STR("AKMP Invalid"); - case REASON_UNSUPP_RSN_IE_VERSION: - return LOG_STR("Unsupported RSN IE version"); - case REASON_INVALID_RSN_IE_CAP: - return LOG_STR("Invalid RSN IE Cap"); - case REASON_802_1X_AUTH_FAILED: - return LOG_STR("802.1x Authentication Failed"); - case REASON_CIPHER_SUITE_REJECTED: - return LOG_STR("Cipher Suite Rejected"); - } - } - - switch (reason) { - case REASON_BEACON_TIMEOUT: - return LOG_STR("Beacon Timeout"); - case REASON_NO_AP_FOUND: - return LOG_STR("AP Not Found"); - case REASON_AUTH_FAIL: - return LOG_STR("Authentication Failed"); - case REASON_ASSOC_FAIL: - return LOG_STR("Association Failed"); - case REASON_HANDSHAKE_TIMEOUT: - return LOG_STR("Handshake Failed"); - case REASON_UNSPECIFIED: - default: - return LOG_STR("Unspecified"); - } + if (reason == REASON_AUTH_EXPIRE) + return LOG_STR("Auth Expired"); + if (reason == REASON_AUTH_LEAVE) + return LOG_STR("Auth Leave"); + if (reason == REASON_ASSOC_EXPIRE) + return LOG_STR("Association Expired"); + if (reason == REASON_ASSOC_TOOMANY) + return LOG_STR("Too Many Associations"); + if (reason == REASON_NOT_AUTHED) + return LOG_STR("Not Authenticated"); + if (reason == REASON_NOT_ASSOCED) + return LOG_STR("Not Associated"); + if (reason == REASON_ASSOC_LEAVE) + return LOG_STR("Association Leave"); + if (reason == REASON_ASSOC_NOT_AUTHED) + return LOG_STR("Association not Authenticated"); + if (reason == REASON_DISASSOC_PWRCAP_BAD) + return LOG_STR("Disassociate Power Cap Bad"); + if (reason == REASON_DISASSOC_SUPCHAN_BAD) + return LOG_STR("Disassociate Supported Channel Bad"); + if (reason == REASON_IE_INVALID) + return LOG_STR("IE Invalid"); + if (reason == REASON_MIC_FAILURE) + return LOG_STR("Mic Failure"); + if (reason == REASON_4WAY_HANDSHAKE_TIMEOUT) + return LOG_STR("4-Way Handshake Timeout"); + if (reason == REASON_GROUP_KEY_UPDATE_TIMEOUT) + return LOG_STR("Group Key Update Timeout"); + if (reason == REASON_IE_IN_4WAY_DIFFERS) + return LOG_STR("IE In 4-Way Handshake Differs"); + if (reason == REASON_GROUP_CIPHER_INVALID) + return LOG_STR("Group Cipher Invalid"); + if (reason == REASON_PAIRWISE_CIPHER_INVALID) + return LOG_STR("Pairwise Cipher Invalid"); + if (reason == REASON_AKMP_INVALID) + return LOG_STR("AKMP Invalid"); + if (reason == REASON_UNSUPP_RSN_IE_VERSION) + return LOG_STR("Unsupported RSN IE version"); + if (reason == REASON_INVALID_RSN_IE_CAP) + return LOG_STR("Invalid RSN IE Cap"); + if (reason == REASON_802_1X_AUTH_FAILED) + return LOG_STR("802.1x Authentication Failed"); + if (reason == REASON_CIPHER_SUITE_REJECTED) + return LOG_STR("Cipher Suite Rejected"); + if (reason == REASON_BEACON_TIMEOUT) + return LOG_STR("Beacon Timeout"); + if (reason == REASON_NO_AP_FOUND) + return LOG_STR("AP Not Found"); + if (reason == REASON_AUTH_FAIL) + return LOG_STR("Authentication Failed"); + if (reason == REASON_ASSOC_FAIL) + return LOG_STR("Association Failed"); + if (reason == REASON_HANDSHAKE_TIMEOUT) + return LOG_STR("Handshake Failed"); + return LOG_STR("Unspecified"); } // TODO: This callback runs in ESP8266 system context with limited stack (~2KB). @@ -645,21 +635,15 @@ void WiFiComponent::wifi_pre_setup_() { WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { station_status_t status = wifi_station_get_connect_status(); - switch (status) { - case STATION_GOT_IP: - return WiFiSTAConnectStatus::CONNECTED; - case STATION_NO_AP_FOUND: - return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; - ; - case STATION_CONNECT_FAIL: - case STATION_WRONG_PASSWORD: - return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; - case STATION_CONNECTING: - return WiFiSTAConnectStatus::CONNECTING; - case STATION_IDLE: - default: - return WiFiSTAConnectStatus::IDLE; - } + if (status == STATION_GOT_IP) + return WiFiSTAConnectStatus::CONNECTED; + if (status == STATION_NO_AP_FOUND) + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD) + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + if (status == STATION_CONNECTING) + return WiFiSTAConnectStatus::CONNECTING; + return WiFiSTAConnectStatus::IDLE; } bool WiFiComponent::wifi_scan_start_(bool passive) { static bool first_scan = false; From 238e40966f8a373ac634987fd30f871d55f0cf59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 18:33:26 +0100 Subject: [PATCH 72/95] [light] Move CSWTCH lookup table to PROGMEM in get_suitable_color_modes_mask_ (#13801) --- esphome/components/light/light_call.cpp | 88 ++++++++++++++----------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index f4bb0c5d11..3b4e136ba5 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -445,6 +445,52 @@ ColorMode LightCall::compute_color_mode_() { LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } +// PROGMEM lookup table for get_suitable_color_modes_mask_(). +// Maps 4-bit key (white | ct<<1 | cwww<<2 | rgb<<3) to color mode bitmask. +// Packed into uint8_t by right-shifting by PACK_SHIFT since the lower bits +// (UNKNOWN, ON_OFF, BRIGHTNESS) are never present in suitable mode masks. +static constexpr unsigned PACK_SHIFT = ColorModeBitPolicy::to_bit(ColorMode::WHITE); +// clang-format off +static constexpr uint8_t SUITABLE_COLOR_MODES[] PROGMEM = { + // [0] none - all modes with brightness + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [1] white only + static_cast<uint8_t>(ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [2] ct only + static_cast<uint8_t>(ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [3] white + ct + static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [4] cwww only + static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + 0, // [5] white + cwww (conflicting) + 0, // [6] ct + cwww (conflicting) + 0, // [7] white + ct + cwww (conflicting) + // [8] rgb only + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [9] rgb + white + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [10] rgb + ct + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [11] rgb + white + ct + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [12] rgb + cwww + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + 0, // [13] rgb + white + cwww (conflicting) + 0, // [14] rgb + ct + cwww (conflicting) + 0, // [15] all (conflicting) +}; +// clang-format on + color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { bool has_white = this->has_white() && this->white_ > 0.0f; bool has_ct = this->has_color_temperature(); @@ -454,46 +500,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { (this->has_red() || this->has_green() || this->has_blue()); // Build key from flags: [rgb][cwww][ct][white] -#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) - - uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb); - - switch (key) { - case KEY(true, false, false, false): // white only - return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, - ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, true, false, false): // ct only - return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(true, true, false, false): // white + ct - return ColorModeMask( - {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, false, true, false): // cwww only - return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, false, false): // none - return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, - ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}) - .get_mask(); - case KEY(true, false, false, true): // rgb + white - return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, true, false, true): // rgb + ct - case KEY(true, true, false, true): // rgb + white + ct - return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, true, true): // rgb + cwww - return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, false, true): // rgb only - return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, - ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - default: - return 0; // conflicting flags - } - -#undef KEY + uint8_t key = has_white | (has_ct << 1) | (has_cwww << 2) | (has_rgb << 3); + return static_cast<color_mode_bitmask_t>(progmem_read_byte(&SUITABLE_COLOR_MODES[key])) << PACK_SHIFT; } LightCall &LightCall::set_effect(const char *effect, size_t len) { From 155447f541b18524ce8a9509e3614c1411c91e6e Mon Sep 17 00:00:00 2001 From: PolarGoose <35307286+PolarGoose@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:53:59 +0100 Subject: [PATCH 73/95] [dsmr] Fix issue with parsing lines like `1-0:0.2.0((ER11))` (#13780) --- .clang-tidy.hash | 2 +- esphome/components/dsmr/__init__.py | 2 +- esphome/components/dsmr/sensor.py | 8 -------- esphome/components/dsmr/text_sensor.py | 2 ++ platformio.ini | 2 +- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index ab354259e3..63ddbbac05 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3 +37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 386da3ce21..7d76856f28 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -66,7 +66,7 @@ async def to_code(config): cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("esphome/dsmr_parser", "1.0.0") + cg.add_library("esphome/dsmr_parser", "1.1.0") # Crypto cg.add_library("polargoose/Crypto-no-arduino", "0.4.0") diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 7d69f79530..863af42d1b 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -718,14 +718,6 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional("fw_core_version"): sensor.sensor_schema( - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("fw_module_version"): sensor.sensor_schema( - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 4c7455a38f..203c9c997e 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -26,7 +26,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(), + cv.Optional("fw_core_version"): text_sensor.text_sensor_schema(), cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(), + cv.Optional("fw_module_version"): text_sensor.text_sensor_schema(), cv.Optional("telegram"): text_sensor.text_sensor_schema().extend( {cv.Optional(CONF_INTERNAL, default=True): cv.boolean} ), diff --git a/platformio.ini b/platformio.ini index bb0de3c2b1..d198862a25 100644 --- a/platformio.ini +++ b/platformio.ini @@ -37,7 +37,7 @@ lib_deps_base = wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier - esphome/dsmr_parser@1.0.0 ; dsmr + esphome/dsmr_parser@1.1.0 ; dsmr polargoose/Crypto-no-arduino@0.4.0 ; dsmr https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps ; This is using the repository until a new release is published to PlatformIO From 9315da79bc016c5b45ba93ca85a2c257b89d559c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:03:16 -0500 Subject: [PATCH 74/95] [core] Add missing requests dependency to requirements.txt (#13803) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index bb118c5f9a..1771867535 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 +requests==2.32.5 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From 41cecbfb0f62db7e6dffff709e34141a06e59f7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 19:22:26 +0100 Subject: [PATCH 75/95] [template] Convert alarm sensor type to PROGMEM_STRING_TABLE and narrow enum to uint8_t (#13804) --- .../template_alarm_control_panel.cpp | 16 +++++----------- .../template_alarm_control_panel.h | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 1a5aef6b8d..09efe678ce 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -5,6 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::template_ { @@ -28,18 +29,11 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, this->sensor_data_.push_back(sd); }; +// Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS +PROGMEM_STRING_TABLE(AlarmSensorTypeStrings, "delayed", "instant", "delayed_follower", "instant_always"); + static const LogString *sensor_type_to_string(AlarmSensorType type) { - switch (type) { - case ALARM_SENSOR_TYPE_INSTANT: - return LOG_STR("instant"); - case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: - return LOG_STR("delayed_follower"); - case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: - return LOG_STR("instant_always"); - case ALARM_SENSOR_TYPE_DELAYED: - default: - return LOG_STR("delayed"); - } + return AlarmSensorTypeStrings::get_log_str(static_cast<uint8_t>(type), 0); } #endif diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index df3b64fb6e..4f32e99fd7 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -26,7 +26,7 @@ enum BinarySensorFlags : uint16_t { BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4, }; -enum AlarmSensorType : uint16_t { +enum AlarmSensorType : uint8_t { ALARM_SENSOR_TYPE_DELAYED = 0, ALARM_SENSOR_TYPE_INSTANT, ALARM_SENSOR_TYPE_DELAYED_FOLLOWER, From 86f91eed2f69a7cdb7789fb4927d560a7133e890 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 19:30:05 +0100 Subject: [PATCH 76/95] [mqtt] Move switch string tables to PROGMEM_STRING_TABLE (#13802) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../mqtt/mqtt_alarm_control_panel.cpp | 29 +---- esphome/components/mqtt/mqtt_client.cpp | 38 ++---- esphome/components/mqtt/mqtt_climate.cpp | 115 ++++-------------- esphome/components/mqtt/mqtt_component.cpp | 13 +- esphome/components/mqtt/mqtt_number.cpp | 17 ++- esphome/components/mqtt/mqtt_text.cpp | 14 +-- 6 files changed, 58 insertions(+), 168 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index dc8f75d8f5..a461a140ae 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -13,31 +13,12 @@ static const char *const TAG = "mqtt.alarm_control_panel"; using namespace esphome::alarm_control_panel; +// Alarm state MQTT strings indexed by AlarmControlPanelState enum (0-9) +PROGMEM_STRING_TABLE(AlarmMqttStateStrings, "disarmed", "armed_home", "armed_away", "armed_night", "armed_vacation", + "armed_custom_bypass", "pending", "arming", "disarming", "triggered", "unknown"); + static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { - switch (state) { - case ACP_STATE_DISARMED: - return ESPHOME_F("disarmed"); - case ACP_STATE_ARMED_HOME: - return ESPHOME_F("armed_home"); - case ACP_STATE_ARMED_AWAY: - return ESPHOME_F("armed_away"); - case ACP_STATE_ARMED_NIGHT: - return ESPHOME_F("armed_night"); - case ACP_STATE_ARMED_VACATION: - return ESPHOME_F("armed_vacation"); - case ACP_STATE_ARMED_CUSTOM_BYPASS: - return ESPHOME_F("armed_custom_bypass"); - case ACP_STATE_PENDING: - return ESPHOME_F("pending"); - case ACP_STATE_ARMING: - return ESPHOME_F("arming"); - case ACP_STATE_DISARMING: - return ESPHOME_F("disarming"); - case ACP_STATE_TRIGGERED: - return ESPHOME_F("triggered"); - default: - return ESPHOME_F("unknown"); - } + return AlarmMqttStateStrings::get_progmem_str(static_cast<uint8_t>(state), AlarmMqttStateStrings::LAST_INDEX); } MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index a284b162dd..d503461257 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -8,6 +8,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/version.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -27,6 +28,11 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8) +PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version", + "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized", + "Not Enough Space", "TLS Bad Fingerprint", "DNS Resolve Error", "Unknown"); + MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; char mac_addr[MAC_ADDRESS_BUFFER_SIZE]; @@ -348,36 +354,8 @@ void MQTTClientComponent::loop() { mqtt_backend_.loop(); if (this->disconnect_reason_.has_value()) { - const LogString *reason_s; - switch (*this->disconnect_reason_) { - case MQTTClientDisconnectReason::TCP_DISCONNECTED: - reason_s = LOG_STR("TCP disconnected"); - break; - case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: - reason_s = LOG_STR("Unacceptable Protocol Version"); - break; - case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: - reason_s = LOG_STR("Identifier Rejected"); - break; - case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: - reason_s = LOG_STR("Server Unavailable"); - break; - case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: - reason_s = LOG_STR("Malformed Credentials"); - break; - case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED: - reason_s = LOG_STR("Not Authorized"); - break; - case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: - reason_s = LOG_STR("Not Enough Space"); - break; - case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT: - reason_s = LOG_STR("TLS Bad Fingerprint"); - break; - default: - reason_s = LOG_STR("Unknown"); - break; - } + const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str( + static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX); if (!network::is_connected()) { reason_s = LOG_STR("WiFi disconnected"); } diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 673593ef84..158cfb5552 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -13,109 +13,44 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; +// Climate mode MQTT strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO +PROGMEM_STRING_TABLE(ClimateMqttModeStrings, "off", "heat_cool", "cool", "heat", "fan_only", "dry", "auto", "unknown"); + static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { - switch (mode) { - case CLIMATE_MODE_OFF: - return ESPHOME_F("off"); - case CLIMATE_MODE_HEAT_COOL: - return ESPHOME_F("heat_cool"); - case CLIMATE_MODE_AUTO: - return ESPHOME_F("auto"); - case CLIMATE_MODE_COOL: - return ESPHOME_F("cool"); - case CLIMATE_MODE_HEAT: - return ESPHOME_F("heat"); - case CLIMATE_MODE_FAN_ONLY: - return ESPHOME_F("fan_only"); - case CLIMATE_MODE_DRY: - return ESPHOME_F("dry"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), ClimateMqttModeStrings::LAST_INDEX); } +// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN +PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan", + "unknown"); + static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { - switch (action) { - case CLIMATE_ACTION_OFF: - return ESPHOME_F("off"); - case CLIMATE_ACTION_COOLING: - return ESPHOME_F("cooling"); - case CLIMATE_ACTION_HEATING: - return ESPHOME_F("heating"); - case CLIMATE_ACTION_IDLE: - return ESPHOME_F("idle"); - case CLIMATE_ACTION_DRYING: - return ESPHOME_F("drying"); - case CLIMATE_ACTION_FAN: - return ESPHOME_F("fan"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttActionStrings::get_progmem_str(static_cast<uint8_t>(action), ClimateMqttActionStrings::LAST_INDEX); } +// Climate fan mode MQTT strings indexed by ClimateFanMode enum (0-9) +PROGMEM_STRING_TABLE(ClimateMqttFanModeStrings, "on", "off", "auto", "low", "medium", "high", "middle", "focus", + "diffuse", "quiet", "unknown"); + static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) { - switch (fan_mode) { - case CLIMATE_FAN_ON: - return ESPHOME_F("on"); - case CLIMATE_FAN_OFF: - return ESPHOME_F("off"); - case CLIMATE_FAN_AUTO: - return ESPHOME_F("auto"); - case CLIMATE_FAN_LOW: - return ESPHOME_F("low"); - case CLIMATE_FAN_MEDIUM: - return ESPHOME_F("medium"); - case CLIMATE_FAN_HIGH: - return ESPHOME_F("high"); - case CLIMATE_FAN_MIDDLE: - return ESPHOME_F("middle"); - case CLIMATE_FAN_FOCUS: - return ESPHOME_F("focus"); - case CLIMATE_FAN_DIFFUSE: - return ESPHOME_F("diffuse"); - case CLIMATE_FAN_QUIET: - return ESPHOME_F("quiet"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttFanModeStrings::get_progmem_str(static_cast<uint8_t>(fan_mode), + ClimateMqttFanModeStrings::LAST_INDEX); } +// Climate swing mode MQTT strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL +PROGMEM_STRING_TABLE(ClimateMqttSwingModeStrings, "off", "both", "vertical", "horizontal", "unknown"); + static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) { - switch (swing_mode) { - case CLIMATE_SWING_OFF: - return ESPHOME_F("off"); - case CLIMATE_SWING_BOTH: - return ESPHOME_F("both"); - case CLIMATE_SWING_VERTICAL: - return ESPHOME_F("vertical"); - case CLIMATE_SWING_HORIZONTAL: - return ESPHOME_F("horizontal"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttSwingModeStrings::get_progmem_str(static_cast<uint8_t>(swing_mode), + ClimateMqttSwingModeStrings::LAST_INDEX); } +// Climate preset MQTT strings indexed by ClimatePreset enum (0-7) +PROGMEM_STRING_TABLE(ClimateMqttPresetStrings, "none", "home", "away", "boost", "comfort", "eco", "sleep", "activity", + "unknown"); + static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) { - switch (preset) { - case CLIMATE_PRESET_NONE: - return ESPHOME_F("none"); - case CLIMATE_PRESET_HOME: - return ESPHOME_F("home"); - case CLIMATE_PRESET_ECO: - return ESPHOME_F("eco"); - case CLIMATE_PRESET_AWAY: - return ESPHOME_F("away"); - case CLIMATE_PRESET_BOOST: - return ESPHOME_F("boost"); - case CLIMATE_PRESET_COMFORT: - return ESPHOME_F("comfort"); - case CLIMATE_PRESET_SLEEP: - return ESPHOME_F("sleep"); - case CLIMATE_PRESET_ACTIVITY: - return ESPHOME_F("activity"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttPresetStrings::get_progmem_str(static_cast<uint8_t>(preset), ClimateMqttPresetStrings::LAST_INDEX); } void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index a77afd3f4e..e4b80de50a 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -14,6 +14,9 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt.component"; +// Entity category MQTT strings indexed by EntityCategory enum: NONE(0) is skipped, CONFIG(1), DIAGNOSTIC(2) +PROGMEM_STRING_TABLE(EntityCategoryMqttStrings, "", "config", "diagnostic"); + // Helper functions for building topic strings on stack inline char *append_str(char *p, const char *s, size_t len) { memcpy(p, s, len); @@ -213,13 +216,9 @@ bool MQTTComponent::send_discovery_() { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); - switch (entity_category) { - case ENTITY_CATEGORY_NONE: - break; - case ENTITY_CATEGORY_CONFIG: - case ENTITY_CATEGORY_DIAGNOSTIC: - root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic"; - break; + if (entity_category != ENTITY_CATEGORY_NONE) { + root[MQTT_ENTITY_CATEGORY] = EntityCategoryMqttStrings::get_progmem_str( + static_cast<uint8_t>(entity_category), static_cast<uint8_t>(ENTITY_CATEGORY_CONFIG)); } if (config.state_topic) { diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 7dc93eee0c..fdc909fcc9 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -1,5 +1,6 @@ #include "mqtt_number.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.number"; using namespace esphome::number; +// Number mode MQTT strings indexed by NumberMode enum: AUTO(0) is skipped, BOX(1), SLIDER(2) +PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider"); + MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {} void MQTTNumberComponent::setup() { @@ -48,15 +52,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; } - switch (this->number_->traits.get_mode()) { - case NUMBER_MODE_AUTO: - break; - case NUMBER_MODE_BOX: - root[MQTT_MODE] = "box"; - break; - case NUMBER_MODE_SLIDER: - root[MQTT_MODE] = "slider"; - break; + const auto mode = this->number_->traits.get_mode(); + if (mode != NUMBER_MODE_AUTO) { + root[MQTT_MODE] = + NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX)); } const auto device_class = this->number_->traits.get_device_class_ref(); if (!device_class.empty()) { diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index 16293c0603..200e420c9c 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -1,5 +1,6 @@ #include "mqtt_text.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.text"; using namespace esphome::text; +// Text mode MQTT strings indexed by TextMode enum (0-1): TEXT, PASSWORD +PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password"); + MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {} void MQTTTextComponent::setup() { @@ -34,14 +38,8 @@ const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - switch (this->text_->traits.get_mode()) { - case TEXT_MODE_TEXT: - root[MQTT_MODE] = "text"; - break; - case TEXT_MODE_PASSWORD: - root[MQTT_MODE] = "password"; - break; - } + root[MQTT_MODE] = TextMqttModeStrings::get_progmem_str(static_cast<uint8_t>(this->text_->traits.get_mode()), + static_cast<uint8_t>(TEXT_MODE_TEXT)); config.command_topic = true; } From eb7aa3420fcf0d649953a0ce6c8643c0a2357e31 Mon Sep 17 00:00:00 2001 From: tronikos <tronikos@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:23:42 -0800 Subject: [PATCH 77/95] Add target_temperature to the template water heater (#13661) Co-authored-by: J. Nick Koston <nick@koston.org> --- .../components/template/water_heater/__init__.py | 9 +++++++++ .../water_heater/template_water_heater.cpp | 14 +++++++++++++- .../template/water_heater/template_water_heater.h | 4 ++++ tests/components/template/common-base.yaml | 1 + .../fixtures/water_heater_template.yaml | 1 + tests/integration/test_water_heater_template.py | 3 +++ 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index bddd378b23..5f96155fbf 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -46,6 +46,7 @@ CONFIG_SCHEMA = ( RESTORE_MODES, upper=True ), cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, + cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda, cv.Optional(CONF_MODE): cv.returning_lambda, cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( water_heater.validate_water_heater_mode @@ -78,6 +79,14 @@ async def to_code(config: ConfigType) -> None: ) cg.add(var.set_current_temperature_lambda(template_)) + if CONF_TARGET_TEMPERATURE in config: + template_ = await cg.process_lambda( + config[CONF_TARGET_TEMPERATURE], + [], + return_type=cg.optional.template(cg.float_), + ) + cg.add(var.set_target_temperature_lambda(template_)) + if CONF_MODE in config: template_ = await cg.process_lambda( config[CONF_MODE], diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index f888edb1df..c354deee0e 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() { restore->perform(); } } - if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value()) + if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() && + !this->mode_f_.has_value()) this->disable_loop(); } @@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { } traits.set_supports_current_temperature(true); + if (this->target_temperature_f_.has_value()) { + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE); + } return traits; } @@ -42,6 +46,14 @@ void TemplateWaterHeater::loop() { } } + auto target_temp = this->target_temperature_f_.call(); + if (target_temp.has_value()) { + if (*target_temp != this->target_temperature_) { + this->target_temperature_ = *target_temp; + changed = true; + } + } + auto new_mode = this->mode_f_.call(); if (new_mode.has_value()) { if (*new_mode != this->mode_) { diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index f1cf00a115..22173209aa 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -20,6 +20,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { template<typename F> void set_current_temperature_lambda(F &&f) { this->current_temperature_f_.set(std::forward<F>(f)); } + template<typename F> void set_target_temperature_lambda(F &&f) { + this->target_temperature_f_.set(std::forward<F>(f)); + } template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } @@ -44,6 +47,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller Trigger<> set_trigger_; TemplateLambda<float> current_temperature_f_; + TemplateLambda<float> target_temperature_f_; TemplateLambda<water_heater::WaterHeaterMode> mode_f_; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; water_heater::WaterHeaterModeMask supported_modes_; diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index d1849efaf7..b8742f8c7b 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -412,6 +412,7 @@ water_heater: name: "Template Water Heater" optimistic: true current_temperature: !lambda "return 42.0f;" + target_temperature: !lambda "return 60.0f;" mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" supported_modes: - "OFF" diff --git a/tests/integration/fixtures/water_heater_template.yaml b/tests/integration/fixtures/water_heater_template.yaml index b54ebed789..1aaded1991 100644 --- a/tests/integration/fixtures/water_heater_template.yaml +++ b/tests/integration/fixtures/water_heater_template.yaml @@ -10,6 +10,7 @@ water_heater: name: Test Boiler optimistic: true current_temperature: !lambda "return 45.0f;" + target_temperature: !lambda "return 60.0f;" # Note: No mode lambda - we want optimistic mode changes to stick # A mode lambda would override mode changes in loop() supported_modes: diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index b5f1fb64c0..6b4a685d0d 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -85,6 +85,9 @@ async def test_water_heater_template( assert initial_state.current_temperature == 45.0, ( f"Expected current temp 45.0, got {initial_state.current_temperature}" ) + assert initial_state.target_temperature == 60.0, ( + f"Expected target temp 60.0, got {initial_state.target_temperature}" + ) # Test changing to GAS mode client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS) From 9de91539e6d1990593400db003ad05a7c55795d3 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:24:57 +0100 Subject: [PATCH 78/95] [epaper_spi] Add Waveshare 1.54-G (#13758) --- esphome/components/epaper_spi/colorconv.h | 67 ++++++ .../epaper_spi/epaper_spi_jd79660.cpp | 227 ++++++++++++++++++ .../epaper_spi/epaper_spi_jd79660.h | 145 +++++++++++ .../components/epaper_spi/models/jd79660.py | 86 +++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 16 ++ 5 files changed, 541 insertions(+) create mode 100644 esphome/components/epaper_spi/colorconv.h create mode 100644 esphome/components/epaper_spi/epaper_spi_jd79660.cpp create mode 100644 esphome/components/epaper_spi/epaper_spi_jd79660.h create mode 100644 esphome/components/epaper_spi/models/jd79660.py diff --git a/esphome/components/epaper_spi/colorconv.h b/esphome/components/epaper_spi/colorconv.h new file mode 100644 index 0000000000..a2ea28f4b6 --- /dev/null +++ b/esphome/components/epaper_spi/colorconv.h @@ -0,0 +1,67 @@ +#pragma once + +#include <cstdint> +#include <algorithm> +#include "esphome/core/color.h" + +/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys + * + * Focus in driver layer is on efficiency. + * For optimum output quality on RGB inputs consider offline color keying/dithering. + * Also see e.g. Image component. + */ + +namespace esphome::epaper_spi { + +/** Delta for when to regard as gray */ +static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50; + +/** Map RGB color to discrete BWYR hex 4 color key + * + * @tparam NATIVE_COLOR Type of native hardware color values + * @param color RGB color to convert from + * @param hw_black Native value for black + * @param hw_white Native value for white + * @param hw_yellow Native value for yellow + * @param hw_red Native value for red + * @return Converted native hardware color value + * @internal Constexpr. Does not depend on side effects ("pure"). + */ +template<typename NATIVE_COLOR> +constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_yellow, + NATIVE_COLOR hw_red) { + // --- Step 1: Check for Grayscale (Black or White) --- + // We define "grayscale" as a color where the min and max components + // are close to each other. + + const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) { + // It's a shade of gray. Map to BLACK or WHITE. + // We split the luminance at the halfway point (382 = (255*3)/2) + if ((static_cast<int>(color.r) + color.g + color.b) > 382) { + return hw_white; + } + return hw_black; + } + + // --- Step 2: Check for Primary/Secondary Colors --- + // If it's not gray, it's a color. We check which components are + // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. + const bool r_on = (color.r > 128); + const bool g_on = (color.g > 128); + const bool b_on = (color.b > 128); + + if (r_on) { + if (!b_on) { + return g_on ? hw_yellow : hw_red; + } + + // At least red+blue high (but not gray) -> White + return hw_white; + } else { + return (b_on && g_on) ? hw_white : hw_black; + } +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_jd79660.cpp b/esphome/components/epaper_spi/epaper_spi_jd79660.cpp new file mode 100644 index 0000000000..1cd1087c6b --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_jd79660.cpp @@ -0,0 +1,227 @@ +#include "epaper_spi_jd79660.h" +#include "colorconv.h" + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.jd79660"; + +/** Pixel color as 2bpp. Must match IC LUT values. */ +enum JD79660Color : uint8_t { + BLACK = 0b00, + WHITE = 0b01, + YELLOW = 0b10, + RED = 0b11, +}; + +/** Map RGB color to JD79660 BWYR hex color keys */ +static JD79660Color HOT color_to_hex(Color color) { + return color_to_bwyr(color, JD79660Color::BLACK, JD79660Color::WHITE, JD79660Color::YELLOW, JD79660Color::RED); +} + +void EPaperJD79660::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + EPaperBase::fill(color); + return; + } + + const auto pixel_color = color_to_hex(color); + + // We store 4 pixels per byte + this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6)); +} + +void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + const auto pixel_bits = color_to_hex(color); + const uint32_t pixel_position = x + y * this->get_width_internal(); + // We store 4 pixels per byte at LSB offsets 6, 4, 2, 0 + const uint32_t byte_position = pixel_position / 4; + const uint32_t bit_offset = 6 - ((pixel_position % 4) * 2); + const auto original = this->buffer_[byte_position]; + + this->buffer_[byte_position] = (original & (~(0b11 << bit_offset))) | // mask old 2bpp + (pixel_bits << bit_offset); // add new 2bpp +} + +bool EPaperJD79660::reset() { + // On entry state RESET set step, next state will be RESET_END + if (this->state_ == EPaperState::RESET) { + this->step_ = FSMState::RESET_STEP0_H; + } + + switch (this->step_) { + case FSMState::RESET_STEP0_H: + // Step #0: Reset H for some settle time. + + ESP_LOGVV(TAG, "reset #0"); + this->reset_pin_->digital_write(true); + + this->reset_duration_ = SLEEP_MS_RESET0; + this->step_ = FSMState::RESET_STEP1_L; + return false; // another loop: step #1 below + + case FSMState::RESET_STEP1_L: + // Step #1: Reset L pulse for slightly >1.5ms. + // This is actual reset trigger. + + ESP_LOGVV(TAG, "reset #1"); + + // As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window. + // So do not use FSM loop, and avoid other calls/logs during pulse below. + this->reset_pin_->digital_write(false); + delay(SLEEP_MS_RESET1); + this->reset_pin_->digital_write(true); + + this->reset_duration_ = SLEEP_MS_RESET2; + this->step_ = FSMState::RESET_STEP2_IDLECHECK; + return false; // another loop: step #2 below + + case FSMState::RESET_STEP2_IDLECHECK: + // Step #2: Basically finished. Check sanity, and move FSM to INITIALISE state + ESP_LOGVV(TAG, "reset #2"); + + if (!this->is_idle_()) { + // Expectation: Idle after reset + settle time. + // Improperly connected/unexpected hardware? + // Error path reproducable e.g. with disconnected VDD/... pins + // (optimally while busy_pin configured with local pulldown). + // -> Mark failed to avoid followup problems. + this->mark_failed(LOG_STR("Busy after reset")); + } + break; // End state loop below + + default: + // Unexpected step = bug? + this->mark_failed(); + } + + this->step_ = FSMState::INIT_STEP0_REGULARINIT; // reset for initialize state + return true; +} + +bool EPaperJD79660::initialise(bool partial) { + switch (this->step_) { + case FSMState::INIT_STEP0_REGULARINIT: + // Step #0: Regular init sequence + ESP_LOGVV(TAG, "init #0"); + if (!EPaperBase::initialise(partial)) { // Call parent impl + return false; // If parent should request another loop, do so + } + + // Fast init requested + supported? + if (partial && (this->fast_update_length_ > 0)) { + this->step_ = FSMState::INIT_STEP1_FASTINIT; + this->wait_for_idle_(true); // Must wait for idle before fastinit sequence in next loop + return false; // another loop: step #1 below + } + + break; // End state loop below + + case FSMState::INIT_STEP1_FASTINIT: + // Step #1: Fast init sequence + ESP_LOGVV(TAG, "init #1"); + this->write_fastinit_(); + break; // End state loop below + + default: + // Unexpected step = bug? + this->mark_failed(); + } + + this->step_ = FSMState::NONE; + return true; // Finished: State transition waits for idle +} + +bool EPaperJD79660::transfer_buffer_chunks_() { + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + const uint32_t start_time = App.get_loop_component_start_time(); + const auto buffer_length = this->buffer_length_; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; + + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } + } + } + + // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis()); + } + // Cleanup for next transfer + this->current_data_index_ = 0; + + // Finished with all buffer chunks + return true; +} + +void EPaperJD79660::write_fastinit_() { + // Undocumented register sequence in vendor register range. + // Related to Fast Init/Update. + // Should likely happen after regular init seq and power on, but before refresh. + // Might only work for some models with certain factory MTP. + // Please do not change without knowledge to avoid breakage. + + this->send_init_sequence_(this->fast_update_, this->fast_update_length_); +} + +bool EPaperJD79660::transfer_data() { + // For now always send full frame buffer in chunks. + // JD79660 might support partial window transfers. But sample code missing. + // And likely minimal impact, solely on SPI transfer time into RAM. + + if (this->current_data_index_ == 0) { + this->command(CMD_TRANSFER); + } + + return this->transfer_buffer_chunks_(); +} + +void EPaperJD79660::refresh_screen([[maybe_unused]] bool partial) { + ESP_LOGV(TAG, "Refresh"); + this->cmd_data(CMD_REFRESH, {(uint8_t) 0x00}); +} + +void EPaperJD79660::power_off() { + ESP_LOGV(TAG, "Power off"); + this->cmd_data(CMD_POWEROFF, {(uint8_t) 0x00}); +} + +void EPaperJD79660::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + // "Deepsleep between update": Ensure EPD sleep to avoid early hardware wearout! + this->cmd_data(CMD_DEEPSLEEP, {(uint8_t) 0xA5}); + + // Notes: + // - VDD: Some boards (Waveshare) with "clever reset logic" would allow switching off + // EPD VDD by pulling reset pin low for longer time. + // However, a) not all boards have this, b) reliable sequence timing is difficult, + // c) saving is not worth it after deepsleep command above. + // If needed: Better option is to drive VDD via MOSFET with separate enable pin. + // + // - Possible safe shutdown: + // EPaperBase::on_safe_shutdown() may also trigger deep_sleep() again. + // Regularly, in IDLE state, this does not make sense for this "deepsleep between update" model, + // but SPI sequence should simply be ignored by sleeping receiver. + // But if triggering during lengthy update, this quick SPI sleep sequence may have benefit. + // Optimally, EPDs should even be set all white for longer storage. + // But full sequence (>15s) not possible w/o app logic. +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_jd79660.h b/esphome/components/epaper_spi/epaper_spi_jd79660.h new file mode 100644 index 0000000000..4e488fe93e --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_jd79660.h @@ -0,0 +1,145 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * JD7966x IC driver implementation + * + * Currently tested with: + * - JD79660 (max res: 200x200) + * + * May also work for other JD7966x chipset family members with minimal adaptations. + * + * Capabilities: + * - HW frame buffer layout: + * 4 colors (gray0..3, commonly BWYR). Bytes consist of 4px/2bpp. + * Width must be rounded to multiple of 4. + * - Fast init/update (shorter wave forms): Yes. Controlled by CONF_FULL_UPDATE_EVERY. + * Needs undocumented fastinit sequence, based on likely vendor specific MTP content. + * - Partial transfer (transfer only changed window): No. Maybe possible by HW. + * - Partial refresh (refresh only changed window): No. Likely HW limit. + * + * @internal \c final saves few bytes by devirtualization. Remove \c final when subclassing. + */ +class EPaperJD79660 final : public EPaperBase { + public: + EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR), + fast_update_(fast_update), + fast_update_length_(fast_update_length) { + this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp) + this->buffer_length_ = this->row_width_ * height; + } + + void fill(Color color) override; + + protected: + /** Draw colored pixel into frame buffer */ + void draw_pixel_at(int x, int y, Color color) override; + + /** Reset (multistep sequence) + * @pre this->reset_pin_ != nullptr // cv.Required check + * @post Should be idle on successful reset. Can mark failures. + */ + bool reset() override; + + /** Initialise (multistep sequence) */ + bool initialise(bool partial) override; + + /** Buffer transfer */ + bool transfer_data() override; + + /** Power on: Already part of init sequence (likely needed there before transferring buffers). + * So nothing to do in FSM state. + */ + void power_on() override {} + + /** Refresh screen + * @param partial Ignored: Needed earlier in \a ::initialize + * @pre Must be idle. + * @post Should return to idle later after processing. + */ + void refresh_screen([[maybe_unused]] bool partial) override; + + /** Power off + * @pre Must be idle. + * @post Should return to idle later after processing. + * (latter will take long period like ~15-20s on actual refresh!) + */ + void power_off() override; + + /** Deepsleep: Must be used to avoid hardware wearout! + * @pre Must be idle. + * @post Will go busy, and not return idle till ::reset! + */ + void deep_sleep() override; + + /** Internal: Send fast init sequence via undocumented vendor registers + * @pre Must be directly after regular ::initialise sequence, before ::transfer_data + * @pre Must be idle. + * @post Should return to idle later after processing. + */ + void write_fastinit_(); + + /** Internal: Send raw buffer in chunks + * \retval true Finished + * \retval false Loop time elapsed. Need to call again next loop. + */ + bool transfer_buffer_chunks_(); + + /** @name IC commands @{ */ + static constexpr uint8_t CMD_POWEROFF = 0x02; + static constexpr uint8_t CMD_DEEPSLEEP = 0x07; + static constexpr uint8_t CMD_TRANSFER = 0x10; + static constexpr uint8_t CMD_REFRESH = 0x12; + /** @} */ + + /** State machine constants for \a step_ */ + enum class FSMState : uint8_t { + NONE = 0, //!< Initial/default value: Unused + + /* Reset state steps */ + RESET_STEP0_H, + RESET_STEP1_L, + RESET_STEP2_IDLECHECK, + + /* Init state steps */ + INIT_STEP0_REGULARINIT, + INIT_STEP1_FASTINIT, + }; + + /** Wait time (millisec) for first reset phase: High + * + * Wait via FSM loop. + */ + static constexpr uint16_t SLEEP_MS_RESET0 = 200; + + /** Wait time (millisec) for second reset phase: Low + * + * Holding Reset Low too long may trigger "clever reset" logic + * of e.g. Waveshare Rev2 boards: VDD is shut down via MOSFET, and IC + * will not report idle anymore! + * FSM loop may spuriously increase delay, e.g. >16ms. + * Therefore, sync wait below, as allowed (code rule "delays > 10ms not permitted"), + * yet only slightly exceeding known IC min req of >1.5ms. + */ + static constexpr uint16_t SLEEP_MS_RESET1 = 2; + + /** Wait time (millisec) for third reset phase: High + * + * Wait via FSM loop. + */ + static constexpr uint16_t SLEEP_MS_RESET2 = 200; + + // properties initialised in the constructor + const uint8_t *const fast_update_{}; + const uint16_t fast_update_length_{}; + + /** Counter for tracking substeps within FSM state */ + FSMState step_{FSMState::NONE}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/jd79660.py b/esphome/components/epaper_spi/models/jd79660.py new file mode 100644 index 0000000000..2d8830ebd2 --- /dev/null +++ b/esphome/components/epaper_spi/models/jd79660.py @@ -0,0 +1,86 @@ +import esphome.codegen as cg +from esphome.components.mipi import flatten_sequence +import esphome.config_validation as cv +from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN +from esphome.core import ID + +from ..display import CONF_INIT_SEQUENCE_ID +from . import EpaperModel + + +class JD79660(EpaperModel): + def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs): + super().__init__(name, class_name, **kwargs) + self.fast_update = fast_update + + def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required: + # Validate required pins, as C++ code will assume existence + if name in (CONF_RESET_PIN, CONF_BUSY_PIN): + return cv.Required(name) + + # Delegate to parent + return super().option(name, fallback) + + def get_constructor_args(self, config) -> tuple: + # Resembles init_sequence handling for fast_update config + if self.fast_update is None: + fast_update = cg.nullptr, 0 + else: + flat_fast_update = flatten_sequence(self.fast_update) + fast_update = ( + cg.static_const_array( + ID( + config[CONF_INIT_SEQUENCE_ID].id + "_fast_update", type=cg.uint8 + ), + flat_fast_update, + ), + len(flat_fast_update), + ) + return (*fast_update,) + + +jd79660 = JD79660( + "jd79660", + # Specified refresh times are ~20s (full) or ~15s (fast) due to BWRY. + # So disallow low update intervals (with safety margin), to avoid e.g. FSM update loops. + # Even less frequent intervals (min/h) highly recommended to optimize lifetime! + minimum_update_interval="30s", + # SPI rate: From spec comparisons, IC should allow SCL write cycles up to 10MHz rate. + # Existing code samples also prefer 10MHz. So justifies as default. + # Decrease value further in user config if needed (e.g. poor cabling). + data_rate="10MHz", + # No need to set optional reset_duration: + # Code requires multistep reset sequence with precise timings + # according to data sheet or samples. +) + +# Waveshare 1.54-G +# +# Device may have specific factory provisioned MTP content to facilitate vendor register features like fast init. +# Vendor specific init derived from vendor sample code +# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/1in54_e-Paper_G/ESP32/EPD_1in54g.cpp> +# Compatible MIT license, see esphome/LICENSE file. +# +# fmt: off +jd79660.extend( + "Waveshare-1.54in-G", + width=200, + height=200, + + initsequence=( + (0x4D, 0x78,), + (0x00, 0x0F, 0x29,), + (0x06, 0x0d, 0x12, 0x30, 0x20, 0x19, 0x2a, 0x22,), + (0x50, 0x37,), + (0x61, 200 // 256, 200 % 256, 200 // 256, 200 % 256,), # RES: 200x200 fixed + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), + fast_update=( + (0xE0, 0x02,), + (0xE6, 0x5D,), + (0xA5, 0x00,), + ), +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 621a819c3c..333ab567cd 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -25,6 +25,22 @@ display: lambda: |- it.circle(64, 64, 50, Color::BLACK); + - platform: epaper_spi + spi_id: spi_bus + model: waveshare-1.54in-G + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + - platform: epaper_spi spi_id: spi_bus model: waveshare-2.13in-v3 From a43e3e59483a239467d1db8201522db25291371d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Feb 2026 22:19:20 +0100 Subject: [PATCH 79/95] [dashboard] Close WebSocket after process exit to prevent zombie connections (#13834) --- esphome/dashboard/web_server.py | 1 + tests/dashboard/test_web_server.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index f94d8eea22..da50279864 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -317,6 +317,7 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl # Check if the proc was not forcibly closed _LOGGER.info("Process exited with return code %s", returncode) self.write_message({"event": "exit", "code": returncode}) + self.close() def on_close(self) -> None: # Check if proc exists (if 'start' has been run) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 10ca6061e6..7642876ee5 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -29,7 +29,7 @@ from esphome.dashboard.entries import ( bool_to_entry_state, ) from esphome.dashboard.models import build_importable_device_dict -from esphome.dashboard.web_server import DashboardSubscriber +from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket from esphome.zeroconf import DiscoveredImport from .common import get_fixture_path @@ -1654,3 +1654,25 @@ async def test_websocket_check_origin_multiple_trusted_domains( assert data["event"] == "initial_state" finally: ws.close() + + +def test_proc_on_exit_calls_close() -> None: + """Test _proc_on_exit sends exit event and closes the WebSocket.""" + handler = Mock(spec=EsphomeCommandWebSocket) + handler._is_closed = False + + EsphomeCommandWebSocket._proc_on_exit(handler, 0) + + handler.write_message.assert_called_once_with({"event": "exit", "code": 0}) + handler.close.assert_called_once() + + +def test_proc_on_exit_skips_when_already_closed() -> None: + """Test _proc_on_exit does nothing when WebSocket is already closed.""" + handler = Mock(spec=EsphomeCommandWebSocket) + handler._is_closed = True + + EsphomeCommandWebSocket._proc_on_exit(handler, 0) + + handler.write_message.assert_not_called() + handler.close.assert_not_called() From 7b40e8afcb5c60ec15436da1e5b4588bd028df81 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:21:37 +0100 Subject: [PATCH 80/95] [epaper_spi] Declare leaf classes final (#13776) --- esphome/components/epaper_spi/epaper_spi_spectra_e6.h | 2 +- esphome/components/epaper_spi/epaper_waveshare.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index b8dbf0b0c5..9c251068af 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -4,7 +4,7 @@ namespace esphome::epaper_spi { -class EPaperSpectraE6 : public EPaperBase { +class EPaperSpectraE6 final : public EPaperBase { public: EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, size_t init_sequence_length) diff --git a/esphome/components/epaper_spi/epaper_waveshare.h b/esphome/components/epaper_spi/epaper_waveshare.h index 15fb575ca0..d3ad313d92 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.h +++ b/esphome/components/epaper_spi/epaper_waveshare.h @@ -6,7 +6,7 @@ namespace esphome::epaper_spi { /** * An epaper display that needs LUTs to be sent to it. */ -class EpaperWaveshare : public EPaperMono { +class EpaperWaveshare final : public EPaperMono { public: EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut, From 41fedaedb3aea2a93621f7715e5d6588414eb696 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Feb 2026 08:26:47 -0600 Subject: [PATCH 81/95] [udp] Eliminate per-loop heap allocation using std::span (#13838) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/codegen.py | 1 + .../packet_transport/packet_transport.cpp | 4 +- .../packet_transport/packet_transport.h | 5 ++- esphome/components/udp/__init__.py | 16 ++++++-- .../udp/packet_transport/udp_transport.cpp | 2 +- esphome/components/udp/udp_component.cpp | 8 ++-- esphome/components/udp/udp_component.h | 6 ++- esphome/cpp_types.py | 1 + tests/integration/test_udp.py | 39 ++++++++++++++++--- 9 files changed, 62 insertions(+), 20 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 4a2a5975c6..c5283f4967 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -87,6 +87,7 @@ from esphome.cpp_types import ( # noqa: F401 size_t, std_ns, std_shared_ptr, + std_span, std_string, std_string_ref, std_vector, diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index cefe9a604e..365a5f2ec7 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) { /** * Process a received packet */ -void PacketTransport::process_(const std::vector<uint8_t> &data) { +void PacketTransport::process_(std::span<const uint8_t> data) { auto ping_key_seen = !this->ping_pong_enable_; - PacketDecoder decoder((data.data()), data.size()); + PacketDecoder decoder(data.data(), data.size()); char namebuf[256]{}; uint8_t byte; FuData rdata{}; diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 86ec564fce..57f40874b5 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -9,8 +9,9 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -#include <vector> #include <map> +#include <span> +#include <vector> /** * Providing packet encoding functions for exchanging data with a remote host. @@ -113,7 +114,7 @@ class PacketTransport : public PollingComponent { virtual bool should_send() { return true; } // to be called by child classes when a data packet is received. - void process_(const std::vector<uint8_t> &data); + void process_(std::span<const uint8_t> data); void send_data_(bool all); void flush_(); void add_data_(uint8_t key, const char *id, float data); diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 8252e35023..bfaa5f2516 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -13,7 +13,7 @@ from esphome.components.packet_transport import ( import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID -from esphome.cpp_generator import literal +from esphome.cpp_generator import MockObj CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -23,8 +23,12 @@ MULTI_CONF = True udp_ns = cg.esphome_ns.namespace("udp") UDPComponent = udp_ns.class_("UDPComponent", cg.Component) UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action) -trigger_args = cg.std_vector.template(cg.uint8) trigger_argname = "data" +# Listener callback type (non-owning span from UDP component) +listener_args = cg.std_span.template(cg.uint8.operator("const")) +listener_argtype = [(listener_args, trigger_argname)] +# Automation/trigger type (owned vector, safe for deferred actions like delay) +trigger_args = cg.std_vector.template(cg.uint8) trigger_argtype = [(trigger_args, trigger_argname)] CONF_ADDRESSES = "addresses" @@ -118,7 +122,13 @@ async def to_code(config): trigger_id, trigger_argtype, on_receive ) trigger_lambda = await cg.process_lambda( - trigger.trigger(literal(trigger_argname)), trigger_argtype + trigger.trigger( + cg.std_vector.template(cg.uint8)( + MockObj(trigger_argname).begin(), + MockObj(trigger_argname).end(), + ) + ), + listener_argtype, ) cg.add(var.add_listener(trigger_lambda)) cg.add(var.set_should_listen()) diff --git a/esphome/components/udp/packet_transport/udp_transport.cpp b/esphome/components/udp/packet_transport/udp_transport.cpp index f3e33573a5..b5e73af777 100644 --- a/esphome/components/udp/packet_transport/udp_transport.cpp +++ b/esphome/components/udp/packet_transport/udp_transport.cpp @@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); } void UDPTransport::setup() { PacketTransport::setup(); if (!this->providers_.empty() || this->is_encrypted_()) { - this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); }); + this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); }); } } diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 947a59dfa9..c144212ecf 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -103,8 +103,8 @@ void UDPComponent::setup() { } void UDPComponent::loop() { - auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE); if (this->should_listen_) { + std::array<uint8_t, MAX_PACKET_SIZE> buf; for (;;) { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) auto len = this->listen_socket_->read(buf.data(), buf.size()); @@ -116,9 +116,9 @@ void UDPComponent::loop() { #endif if (len <= 0) break; - buf.resize(len); - ESP_LOGV(TAG, "Received packet of length %zu", len); - this->packet_listeners_.call(buf); + size_t packet_len = static_cast<size_t>(len); + ESP_LOGV(TAG, "Received packet of length %zu", packet_len); + this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len)); } } } diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index 9967e4dbbb..7fd6308065 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -10,7 +10,9 @@ #ifdef USE_SOCKET_IMPL_LWIP_TCP #include <WiFiUdp.h> #endif +#include <array> #include <initializer_list> +#include <span> #include <vector> namespace esphome::udp { @@ -26,7 +28,7 @@ class UDPComponent : public Component { void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; } void set_should_broadcast() { this->should_broadcast_ = true; } void set_should_listen() { this->should_listen_ = true; } - void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) { + void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) { this->packet_listeners_.add(std::move(listener)); } void setup() override; @@ -41,7 +43,7 @@ class UDPComponent : public Component { uint16_t broadcast_port_{}; bool should_broadcast_{}; bool should_listen_{}; - CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{}; + CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{}; #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr; diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 7001c38857..6d255bc0be 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -12,6 +12,7 @@ std_shared_ptr = std_ns.class_("shared_ptr") std_string = std_ns.class_("string") std_string_ref = std_ns.namespace("string &") std_vector = std_ns.class_("vector") +std_span = std_ns.class_("span") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") diff --git a/tests/integration/test_udp.py b/tests/integration/test_udp.py index 74c7ef60e3..2187d13814 100644 --- a/tests/integration/test_udp.py +++ b/tests/integration/test_udp.py @@ -93,23 +93,34 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]] sock.close() +def _get_free_udp_port() -> int: + """Get a free UDP port by binding to port 0 and releasing.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + sock.close() + return port + + @pytest.mark.asyncio async def test_udp_send_receive( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test UDP component can send messages with multiple addresses configured.""" - # Track log lines to verify dump_config output + """Test UDP component can send and receive messages.""" log_lines: list[str] = [] + receive_event = asyncio.Event() def on_log_line(line: str) -> None: log_lines.append(line) + if "Received UDP:" in line: + receive_event.set() - async with udp_listener() as (udp_port, receiver): - # Replace placeholders in the config - config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1)) - config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port)) + async with udp_listener() as (broadcast_port, receiver): + listen_port = _get_free_udp_port() + config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port)) + config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port)) async with ( run_compiled(config, line_callback=on_log_line), @@ -169,3 +180,19 @@ async def test_udp_send_receive( assert "Address: 127.0.0.2" in log_text, ( f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}" ) + + # Test receiving a UDP packet (exercises on_receive with std::span) + test_payload = b"TEST_RECEIVE_UDP" + send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + send_sock.sendto(test_payload, ("127.0.0.1", listen_port)) + finally: + send_sock.close() + + try: + await asyncio.wait_for(receive_event.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"on_receive did not fire. Expected 'Received UDP:' in logs. " + f"Last log lines: {log_lines[-20:]}" + ) From 28b9487b25043dcbecb12068d617bc1909acb22f Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Feb 2026 18:52:05 +0100 Subject: [PATCH 82/95] [nrf52,logger] fix printk (#13874) --- esphome/components/logger/logger_zephyr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index ef1702c5c1..1fc0acd573 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) { #ifdef CONFIG_PRINTK // Requires the debug component and an active SWD connection. // It is used for pyocd rtt -t nrf52840 - k_str_out(const_cast<char *>(msg), len); + printk("%.*s", static_cast<int>(len), msg); #endif if (this->uart_dev_ == nullptr) { return; From 756f1c6b7e8162752af7ca8191e2eff71a122e3a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:53:43 +1100 Subject: [PATCH 83/95] [lvgl] Fix crash with unconfigured `top_layer` (#13846) --- esphome/components/lvgl/schemas.py | 1 + tests/components/lvgl/test.host.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 45d933c00e..2aeeedbd10 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -436,6 +436,7 @@ def container_schema(widget_type: WidgetType, extras=None): schema = schema.extend(widget_type.schema) def validator(value): + value = value or {} return append_layout_schema(schema, value)(value) return validator diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 00a8cd8c01..f84156c9d8 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -20,6 +20,8 @@ lvgl: - id: lvgl_0 default_font: space16 displays: sdl0 + top_layer: + - id: lvgl_1 displays: sdl1 on_idle: From 140ec0639ca3f0e8cac0c839d5e93f67cbee2621 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:24:45 -0600 Subject: [PATCH 84/95] [api] Elide empty message construction in protobuf dispatch (#13871) --- esphome/components/api/api_connection.cpp | 17 ++-- esphome/components/api/api_connection.h | 24 +++-- esphome/components/api/api_pb2_service.cpp | 106 ++++++++------------- esphome/components/api/api_pb2_service.h | 62 ++++++------ script/api_protobuf/api_protobuf.py | 53 ++++++----- 5 files changed, 117 insertions(+), 145 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2aa5956f24..efc3d210b4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -283,7 +283,7 @@ void APIConnection::loop() { #endif } -bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { +bool APIConnection::send_disconnect_response() { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop @@ -292,7 +292,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { DisconnectResponse resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); } -void APIConnection::on_disconnect_response(const DisconnectResponse &value) { +void APIConnection::on_disconnect_response() { this->helper_->close(); this->flags_.remove = true; } @@ -1095,7 +1095,7 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); } -void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { +void APIConnection::unsubscribe_bluetooth_le_advertisements() { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { @@ -1121,8 +1121,7 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); } -bool APIConnection::send_subscribe_bluetooth_connections_free_response( - const SubscribeBluetoothConnectionsFreeRequest &msg) { +bool APIConnection::send_subscribe_bluetooth_connections_free_response() { bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this); return true; } @@ -1491,12 +1490,12 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -bool APIConnection::send_ping_response(const PingRequest &msg) { +bool APIConnection::send_ping_response() { PingResponse resp; return this->send_message(resp, PingResponse::MESSAGE_TYPE); } -bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { +bool APIConnection::send_device_info_response() { DeviceInfoResponse resp{}; resp.name = StringRef(App.get_name()); resp.friendly_name = StringRef(App.get_friendly_name()); @@ -1746,9 +1745,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { - state_subs_at_ = 0; -} +void APIConnection::subscribe_home_assistant_states() { state_subs_at_ = 0; } #endif bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { if (this->flags_.remove) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 40e4fd61c1..935393b2da 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -127,7 +127,7 @@ class APIConnection final : public APIServerConnection { #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; - void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; + void unsubscribe_bluetooth_le_advertisements() override; void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; @@ -136,7 +136,7 @@ class APIConnection final : public APIServerConnection { void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; - bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override; + bool send_subscribe_bluetooth_connections_free_response() override; void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; #endif @@ -187,8 +187,8 @@ class APIConnection final : public APIServerConnection { void update_command(const UpdateCommandRequest &msg) override; #endif - void on_disconnect_response(const DisconnectResponse &value) override; - void on_ping_response(const PingResponse &value) override { + void on_disconnect_response() override; + void on_ping_response() override { // we initiated ping this->flags_.sent_ping = false; } @@ -199,11 +199,11 @@ class APIConnection final : public APIServerConnection { void on_get_time_response(const GetTimeResponse &value) override; #endif bool send_hello_response(const HelloRequest &msg) override; - bool send_disconnect_response(const DisconnectRequest &msg) override; - bool send_ping_response(const PingRequest &msg) override; - bool send_device_info_response(const DeviceInfoRequest &msg) override; - void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } - void subscribe_states(const SubscribeStatesRequest &msg) override { + bool send_disconnect_response() override; + bool send_ping_response() override; + bool send_device_info_response() override; + void list_entities() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } + void subscribe_states() override { this->flags_.state_subscription = true; // Start initial state iterator only if no iterator is active // If list_entities is running, we'll start initial_state when it completes @@ -217,12 +217,10 @@ class APIConnection final : public APIServerConnection { App.schedule_dump_config(); } #ifdef USE_API_HOMEASSISTANT_SERVICES - void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { - this->flags_.service_call_subscription = true; - } + void subscribe_homeassistant_services() override { this->flags_.service_call_subscription = true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES - void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; + void subscribe_home_assistant_states() override; #endif #ifdef USE_API_USER_DEFINED_ACTIONS void execute_service(const ExecuteServiceRequest &msg) override; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index af0a2d0ca2..df66b6eb83 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -15,6 +15,9 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name, const DumpBuffer dump_buf; ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf)); } +void APIServerConnectionBase::log_receive_message_(const LogString *name) { + ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name)); +} #endif void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { @@ -29,66 +32,52 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } case DisconnectRequest::MESSAGE_TYPE: { - DisconnectRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_disconnect_request"), msg); + this->log_receive_message_(LOG_STR("on_disconnect_request")); #endif - this->on_disconnect_request(msg); + this->on_disconnect_request(); break; } case DisconnectResponse::MESSAGE_TYPE: { - DisconnectResponse msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_disconnect_response"), msg); + this->log_receive_message_(LOG_STR("on_disconnect_response")); #endif - this->on_disconnect_response(msg); + this->on_disconnect_response(); break; } case PingRequest::MESSAGE_TYPE: { - PingRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_ping_request"), msg); + this->log_receive_message_(LOG_STR("on_ping_request")); #endif - this->on_ping_request(msg); + this->on_ping_request(); break; } case PingResponse::MESSAGE_TYPE: { - PingResponse msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_ping_response"), msg); + this->log_receive_message_(LOG_STR("on_ping_response")); #endif - this->on_ping_response(msg); + this->on_ping_response(); break; } case DeviceInfoRequest::MESSAGE_TYPE: { - DeviceInfoRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_device_info_request"), msg); + this->log_receive_message_(LOG_STR("on_device_info_request")); #endif - this->on_device_info_request(msg); + this->on_device_info_request(); break; } case ListEntitiesRequest::MESSAGE_TYPE: { - ListEntitiesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_list_entities_request"), msg); + this->log_receive_message_(LOG_STR("on_list_entities_request")); #endif - this->on_list_entities_request(msg); + this->on_list_entities_request(); break; } case SubscribeStatesRequest::MESSAGE_TYPE: { - SubscribeStatesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_states_request")); #endif - this->on_subscribe_states_request(msg); + this->on_subscribe_states_request(); break; } case SubscribeLogsRequest::MESSAGE_TYPE: { @@ -146,12 +135,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #endif #ifdef USE_API_HOMEASSISTANT_SERVICES case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { - SubscribeHomeassistantServicesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request")); #endif - this->on_subscribe_homeassistant_services_request(msg); + this->on_subscribe_homeassistant_services_request(); break; } #endif @@ -166,12 +153,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #ifdef USE_API_HOMEASSISTANT_STATES case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { - SubscribeHomeAssistantStatesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request")); #endif - this->on_subscribe_home_assistant_states_request(msg); + this->on_subscribe_home_assistant_states_request(); break; } #endif @@ -375,23 +360,19 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #endif #ifdef USE_BLUETOOTH_PROXY case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { - SubscribeBluetoothConnectionsFreeRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request")); #endif - this->on_subscribe_bluetooth_connections_free_request(msg); + this->on_subscribe_bluetooth_connections_free_request(); break; } #endif #ifdef USE_BLUETOOTH_PROXY case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { - UnsubscribeBluetoothLEAdvertisementsRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg); + this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request")); #endif - this->on_unsubscribe_bluetooth_le_advertisements_request(msg); + this->on_unsubscribe_bluetooth_le_advertisements_request(); break; } #endif @@ -647,36 +628,29 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) { this->on_fatal_error(); } } -void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { - if (!this->send_disconnect_response(msg)) { +void APIServerConnection::on_disconnect_request() { + if (!this->send_disconnect_response()) { this->on_fatal_error(); } } -void APIServerConnection::on_ping_request(const PingRequest &msg) { - if (!this->send_ping_response(msg)) { +void APIServerConnection::on_ping_request() { + if (!this->send_ping_response()) { this->on_fatal_error(); } } -void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { - if (!this->send_device_info_response(msg)) { +void APIServerConnection::on_device_info_request() { + if (!this->send_device_info_response()) { this->on_fatal_error(); } } -void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); } -void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { - this->subscribe_states(msg); -} +void APIServerConnection::on_list_entities_request() { this->list_entities(); } +void APIServerConnection::on_subscribe_states_request() { this->subscribe_states(); } void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); } #ifdef USE_API_HOMEASSISTANT_SERVICES -void APIServerConnection::on_subscribe_homeassistant_services_request( - const SubscribeHomeassistantServicesRequest &msg) { - this->subscribe_homeassistant_services(msg); -} +void APIServerConnection::on_subscribe_homeassistant_services_request() { this->subscribe_homeassistant_services(); } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { - this->subscribe_home_assistant_states(msg); -} +void APIServerConnection::on_subscribe_home_assistant_states_request() { this->subscribe_home_assistant_states(); } #endif #ifdef USE_API_USER_DEFINED_ACTIONS void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } @@ -793,17 +767,15 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo } #endif #ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_subscribe_bluetooth_connections_free_request( - const SubscribeBluetoothConnectionsFreeRequest &msg) { - if (!this->send_subscribe_bluetooth_connections_free_response(msg)) { +void APIServerConnection::on_subscribe_bluetooth_connections_free_request() { + if (!this->send_subscribe_bluetooth_connections_free_response()) { this->on_fatal_error(); } } #endif #ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { - this->unsubscribe_bluetooth_le_advertisements(msg); +void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request() { + this->unsubscribe_bluetooth_le_advertisements(); } #endif #ifdef USE_BLUETOOTH_PROXY diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e2bc1609ed..b8c9e4da6f 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -14,6 +14,7 @@ class APIServerConnectionBase : public ProtoService { protected: void log_send_message_(const char *name, const char *dump); void log_receive_message_(const LogString *name, const ProtoMessage &msg); + void log_receive_message_(const LogString *name); public: #endif @@ -28,15 +29,15 @@ class APIServerConnectionBase : public ProtoService { virtual void on_hello_request(const HelloRequest &value){}; - virtual void on_disconnect_request(const DisconnectRequest &value){}; - virtual void on_disconnect_response(const DisconnectResponse &value){}; - virtual void on_ping_request(const PingRequest &value){}; - virtual void on_ping_response(const PingResponse &value){}; - virtual void on_device_info_request(const DeviceInfoRequest &value){}; + virtual void on_disconnect_request(){}; + virtual void on_disconnect_response(){}; + virtual void on_ping_request(){}; + virtual void on_ping_response(){}; + virtual void on_device_info_request(){}; - virtual void on_list_entities_request(const ListEntitiesRequest &value){}; + virtual void on_list_entities_request(){}; - virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){}; + virtual void on_subscribe_states_request(){}; #ifdef USE_COVER virtual void on_cover_command_request(const CoverCommandRequest &value){}; @@ -61,14 +62,14 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; + virtual void on_subscribe_homeassistant_services_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; + virtual void on_subscribe_home_assistant_states_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES @@ -147,12 +148,11 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){}; + virtual void on_subscribe_bluetooth_connections_free_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &value){}; + virtual void on_unsubscribe_bluetooth_le_advertisements_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY @@ -231,17 +231,17 @@ class APIServerConnectionBase : public ProtoService { class APIServerConnection : public APIServerConnectionBase { public: virtual bool send_hello_response(const HelloRequest &msg) = 0; - virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; - virtual bool send_ping_response(const PingRequest &msg) = 0; - virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; - virtual void list_entities(const ListEntitiesRequest &msg) = 0; - virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; + virtual bool send_disconnect_response() = 0; + virtual bool send_ping_response() = 0; + virtual bool send_device_info_response() = 0; + virtual void list_entities() = 0; + virtual void subscribe_states() = 0; virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; + virtual void subscribe_homeassistant_services() = 0; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; + virtual void subscribe_home_assistant_states() = 0; #endif #ifdef USE_API_USER_DEFINED_ACTIONS virtual void execute_service(const ExecuteServiceRequest &msg) = 0; @@ -331,11 +331,10 @@ class APIServerConnection : public APIServerConnectionBase { virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; #endif #ifdef USE_BLUETOOTH_PROXY - virtual bool send_subscribe_bluetooth_connections_free_response( - const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; + virtual bool send_subscribe_bluetooth_connections_free_response() = 0; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0; + virtual void unsubscribe_bluetooth_le_advertisements() = 0; #endif #ifdef USE_BLUETOOTH_PROXY virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0; @@ -363,17 +362,17 @@ class APIServerConnection : public APIServerConnectionBase { #endif protected: void on_hello_request(const HelloRequest &msg) override; - void on_disconnect_request(const DisconnectRequest &msg) override; - void on_ping_request(const PingRequest &msg) override; - void on_device_info_request(const DeviceInfoRequest &msg) override; - void on_list_entities_request(const ListEntitiesRequest &msg) override; - void on_subscribe_states_request(const SubscribeStatesRequest &msg) override; + void on_disconnect_request() override; + void on_ping_request() override; + void on_device_info_request() override; + void on_list_entities_request() override; + void on_subscribe_states_request() override; void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; #ifdef USE_API_HOMEASSISTANT_SERVICES - void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; + void on_subscribe_homeassistant_services_request() override; #endif #ifdef USE_API_HOMEASSISTANT_STATES - void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; + void on_subscribe_home_assistant_states_request() override; #endif #ifdef USE_API_USER_DEFINED_ACTIONS void on_execute_service_request(const ExecuteServiceRequest &msg) override; @@ -463,11 +462,10 @@ class APIServerConnection : public APIServerConnectionBase { void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; #endif #ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &msg) override; + void on_subscribe_bluetooth_connections_free_request() override; #endif #ifdef USE_BLUETOOTH_PROXY - void on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; + void on_unsubscribe_bluetooth_le_advertisements_request() override; #endif #ifdef USE_BLUETOOTH_PROXY void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4021a062ca..5fbc1137a8 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2270,10 +2270,13 @@ SOURCE_NAMES = { SOURCE_CLIENT: "SOURCE_CLIENT", } -RECEIVE_CASES: dict[int, tuple[str, str | None]] = {} +RECEIVE_CASES: dict[int, tuple[str, str | None, str]] = {} ifdefs: dict[str, str] = {} +# Track messages with no fields (empty messages) for parameter elision +EMPTY_MESSAGES: set[str] = set() + def get_opt( desc: descriptor.DescriptorProto, @@ -2504,26 +2507,26 @@ def build_service_message_type( # Only add ifdef when we're actually generating content if ifdef is not None: hout += f"#ifdef {ifdef}\n" - # Generate receive + # Generate receive handler and switch case func = f"on_{snake}" - hout += f"virtual void {func}(const {mt.name} &value){{}};\n" - case = "" - case += f"{mt.name} msg;\n" - # Check if this message has any fields (excluding deprecated ones) has_fields = any(not field.options.deprecated for field in mt.field) - if has_fields: - # Normal case: decode the message + is_empty = not has_fields + if is_empty: + EMPTY_MESSAGES.add(mt.name) + hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" + case = "" + if not is_empty: + case += f"{mt.name} msg;\n" case += "msg.decode(msg_data, msg_size);\n" - else: - # Empty message optimization: skip decode since there are no fields - case += "// Empty message: no decode needed\n" if log: case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n' + if is_empty: + case += f'this->log_receive_message_(LOG_STR("{func}"));\n' + else: + case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n' case += "#endif\n" - case += f"this->{func}(msg);\n" + case += f"this->{func}({'msg' if not is_empty else ''});\n" case += "break;" - # Store the message name and ifdef with the case for later use RECEIVE_CASES[id_] = (case, ifdef, mt.name) # Only close ifdef if we opened it @@ -2839,6 +2842,7 @@ static const char *const TAG = "api.service"; hpp += ( " void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n" ) + hpp += " void log_receive_message_(const LogString *name);\n" hpp += " public:\n" hpp += "#endif\n\n" @@ -2862,6 +2866,9 @@ static const char *const TAG = "api.service"; cpp += " DumpBuffer dump_buf;\n" cpp += ' ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));\n' cpp += "}\n" + cpp += f"void {class_name}::log_receive_message_(const LogString *name) {{\n" + cpp += ' ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name));\n' + cpp += "}\n" cpp += "#endif\n\n" for mt in file.message_type: @@ -2929,22 +2936,22 @@ static const char *const TAG = "api.service"; hpp_protected += f"#ifdef {ifdef}\n" cpp += f"#ifdef {ifdef}\n" - hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" + is_empty = inp in EMPTY_MESSAGES + param = "" if is_empty else f"const {inp} &msg" + arg = "" if is_empty else "msg" - # For non-void methods, generate a send_ method instead of return-by-value + hpp_protected += f" void {on_func}({param}) override;\n" if is_void: - hpp += f" virtual void {func}(const {inp} &msg) = 0;\n" + hpp += f" virtual void {func}({param}) = 0;\n" else: - hpp += f" virtual bool send_{func}_response(const {inp} &msg) = 0;\n" + hpp += f" virtual bool send_{func}_response({param}) = 0;\n" - cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" - - # No authentication check here - it's done in read_message + cpp += f"void {class_name}::{on_func}({param}) {{\n" body = "" if is_void: - body += f"this->{func}(msg);\n" + body += f"this->{func}({arg});\n" else: - body += f"if (!this->send_{func}_response(msg)) {{\n" + body += f"if (!this->send_{func}_response({arg})) {{\n" body += " this->on_fatal_error();\n" body += "}\n" From eb6a6f8d0d6a408138d0dd2fd2143a8af9b2ee8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:05 -0600 Subject: [PATCH 85/95] [web_server_idf] Remove unused host() method (#13869) --- esphome/components/web_server_idf/web_server_idf.cpp | 2 -- esphome/components/web_server_idf/web_server_idf.h | 1 - 2 files changed, 3 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 9860810452..39e6b7a790 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -258,8 +258,6 @@ StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) co return StringRef(buffer.data(), decoded_len); } -std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } - void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 817f47da79..1760544963 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -121,7 +121,6 @@ class AsyncWebServerRequest { char buffer[URL_BUF_SIZE]; return std::string(this->url_to(buffer)); } - std::string host() const; // NOLINTNEXTLINE(readability-identifier-naming) size_t contentLength() const { return this->req_->content_len; } From 6ee185c58aa047dde7e933c4d5a1b47c42f6572a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:23 -0600 Subject: [PATCH 86/95] [dashboard] Use resolve/relative_to for download path validation (#13867) --- esphome/dashboard/web_server.py | 17 ++++-- tests/dashboard/test_web_server.py | 93 +++++++++++++++++++++++++----- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index da50279864..00974bf460 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1054,17 +1054,26 @@ class DownloadBinaryRequestHandler(BaseHandler): # fallback to type=, but prioritize file= file_name = self.get_argument("type", None) file_name = self.get_argument("file", file_name) - if file_name is None: + if file_name is None or not file_name.strip(): self.send_error(400) return - file_name = file_name.replace("..", "").lstrip("/") # get requested download name, or build it based on filename download_name = self.get_argument( "download", f"{storage_json.name}-{file_name}", ) - path = storage_json.firmware_bin_path.parent.joinpath(file_name) + if storage_json.firmware_bin_path is None: + self.send_error(404) + return + + base_dir = storage_json.firmware_bin_path.parent.resolve() + path = base_dir.joinpath(file_name).resolve() + try: + path.relative_to(base_dir) + except ValueError: + self.send_error(403) + return if not path.is_file(): args = ["esphome", "idedata", settings.rel_path(configuration)] @@ -1078,7 +1087,7 @@ class DownloadBinaryRequestHandler(BaseHandler): found = False for image in idedata.extra_flash_images: - if image.path.endswith(file_name): + if image.path.as_posix().endswith(file_name): path = image.path download_name = file_name found = True diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 7642876ee5..9ea7a5164b 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -8,6 +8,7 @@ import gzip import json import os from pathlib import Path +import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -421,7 +422,7 @@ async def test_download_binary_handler_idedata_fallback( # Mock idedata response mock_image = Mock() - mock_image.path = str(bootloader_file) + mock_image.path = bootloader_file mock_idedata_instance = Mock() mock_idedata_instance.extra_flash_images = [mock_image] mock_idedata.return_value = mock_idedata_instance @@ -528,14 +529,22 @@ async def test_download_binary_handler_subdirectory_file_url_encoded( @pytest.mark.asyncio @pytest.mark.usefixtures("mock_ext_storage_path") @pytest.mark.parametrize( - "attack_path", + ("attack_path", "expected_code"), [ - pytest.param("../../../secrets.yaml", id="basic_traversal"), - pytest.param("..%2F..%2F..%2Fsecrets.yaml", id="url_encoded"), - pytest.param("zephyr/../../../secrets.yaml", id="traversal_with_prefix"), - pytest.param("/etc/passwd", id="absolute_path"), - pytest.param("//etc/passwd", id="double_slash_absolute"), - pytest.param("....//secrets.yaml", id="multiple_dots"), + pytest.param("../../../secrets.yaml", 403, id="basic_traversal"), + pytest.param("..%2F..%2F..%2Fsecrets.yaml", 403, id="url_encoded"), + pytest.param("zephyr/../../../secrets.yaml", 403, id="traversal_with_prefix"), + pytest.param("/etc/passwd", 403, id="absolute_path"), + pytest.param("//etc/passwd", 403, id="double_slash_absolute"), + pytest.param( + "....//secrets.yaml", + # On Windows, Path.resolve() treats "..." and "...." as parent + # traversal (like ".."), so the path escapes base_dir -> 403. + # On Unix, "...." is a literal directory name that stays inside + # base_dir but doesn't exist -> 404. + 403 if sys.platform == "win32" else 404, + id="multiple_dots", + ), ], ) async def test_download_binary_handler_path_traversal_protection( @@ -543,11 +552,14 @@ async def test_download_binary_handler_path_traversal_protection( tmp_path: Path, mock_storage_json: MagicMock, attack_path: str, + expected_code: int, ) -> None: """Test that DownloadBinaryRequestHandler prevents path traversal attacks. - Verifies that attempts to use '..' in file paths are sanitized to prevent - accessing files outside the build directory. Tests multiple attack vectors. + Verifies that attempts to escape the build directory via '..' are rejected + using resolve()/relative_to() validation. Tests multiple attack vectors. + Real traversals that escape the base directory get 403. Paths like '....' + that resolve inside the base directory but don't exist get 404. """ # Create build structure build_dir = get_build_path(tmp_path, "test") @@ -565,14 +577,67 @@ async def test_download_binary_handler_path_traversal_protection( mock_storage.firmware_bin_path = firmware_file mock_storage_json.load.return_value = mock_storage - # Attempt path traversal attack - should be blocked - with pytest.raises(HTTPClientError) as exc_info: + # Mock async_run_system_command so paths that pass validation but don't exist + # return 404 deterministically without spawning a real subprocess. + with ( + patch( + "esphome.dashboard.web_server.async_run_system_command", + new_callable=AsyncMock, + return_value=(2, "", ""), + ), + pytest.raises(HTTPClientError) as exc_info, + ): await dashboard.fetch( f"/download.bin?configuration=test.yaml&file={attack_path}", method="GET", ) - # Should get 404 (file not found after sanitization) or 500 (idedata fails) - assert exc_info.value.code in (404, 500) + assert exc_info.value.code == expected_code + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_no_firmware_bin_path( + dashboard: DashboardTestHelper, + mock_storage_json: MagicMock, +) -> None: + """Test that download returns 404 when firmware_bin_path is None. + + This covers configs created by StorageJSON.from_wizard() where no + firmware has been compiled yet. + """ + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = None + mock_storage_json.load.return_value = mock_storage + + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=firmware.bin", + method="GET", + ) + assert exc_info.value.code == 404 + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +@pytest.mark.parametrize("file_value", ["", "%20%20", "%20"]) +async def test_download_binary_handler_empty_file_name( + dashboard: DashboardTestHelper, + mock_storage_json: MagicMock, + file_value: str, +) -> None: + """Test that download returns 400 for empty or whitespace-only file names.""" + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = Path("/fake/firmware.bin") + mock_storage_json.load.return_value = mock_storage + + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + f"/download.bin?configuration=test.yaml&file={file_value}", + method="GET", + ) + assert exc_info.value.code == 400 @pytest.mark.asyncio From 5370687001745b2dfd590426eb3008158469a56b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:41 -0600 Subject: [PATCH 87/95] [wizard] Use secrets module for fallback AP password generation (#13864) --- esphome/wizard.py | 3 +-- tests/unit_tests/test_wizard.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/esphome/wizard.py b/esphome/wizard.py index 4b74847996..f83342cc6a 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,6 +1,5 @@ import base64 from pathlib import Path -import random import secrets import string from typing import Literal, NotRequired, TypedDict, Unpack @@ -130,7 +129,7 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: if len(ap_name) > 32: ap_name = ap_name_base kwargs["fallback_name"] = ap_name - kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) + kwargs["fallback_psk"] = "".join(secrets.choice(letters) for _ in range(12)) base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index eb44c1c20f..0ce89230d8 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Any -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest from pytest import MonkeyPatch @@ -632,3 +632,14 @@ def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch): # rpipico doesn't support WiFi, so no api_encryption_key or ota_password assert "api_encryption_key" not in call_kwargs assert "ota_password" not in call_kwargs + + +def test_fallback_psk_uses_secrets_choice( + default_config: dict[str, Any], +) -> None: + """Test that fallback PSK is generated using secrets.choice.""" + with patch("esphome.wizard.secrets.choice", return_value="X") as mock_choice: + config = wz.wizard_file(**default_config) + + assert 'password: "XXXXXXXXXXXX"' in config + assert mock_choice.call_count == 12 From e24528c842f6cbab9b71a07d6d738006fd5dd509 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:59 -0600 Subject: [PATCH 88/95] [analyze-memory] Attribute CSWTCH symbols from SDK archives (#13850) --- esphome/analyze_memory/__init__.py | 156 +++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 42 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index d8c941e76f..d8abc8bafb 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -397,47 +397,38 @@ class MemoryAnalyzer: return pioenvs_dir return None - def _scan_cswtch_in_objects( - self, obj_dir: Path - ) -> dict[str, list[tuple[str, int]]]: - """Scan object files for CSWTCH symbols using a single nm invocation. + @staticmethod + def _parse_nm_cswtch_output( + output: str, + base_dir: Path | None, + cswtch_map: dict[str, list[tuple[str, int]]], + ) -> None: + """Parse nm output for CSWTCH symbols and add to cswtch_map. - Uses ``nm --print-file-name -S`` on all ``.o`` files at once. - Output format: ``/path/to/file.o:address size type name`` + Handles both ``.o`` files and ``.a`` archives. + + nm output formats:: + + .o files: /path/file.o:hex_addr hex_size type name + .a files: /path/lib.a:member.o:hex_addr hex_size type name + + For ``.o`` files, paths are made relative to *base_dir* when possible. + For ``.a`` archives (detected by ``:`` in the file portion), paths are + formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``). Args: - obj_dir: Directory containing object files (.pioenvs/<env>/) - - Returns: - Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples. + output: Raw stdout from ``nm --print-file-name -S``. + base_dir: Base directory for computing relative paths of ``.o`` files. + Pass ``None`` when scanning archives outside the build tree. + cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list. """ - cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) - - if not self.nm_path: - return cswtch_map - - # Find all .o files recursively, sorted for deterministic output - obj_files = sorted(obj_dir.rglob("*.o")) - if not obj_files: - return cswtch_map - - _LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files)) - - # Single nm call with --print-file-name for all object files - result = run_tool( - [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files], - timeout=30, - ) - if result is None or result.returncode != 0: - return cswtch_map - - for line in result.stdout.splitlines(): + for line in output.splitlines(): if "CSWTCH$" not in line: continue # Split on last ":" that precedes a hex address. - # nm --print-file-name format: filepath:hex_addr hex_size type name - # We split from the right: find the last colon followed by hex digits. + # For .o: "filepath.o" : "hex_addr hex_size type name" + # For .a: "filepath.a:member.o" : "hex_addr hex_size type name" parts_after_colon = line.rsplit(":", 1) if len(parts_after_colon) != 2: continue @@ -457,16 +448,89 @@ class MemoryAnalyzer: except ValueError: continue - # Get relative path from obj_dir for readability - try: - rel_path = str(Path(file_path).relative_to(obj_dir)) - except ValueError: + # Determine readable source path + # Use ".a:" to detect archive format (not bare ":" which matches + # Windows drive letters like "C:\...\file.o"). + if ".a:" in file_path: + # Archive format: "archive.a:member.o" → "archive_stem/member.o" + archive_part, member = file_path.rsplit(":", 1) + archive_name = Path(archive_part).stem + rel_path = f"{archive_name}/{member}" + elif base_dir is not None: + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + rel_path = file_path + else: rel_path = file_path key = f"{sym_name}:{size}" cswtch_map[key].append((rel_path, size)) - return cswtch_map + def _run_nm_cswtch_scan( + self, + files: list[Path], + base_dir: Path | None, + cswtch_map: dict[str, list[tuple[str, int]]], + ) -> None: + """Run nm on *files* and add any CSWTCH symbols to *cswtch_map*. + + Args: + files: Object (``.o``) or archive (``.a``) files to scan. + base_dir: Base directory for relative path computation (see + :meth:`_parse_nm_cswtch_output`). + cswtch_map: Dict to populate with results. + """ + if not self.nm_path or not files: + return + + _LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files)) + + result = run_tool( + [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files], + timeout=30, + ) + if result is None or result.returncode != 0: + _LOGGER.debug( + "nm failed or timed out scanning %d files for CSWTCH symbols", + len(files), + ) + return + + self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map) + + def _scan_cswtch_in_sdk_archives( + self, cswtch_map: dict[str, list[tuple[str, int]]] + ) -> None: + """Scan SDK library archives (.a) for CSWTCH symbols. + + Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source, + so their CSWTCH symbols only exist inside ``.a`` archives. Results are + merged into *cswtch_map* for keys not already found in ``.o`` files. + + The same source file (e.g. ``lwip-esp.o``) often appears in multiple + library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.), + so results are deduplicated by member name. + """ + sdk_dirs = self._find_sdk_library_dirs() + if not sdk_dirs: + return + + sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a")) + + sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + self._run_nm_cswtch_scan(sdk_archives, None, sdk_map) + + # Merge SDK results, deduplicating by member name. + for key, sources in sdk_map.items(): + if key in cswtch_map: + continue + seen: dict[str, tuple[str, int]] = {} + for path, sz in sources: + member = Path(path).name + if member not in seen: + seen[member] = (path, sz) + cswtch_map[key] = list(seen.values()) def _source_file_to_component(self, source_file: str) -> str: """Map a source object file path to its component name. @@ -505,17 +569,25 @@ class MemoryAnalyzer: CSWTCH symbols are compiler-generated lookup tables for switch statements. They are local symbols, so the same name can appear in different object files. - This method scans .o files to attribute them to their source components. + This method scans .o files and SDK archives to attribute them to their + source components. """ obj_dir = self._find_object_files_dir() if obj_dir is None: _LOGGER.debug("No object files directory found, skipping CSWTCH analysis") return - # Scan object files for CSWTCH symbols - cswtch_map = self._scan_cswtch_in_objects(obj_dir) + # Scan build-dir object files for CSWTCH symbols + cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map) + + # Also scan SDK library archives (.a) for CSWTCH symbols. + # Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source + # so their symbols only exist inside .a archives, not as loose .o files. + self._scan_cswtch_in_sdk_archives(cswtch_map) + if not cswtch_map: - _LOGGER.debug("No CSWTCH symbols found in object files") + _LOGGER.debug("No CSWTCH symbols found in object files or SDK archives") return # Collect CSWTCH symbols from the ELF (already parsed in sections) From 46f8302d8ff4aa8acf9751d0df948f86e4b61aa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:26:15 -0600 Subject: [PATCH 89/95] [mqtt] Use stack buffer for discovery topic to avoid heap allocation (#13812) --- esphome/components/mqtt/mqtt_component.cpp | 21 +++++++++++---------- esphome/components/mqtt/mqtt_component.h | 9 +++++++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index e4b80de50a..8cf393e2df 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -34,10 +34,7 @@ inline char *append_char(char *p, char c) { // MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h. // ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h. // This ensures the stack buffers below are always large enough. -static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) -// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null -static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + - ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; +// MQTT_DISCOVERY_PREFIX_MAX_LEN and MQTT_DISCOVERY_TOPIC_MAX_LEN are defined in mqtt_component.h // Function implementation of LOG_MQTT_COMPONENT macro to reduce code size void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) { @@ -54,15 +51,15 @@ void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } -std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { +StringRef MQTTComponent::get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf, + const MQTTDiscoveryInfo &discovery_info) const { char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1]; str_sanitize_to(sanitized_name, App.get_name().c_str()); const char *comp_type = this->component_type(); char object_id_buf[OBJECT_ID_MAX_LEN]; StringRef object_id = this->get_default_object_id_to_(object_id_buf); - char buf[DISCOVERY_TOPIC_MAX_LEN]; - char *p = buf; + char *p = buf.data(); p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size()); p = append_char(p, '/'); @@ -72,8 +69,9 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove p = append_char(p, '/'); p = append_str(p, object_id.c_str(), object_id.size()); p = append_str(p, "/config", 7); + *p = '\0'; - return std::string(buf, p - buf); + return StringRef(buf.data(), p - buf.data()); } StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix, @@ -182,16 +180,19 @@ bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); + char discovery_topic_buf[MQTT_DISCOVERY_TOPIC_MAX_LEN]; + StringRef discovery_topic = this->get_discovery_topic_to_(discovery_topic_buf, discovery_info); + if (discovery_info.clean) { ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str()); - return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); + return global_mqtt_client->publish(discovery_topic.c_str(), "", 0, this->qos_, true); } ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( - this->get_discovery_topic_(discovery_info), + discovery_topic.c_str(), [this](JsonObject root) { SendDiscoveryConfig config; config.state_topic = true; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2cec6fda7e..76375fb106 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -32,6 +32,10 @@ static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: // Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN = MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1; +static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) +// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null +static constexpr size_t MQTT_DISCOVERY_TOPIC_MAX_LEN = MQTT_DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + + 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; class MQTTComponent; // Forward declaration void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic); @@ -263,8 +267,9 @@ class MQTTComponent : public Component { void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0); protected: - /// Helper method to get the discovery topic for this component. - std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const; + /// Helper method to get the discovery topic for this component into a buffer. + StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf, + const MQTTDiscoveryInfo &discovery_info) const; /** Get this components state/command/... topic into a buffer. * From c3c0c40524105ad3165581b2c8241dc1990201cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:26:29 -0600 Subject: [PATCH 90/95] [mqtt] Return friendly_name_() by const reference to avoid string copies (#13810) --- esphome/components/mqtt/mqtt_component.cpp | 6 +++--- esphome/components/mqtt/mqtt_component.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 8cf393e2df..09570106df 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -205,7 +205,7 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : ""; + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : StringRef(); if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; @@ -249,7 +249,7 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32, - fnv1_hash(this->friendly_name_())); + fnv1_hash(this->friendly_name_().c_str())); // Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678") // MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43 char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11]; @@ -415,7 +415,7 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden -std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const { return this->get_entity()->get_object_id_to(buf); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 76375fb106..eb98158647 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -293,7 +293,7 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - std::string friendly_name_() const; + const StringRef &friendly_name_() const; /// Get the icon field of this component as StringRef StringRef get_icon_ref_() const; From 422f413680690b257429a4c4f8b51a700bee40e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:26:44 -0600 Subject: [PATCH 91/95] [lps22] Replace set_retry with set_interval to avoid heap allocation (#13841) --- esphome/components/lps22/lps22.cpp | 14 ++++++++++---- esphome/components/lps22/lps22.h | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp index 526286ba72..7fc5774b08 100644 --- a/esphome/components/lps22/lps22.cpp +++ b/esphome/components/lps22/lps22.cpp @@ -38,22 +38,29 @@ void LPS22Component::dump_config() { LOG_UPDATE_INTERVAL(this); } +static constexpr uint32_t INTERVAL_READ = 0; + void LPS22Component::update() { uint8_t value = 0x00; this->read_register(CTRL_REG2, &value, 1); value |= CTRL_REG2_ONE_SHOT_MASK; this->write_register(CTRL_REG2, &value, 1); - this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); }); + this->read_attempts_remaining_ = READ_ATTEMPTS; + this->set_interval(INTERVAL_READ, READ_INTERVAL, [this]() { this->try_read_(); }); } -RetryResult LPS22Component::try_read_() { +void LPS22Component::try_read_() { uint8_t value = 0x00; this->read_register(STATUS, &value, 1); const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK; if ((value & expected_status_mask) != expected_status_mask) { ESP_LOGD(TAG, "STATUS not ready: %x", value); - return RetryResult::RETRY; + if (--this->read_attempts_remaining_ == 0) { + this->cancel_interval(INTERVAL_READ); + } + return; } + this->cancel_interval(INTERVAL_READ); if (this->temperature_sensor_ != nullptr) { uint8_t t_buf[2]{0}; @@ -68,7 +75,6 @@ RetryResult LPS22Component::try_read_() { uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]); this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb)); } - return RetryResult::DONE; } } // namespace lps22 diff --git a/esphome/components/lps22/lps22.h b/esphome/components/lps22/lps22.h index 549ea524ea..95ee4ad442 100644 --- a/esphome/components/lps22/lps22.h +++ b/esphome/components/lps22/lps22.h @@ -17,10 +17,11 @@ class LPS22Component : public sensor::Sensor, public PollingComponent, public i2 void dump_config() override; protected: + void try_read_(); + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; - - RetryResult try_read_(); + uint8_t read_attempts_remaining_{0}; }; } // namespace lps22 From bed01da345c6c652ef464d800aa2c8152c9681a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:45:40 -0600 Subject: [PATCH 92/95] [api] Guard varint parsing against overlong encodings (#13870) --- esphome/components/api/proto.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 92978f765f..41ea0043f9 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -112,8 +112,12 @@ class ProtoVarInt { uint64_t result = buffer[0] & 0x7F; uint8_t bitpos = 7; + // A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings + // to avoid undefined behavior from shifting uint64_t by >= 64 bits. + uint32_t max_len = std::min(len, uint32_t(10)); + // Start from the second byte since we've already processed the first - for (uint32_t i = 1; i < len; i++) { + for (uint32_t i = 1; i < max_len; i++) { uint8_t val = buffer[i]; result |= uint64_t(val & 0x7F) << uint64_t(bitpos); bitpos += 7; From fb9328372028c941d7a1c1bc446efbf157c835d0 Mon Sep 17 00:00:00 2001 From: tronikos <tronikos@users.noreply.github.com> Date: Mon, 9 Feb 2026 01:52:49 -0800 Subject: [PATCH 93/95] [water_heater] Add state masking to distinguish explicit commands from no-change (#13879) --- .../components/water_heater/water_heater.cpp | 18 ++++++++++++------ esphome/components/water_heater/water_heater.h | 4 ++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index e6d1562352..9d7ae0cbc0 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -65,6 +65,7 @@ WaterHeaterCall &WaterHeaterCall::set_away(bool away) { } else { this->state_ &= ~WATER_HEATER_STATE_AWAY; } + this->state_mask_ |= WATER_HEATER_STATE_AWAY; return *this; } @@ -74,6 +75,7 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) { } else { this->state_ &= ~WATER_HEATER_STATE_ON; } + this->state_mask_ |= WATER_HEATER_STATE_ON; return *this; } @@ -92,11 +94,11 @@ void WaterHeaterCall::perform() { if (!std::isnan(this->target_temperature_high_)) { ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); } - if (this->state_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: YES"); + if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { + ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); } - if (this->state_ & WATER_HEATER_STATE_ON) { - ESP_LOGD(TAG, " On: YES"); + if (this->state_mask_ & WATER_HEATER_STATE_ON) { + ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } this->parent_->control(*this); } @@ -137,13 +139,17 @@ void WaterHeaterCall::validate_() { this->target_temperature_high_ = NAN; } } - if ((this->state_ & WATER_HEATER_STATE_AWAY) && !traits.get_supports_away_mode()) { - ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str()); + if (!traits.get_supports_away_mode()) { + if (this->state_ & WATER_HEATER_STATE_AWAY) { + ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str()); + } this->state_ &= ~WATER_HEATER_STATE_AWAY; + this->state_mask_ &= ~WATER_HEATER_STATE_AWAY; } // If ON/OFF not supported, device is always on - clear the flag silently if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { this->state_ &= ~WATER_HEATER_STATE_ON; + this->state_mask_ &= ~WATER_HEATER_STATE_ON; } } diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 7bd05ba7f5..93fcf5f401 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -91,6 +91,8 @@ class WaterHeaterCall { float get_target_temperature_high() const { return this->target_temperature_high_; } /// Get state flags value uint32_t get_state() const { return this->state_; } + /// Get mask of state flags that are being changed + uint32_t get_state_mask() const { return this->state_mask_; } protected: void validate_(); @@ -100,6 +102,7 @@ class WaterHeaterCall { float target_temperature_low_{NAN}; float target_temperature_high_{NAN}; uint32_t state_{0}; + uint32_t state_mask_{0}; }; struct WaterHeaterCallInternal : public WaterHeaterCall { @@ -111,6 +114,7 @@ struct WaterHeaterCallInternal : public WaterHeaterCall { this->target_temperature_low_ = restore.target_temperature_low_; this->target_temperature_high_ = restore.target_temperature_high_; this->state_ = restore.state_; + this->state_mask_ = restore.state_mask_; return *this; } }; From 790ac620ab10a228ac54ae0caedf7669650ff9e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 06:42:12 -0600 Subject: [PATCH 94/95] [web_server_idf] Use C++17 nested namespace style (#13856) --- esphome/components/web_server_idf/multipart.cpp | 6 ++---- esphome/components/web_server_idf/multipart.h | 6 ++---- esphome/components/web_server_idf/utils.cpp | 6 ++---- esphome/components/web_server_idf/utils.h | 6 ++---- esphome/components/web_server_idf/web_server_idf.cpp | 6 ++---- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 2092a41a8e..52dafeb997 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -6,8 +6,7 @@ #include <cstring> #include "multipart_parser.h" -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { static const char *const TAG = "multipart"; @@ -249,6 +248,5 @@ std::string str_trim(const std::string &str) { return str.substr(start, end - start + 1); } -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index 8fbe90c4a0..9008be6459 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -10,8 +10,7 @@ #include <string> #include <utility> -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { // Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads class MultipartReader { @@ -81,6 +80,5 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st // Trim whitespace from both ends of a string std::string str_trim(const std::string &str); -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index d3c3c3dc55..a1a2793b4a 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -8,8 +8,7 @@ #include "utils.h" -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { static const char *const TAG = "web_server_idf_utils"; @@ -119,6 +118,5 @@ const char *stristr(const char *haystack, const char *needle) { return nullptr; } -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index ae58f82398..bb0610dac0 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -5,8 +5,7 @@ #include <string> #include "esphome/core/helpers.h" -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { /// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space) /// Returns the new length of the decoded string @@ -29,6 +28,5 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n); // Case-insensitive string search (like strstr but case-insensitive) const char *stristr(const char *haystack, const char *needle); -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 39e6b7a790..ac7f99bef7 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -30,8 +30,7 @@ #include <cerrno> #include <sys/socket.h> -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { #ifndef HTTPD_409 #define HTTPD_409 "409 Conflict" @@ -895,7 +894,6 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } #endif // USE_WEBSERVER_OTA -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // !defined(USE_ESP32) From 22c77866d89da64ab96c830c716225d700b33906 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 06:42:26 -0600 Subject: [PATCH 95/95] [e131] Remove unnecessary heap allocation from packet receive loop (#13852) --- esphome/components/e131/e131.cpp | 7 ++----- esphome/components/e131/e131.h | 2 +- esphome/components/e131/e131_packet.cpp | 6 +++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index f11e7f4fe3..4187857901 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -55,7 +55,6 @@ void E131Component::setup() { } void E131Component::loop() { - std::vector<uint8_t> payload; E131Packet packet; int universe = 0; uint8_t buf[1460]; @@ -64,11 +63,9 @@ void E131Component::loop() { if (len == -1) { return; } - payload.resize(len); - memmove(&payload[0], buf, len); - if (!this->packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); return; } diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 831138a545..d4b272eae2 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -38,7 +38,7 @@ class E131Component : public esphome::Component { void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } protected: - bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet); + bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); void join_(int universe); diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index e663a3d0fc..ed081e5758 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -116,11 +116,11 @@ void E131Component::leave_(int universe) { ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); } -bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) { - if (data.size() < E131_MIN_PACKET_SIZE) +bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet) { + if (len < E131_MIN_PACKET_SIZE) return false; - auto *sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]); + auto *sbuff = reinterpret_cast<const E131RawPacket *>(data); if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) return false;