From ed8c062d9fc688e97758e702fb743d966334db5b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:53:02 -0400 Subject: [PATCH 01/15] [esp32_touch] Fix initial state never published when sensor untouched (#15032) --- esphome/components/esp32_touch/esp32_touch.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e7124ce92f..0d331b29d6 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() { } // Publish initial OFF state for sensors that haven't received events yet + bool all_initial_published = true; for (auto *child : this->children_) { this->publish_initial_state_if_needed_(child, now); + if (!child->initial_state_published_) { + all_initial_published = false; + } } - if (!this->setup_mode_) { + // Only disable loop once all initial states are published + if (!this->setup_mode_ && all_initial_published) { this->disable_loop(); } } From 0b01f9fc42aa7e848dfbab78500d8ba97778dc29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:57:51 -1000 Subject: [PATCH 02/15] [web_server] Increase httpd task stack size to prevent stack overflow (#14997) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/web_server_idf/web_server_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 60816fc6dd..fb0c17c854 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -121,7 +121,10 @@ void AsyncWebServer::begin() { if (this->server_) { this->end(); } + // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's + // 640-byte stack buffer used by web_server JSON request handlers. httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = config.stack_size + 256; config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Always enable LRU purging to handle socket exhaustion gracefully. From 8fa2e75afaacb55d6b8aaa3ca49f746789ab8b83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:58:02 -1000 Subject: [PATCH 03/15] [core] Add copy() method to StringRef for std::string compatibility (#15028) --- esphome/core/string_ref.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 6047202753..34ba2474b2 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -76,6 +76,15 @@ class StringRef { constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + /// Copy characters to destination buffer (std::string::copy-like, but returns 0 instead of throwing on out-of-range) + size_type copy(char *dest, size_type count, size_type pos = 0) const { + if (pos >= len_) + return 0; + size_type actual = (count > len_ - pos) ? len_ - pos : count; + std::memcpy(dest, base_ + pos, actual); + return actual; + } + std::string str() const { return std::string(base_, len_); } const uint8_t *byte() const { return reinterpret_cast(base_); } From a9a8f4cb3bf9f304abcd872995dc91aa988859ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:58:14 -1000 Subject: [PATCH 04/15] [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) --- esphome/core/time.cpp | 16 ++++++---------- esphome/core/time.h | 2 ++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 73ba0a9be7..6add82e7d1 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -283,19 +283,15 @@ void ESPTime::recalc_timestamp_local() { bool dst_valid = time::is_in_dst(utc_if_dst, tz); bool std_valid = !time::is_in_dst(utc_if_std, tz); - if (dst_valid && std_valid) { - // Ambiguous time (repeated hour during fall-back) - prefer standard time - this->timestamp = utc_if_std; - } else if (dst_valid) { + if (dst_valid && !std_valid) { // Only DST interpretation is valid this->timestamp = utc_if_dst; - } else if (std_valid) { - // Only standard interpretation is valid - this->timestamp = utc_if_std; } else { - // Invalid time (skipped hour during spring-forward) - // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT - // Using std offset achieves this since the UTC result falls during DST + // All other cases use standard offset: + // - Both valid (ambiguous fall-back repeated hour): prefer standard time + // - Only standard valid: straightforward + // - Neither valid (spring-forward skipped hour): std offset normalizes + // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT this->timestamp = utc_if_std; } #else diff --git a/esphome/core/time.h b/esphome/core/time.h index 874f0db4b4..1716c51ffd 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #include #include #include From 2d39cc2540e877f7bec755b74c2cdf60e625c6b0 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:38:04 -0400 Subject: [PATCH 05/15] [spa06_i2c] Add SPA06-003 Temperature and Pressure Sensor - I2C support (Part 2 of 3) (#14522) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_i2c/__init__.py | 0 esphome/components/spa06_i2c/sensor.py | 23 +++++++++++++++++++ esphome/components/spa06_i2c/spa06_i2c.cpp | 14 +++++++++++ esphome/components/spa06_i2c/spa06_i2c.h | 20 ++++++++++++++++ tests/components/spa06_i2c/common.yaml | 15 ++++++++++++ .../components/spa06_i2c/test.esp32-idf.yaml | 4 ++++ .../spa06_i2c/test.esp8266-ard.yaml | 4 ++++ .../components/spa06_i2c/test.rp2040-ard.yaml | 4 ++++ 9 files changed, 85 insertions(+) create mode 100644 esphome/components/spa06_i2c/__init__.py create mode 100644 esphome/components/spa06_i2c/sensor.py create mode 100644 esphome/components/spa06_i2c/spa06_i2c.cpp create mode 100644 esphome/components/spa06_i2c/spa06_i2c.h create mode 100644 tests/components/spa06_i2c/common.yaml create mode 100644 tests/components/spa06_i2c/test.esp32-idf.yaml create mode 100644 tests/components/spa06_i2c/test.esp8266-ard.yaml create mode 100644 tests/components/spa06_i2c/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 5869925d7c..e3e09cbc11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -458,6 +458,7 @@ esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net +esphome/components/spa06_i2c/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_i2c/__init__.py b/esphome/components/spa06_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/spa06_i2c/sensor.py b/esphome/components/spa06_i2c/sensor.py new file mode 100644 index 0000000000..b48a5bca50 --- /dev/null +++ b/esphome/components/spa06_i2c/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv + +from ..spa06_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["spa06_base"] +CODEOWNERS = ["@danielkent-net"] +DEPENDENCIES = ["i2c"] + +spa06_ns = cg.esphome_ns.namespace("spa06_i2c") +SPA06I2CComponent = spa06_ns.class_( + "SPA06I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(SPA06I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/spa06_i2c/spa06_i2c.cpp b/esphome/components/spa06_i2c/spa06_i2c.cpp new file mode 100644 index 0000000000..4970b0822d --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.cpp @@ -0,0 +1,14 @@ +#include "spa06_i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::spa06_i2c { + +static const char *const TAG = "spa06_i2c"; + +void SPA06I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + SPA06Component::dump_config(); +} + +} // namespace esphome::spa06_i2c diff --git a/esphome/components/spa06_i2c/spa06_i2c.h b/esphome/components/spa06_i2c/spa06_i2c.h new file mode 100644 index 0000000000..6b4bce3a4e --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.h @@ -0,0 +1,20 @@ +#pragma once +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::spa06_i2c { + +class SPA06I2CComponent : public spa06_base::SPA06Component, public i2c::I2CDevice { + public: + bool spa_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool spa_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return write_bytes(a_register, data, len); + } + void dump_config() override; +}; + +} // namespace esphome::spa06_i2c diff --git a/tests/components/spa06_i2c/common.yaml b/tests/components/spa06_i2c/common.yaml new file mode 100644 index 0000000000..d2be0e3ac9 --- /dev/null +++ b/tests/components/spa06_i2c/common.yaml @@ -0,0 +1,15 @@ +sensor: + - platform: spa06_i2c + i2c_id: i2c_bus + address: 0x77 + temperature: + id: spa06_i2c_temperature + name: Outside Temperature + sample_rate: 1 + oversampling: NONE + pressure: + name: Outside Pressure + id: spa06_i2c_pressure + sample_rate: 25p4 + oversampling: 16X + update_interval: 15s diff --git a/tests/components/spa06_i2c/test.esp32-idf.yaml b/tests/components/spa06_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/spa06_i2c/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.esp8266-ard.yaml b/tests/components/spa06_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/spa06_i2c/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.rp2040-ard.yaml b/tests/components/spa06_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/spa06_i2c/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 12ead0408ab97d21f2d01fc691db2baed7a99d9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:00:56 -1000 Subject: [PATCH 06/15] [gpio] Use constexpr uint32_t timer ID for interlock timeout (#15010) --- esphome/components/gpio/switch/gpio_switch.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 9043a6a493..d461fab051 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -5,6 +5,7 @@ namespace esphome { namespace gpio { static const char *const TAG = "switch.gpio"; +static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0; float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -51,7 +52,7 @@ void GPIOSwitch::write_state(bool state) { } } if (found && this->interlock_wait_time_ != 0) { - this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + this->set_timeout(INTERLOCK_TIMEOUT_ID, this->interlock_wait_time_, [this, state] { // Don't write directly, call the function again // (some other switch may have changed state while we were waiting) this->write_state(state); @@ -61,7 +62,7 @@ void GPIOSwitch::write_state(bool state) { } else if (this->interlock_wait_time_ != 0) { // If we are switched off during the interlock wait time, cancel any pending // re-activations - this->cancel_timeout("interlock"); + this->cancel_timeout(INTERLOCK_TIMEOUT_ID); } this->pin_->digital_write(state); From 391ffe34f87158a0b308634018be3a03ac33814a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:01:11 -1000 Subject: [PATCH 07/15] [rp2040] Fix get_mac_address_raw to use ethernet MAC when WiFi unavailable (#15033) --- esphome/components/rp2040/helpers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index ad69192af9..e360b45ebb 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -10,6 +10,7 @@ #include // For cyw43_arch_lwip_begin/end (LwIPLock) #elif defined(USE_ETHERNET) #include // For ethernet_arch_lwip_begin/end (LwIPLock) +#include "esphome/components/ethernet/ethernet_component.h" #endif #include #include @@ -71,6 +72,8 @@ LwIPLock::~LwIPLock() {} void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #ifdef USE_WIFI WiFi.macAddress(mac); +#elif defined(USE_ETHERNET) + ethernet::global_eth_component->get_eth_mac_address_raw(mac); #endif } From 51335e88301d8ae4f6872cb8546c799ef300f964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:01:30 -1000 Subject: [PATCH 08/15] [ledc] Fix deprecated intr_type warning on ESP-IDF 6.0+ (#15009) --- esphome/components/ledc/ledc_output.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index d2f2d72acb..5b7b6c7ee6 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -193,7 +193,9 @@ void LEDCOutput::setup() { chan_conf.gpio_num = static_cast(this->pin_->get_pin()); chan_conf.speed_mode = speed_mode; chan_conf.channel = chan_num; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) chan_conf.intr_type = LEDC_INTR_DISABLE; +#endif chan_conf.timer_sel = timer_num; chan_conf.duty = this->inverted_ == this->pin_->is_inverted() ? 0 : (1U << this->bit_depth_); chan_conf.hpoint = hpoint; From edf5542559a4868c57df319ad89e4a7ebe829658 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:05:17 -1000 Subject: [PATCH 09/15] [analyze-memory] Attribute extern C symbols to components via source file mapping (#15006) --- esphome/analyze_memory/__init__.py | 125 +++++++++++++++++++++++++++++ esphome/analyze_memory/const.py | 1 - 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 7954c22822..48ecf2c1dc 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -201,6 +201,9 @@ class MemoryAnalyzer: self._cswtch_symbols: list[tuple[str, int, str, str]] = [] # Library symbol mapping: symbol_name -> library_name self._lib_symbol_map: dict[str, str] = {} + # Source file symbol mapping: symbol_name -> component_name + # Used for extern "C" and other symbols without C++ namespace + self._source_symbol_map: dict[str, str] = {} # Library dir to name mapping: "lib641" -> "espsoftwareserial", # "espressif__mdns" -> "mdns" self._lib_hash_to_name: dict[str, str] = {} @@ -214,6 +217,7 @@ class MemoryAnalyzer: self._parse_sections() self._parse_symbols() self._scan_libraries() + self._scan_source_symbols() self._categorize_symbols() self._analyze_cswtch_symbols() self._analyze_sdk_libraries() @@ -363,6 +367,11 @@ class MemoryAnalyzer: if lib_name := self._lib_symbol_map.get(symbol_name): return f"{_COMPONENT_PREFIX_LIB}{lib_name}" + # Check source file mapping (catches extern "C" functions in ESPHome sources) + # Must be before heuristic patterns since source attribution is authoritative + if component := self._source_symbol_map.get(symbol_name): + return component + # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): if any(pattern in symbol_name for pattern in patterns): @@ -653,6 +662,7 @@ class MemoryAnalyzer: return None symbol_map: dict[str, str] = {} + source_symbol_map: dict[str, str] = {} current_symbol: str | None = None section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.") @@ -688,9 +698,18 @@ class MemoryAnalyzer: if dir_key in source_path: symbol_map[current_symbol] = lib_name break + else: + # Map ESPHome source files to components for extern "C" + # and other symbols without C++ namespace + component = self._source_file_to_component(source_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_symbol_map[current_symbol] = component current_symbol = None + self._source_symbol_map = source_symbol_map return symbol_map or None def _scan_libraries(self) -> None: @@ -741,6 +760,112 @@ class MemoryAnalyzer: len(libraries), ) + def _scan_source_symbols(self) -> None: + """Scan ESPHome source object files to map extern "C" symbols to components. + + When no linker map file is available, this uses ``nm`` to scan ``.o`` files + under ``src/esphome/`` and build a symbol-to-component mapping. This catches + ``extern "C"`` functions and other symbols that lack C++ namespace prefixes. + + Skips scanning if ``_source_symbol_map`` was already populated by + ``_parse_map_file()``. + """ + if self._source_symbol_map or not self.nm_path: + return + + obj_dir = self._find_object_files_dir() + if obj_dir is None: + return + + # Find ESPHome source object files + esphome_src_dir = obj_dir / "src" / "esphome" + if not esphome_src_dir.is_dir(): + return + + obj_files = sorted(esphome_src_dir.rglob("*.o")) + if not obj_files: + return + + # Run nm with --print-file-name to get file:symbol mapping + result = run_tool( + [self.nm_path, "--print-file-name", "-g", "--defined-only"] + + [str(f) for f in obj_files], + ) + if result is None or result.returncode != 0: + _LOGGER.debug("nm scan of source objects failed") + return + + self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir) + if self._source_symbol_map: + _LOGGER.info( + "Built source symbol map from nm: %d symbols", + len(self._source_symbol_map), + ) + + def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]: + """Parse nm output to map non-namespaced symbols to ESPHome components. + + Extracts global defined symbols from ESPHome source object files that + don't use C++ namespacing (e.g. ``extern "C"`` functions). + + Args: + output: Raw stdout from ``nm --print-file-name -g --defined-only`` + or ``nm --print-file-name -S``. + base_dir: Build directory for computing relative paths. + + Returns: + Dict mapping symbol names to component names. + """ + source_map: dict[str, str] = {} + for line in output.splitlines(): + # Format: /path/to/file.o: addr type name + # or: /path/to/file.o: addr size type name (with -S) + colon_idx = line.rfind(".o:") + if colon_idx == -1: + continue + + file_path = line[: colon_idx + 2] + fields = line[colon_idx + 3 :].split() + if len(fields) < 3: + continue + + # With -S flag, format is: addr size type name + # Without -S flag: addr type name + # type is a single char; size is hex digits + # Detect by checking if fields[1] is a single uppercase letter (type) + if len(fields[1]) == 1 and fields[1].isalpha(): + # addr type name + sym_type = fields[1] + symbol_name = fields[2] + elif len(fields) >= 4: + # addr size type name + sym_type = fields[2] + symbol_name = fields[3] + else: + continue + + # Only global defined symbols (uppercase type) + if not sym_type.isupper() or sym_type == "U": + continue + + # Skip symbols already in esphome:: namespace + if symbol_name.startswith("_ZN7esphome"): + continue + + # Make path relative to base_dir for _source_file_to_component + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + continue + + component = self._source_file_to_component(rel_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_map[symbol_name] = component + + return source_map + def _find_object_files_dir(self) -> Path | None: """Find the directory containing object files for this build. diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 3bdf555ae3..0c871d0727 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -408,7 +408,6 @@ SYMBOL_PATTERNS = { ], "arduino_core": [ "pinMode", - "resetPins", "millis", "micros", "delay(", # More specific - Arduino delay function with parenthesis From 564d155cb6980e1b24cbd640cb014128dcb1cc92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:08:33 -1000 Subject: [PATCH 10/15] [wifi] Use LOG_STR_LITERAL for scan complete log on ESP8266 (#15001) --- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 5514f1c6be..f2fabb9080 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -735,7 +735,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } } ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(), - needs_full ? "" : " (filtered)"); + needs_full ? LOG_STR_LITERAL("") : LOG_STR_LITERAL(" (filtered)")); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->pending_.scan_complete = true; // Defer listener callbacks to main loop From 7f500c4b6ef14967771cec2cb2f8785cc552b891 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:08:46 -1000 Subject: [PATCH 11/15] [modbus] Fix size_t format warning in clear_rx_buffer_ (#15002) --- esphome/components/modbus/modbus.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 7a61868e6e..4146a54c87 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -415,10 +415,10 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) { size_t at = this->rx_buffer_.size(); if (at > 0) { if (warn) { - ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } else { - ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } this->rx_buffer_.clear(); From 51ccad8461069ac04cd568971b9bf4b1b148e26e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:09:01 -1000 Subject: [PATCH 12/15] [preferences] Shorten TAG strings across all platforms (#15004) --- esphome/components/esp32/preferences.cpp | 2 +- esphome/components/esp8266/preferences.cpp | 2 +- esphome/components/host/preferences.cpp | 2 +- esphome/components/libretiny/preferences.cpp | 2 +- esphome/components/rp2040/preferences.cpp | 2 +- esphome/components/zephyr/preferences.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7260bf54e0..e88ace3e6b 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::esp32 { -static const char *const TAG = "esp32.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 0b31c53ff8..906fed2b29 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -13,7 +13,7 @@ extern "C" { namespace esphome::esp8266 { -static const char *const TAG = "esp8266.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index fce3d62dda..c0be270062 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::host { namespace fs = std::filesystem; -static const char *const TAG = "host.preferences"; +static const char *const TAG = "preferences"; void HostPreferences::setup_() { if (this->setup_complete_) diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index f22c12f1fb..344ca4a8b3 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::libretiny { -static const char *const TAG = "lt.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index 0a91136a9f..cfc802b28f 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -14,7 +14,7 @@ namespace esphome::rp2040 { -static const char *const TAG = "rp2040.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512; diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index df69c0e652..c26a1d6d53 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::zephyr { -static const char *const TAG = "zephyr.preferences"; +static const char *const TAG = "preferences"; bool ZephyrPreferenceBackend::save(const uint8_t *data, size_t len) { this->data.resize(len); From 2c872600464c2b0b168e09277bb5bbe16da4fac0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:09:13 -1000 Subject: [PATCH 13/15] [core] Optimize Component::is_ready() with bitmask check (#15005) --- esphome/core/component.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 00dda0cc26..89ac0c7a2a 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -378,9 +378,10 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: #pragma GCC diagnostic pop } bool Component::is_ready() const { - return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; + // Bitmask check: valid states are SETUP(1), LOOP(2), LOOP_DONE(4) + // (1 << state) & 0b10110 checks membership in one instruction + return ((1u << (this->component_state_ & COMPONENT_STATE_MASK)) & + ((1u << COMPONENT_STATE_SETUP) | (1u << COMPONENT_STATE_LOOP) | (1u << COMPONENT_STATE_LOOP_DONE))) != 0; } bool Component::can_proceed() { return true; } bool Component::set_status_flag_(uint8_t flag) { From 32db055b98cbb964663396b6aa63239cc4f97591 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:09:28 -1000 Subject: [PATCH 14/15] [number] Clean up NumberCall::perform() increment/decrement logic (#15000) --- esphome/components/number/number_call.cpp | 22 ++++++---------------- esphome/components/number/number_call.h | 3 +++ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 27a857c112..aac9b2a23d 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -80,35 +80,25 @@ void NumberCall::perform() { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; } auto step = traits.get_step(); target_value = parent->state + (std::isnan(step) ? 1 : step); - if (target_value > max_value) { - if (this->cycle_ && !std::isnan(min_value)) { - target_value = min_value; - } else { - target_value = max_value; - } - } + if (target_value > max_value) + target_value = this->cycle_or_clamp_(max_value, min_value); } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; } auto step = traits.get_step(); target_value = parent->state - (std::isnan(step) ? 1 : step); - if (target_value < min_value) { - if (this->cycle_ && !std::isnan(max_value)) { - target_value = max_value; - } else { - target_value = min_value; - } - } + if (target_value < min_value) + target_value = this->cycle_or_clamp_(min_value, max_value); } if (target_value < min_value) { diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 584c13f413..29eaeb72d9 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -33,6 +33,9 @@ class NumberCall { NumberCall &with_cycle(bool cycle); protected: + float cycle_or_clamp_(float clamp, float opposite) const { + return (this->cycle_ && !std::isnan(opposite)) ? opposite : clamp; + } void log_perform_warning_(const LogString *message); void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, float limit); From 21e384cafd89f1a441a7de2c67816338ca90c569 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:10:18 -1000 Subject: [PATCH 15/15] [esp32] Disable PicolibC Newlib compatibility shim on IDF 6.0+ (#15008) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 64e5f44081..f85f13fe73 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1920,6 +1920,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) + # Disable PicolibC Newlib compatibility shim on IDF 6.0+ + # IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local + # stdin/stdout/stderr and getreent() for code compiled against Newlib. + # ESPHome doesn't link against Newlib-built libraries that use stdio. + # If a component needs it (e.g. precompiled Newlib binaries), re-enable via: + # esp32: + # framework: + # sdkconfig_options: + # CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y" + if idf_version() >= cv.Version(6, 0, 0): + add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False) + # Disable regi2c control functions in IRAM # Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: