From 8dd1aec60672f26d1ec2a2d16a576752bde14e64 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:17:11 -0500 Subject: [PATCH 1/9] [esp32] Add warning for experimental 400MHz on ESP32-P4 (#13433) Co-authored-by: Claude Opus 4.5 --- esphome/components/esp32/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 07446ab046..e23a70a373 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -182,6 +182,12 @@ def set_core_data(config): path=[CONF_CPU_FREQUENCY], ) + if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ": + _LOGGER.warning( + "400MHz on ESP32-P4 is experimental and may not boot. " + "Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425" + ) + CORE.data[KEY_ESP32] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32 conf = config[CONF_FRAMEWORK] From ebf589560d88844dfd0c2bb56376124d18fba59e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jan 2026 14:03:49 -1000 Subject: [PATCH 2/9] [wifi] Fix bk72xx manual_ip preventing API connection (#13426) --- esphome/components/wifi/wifi_component_libretiny.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 162ed4e835..cc9f4ec193 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -460,13 +460,15 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); } #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) + // For static IP configurations, GOT_IP event may not fire, so set connected state here +#ifdef USE_WIFI_MANUAL_IP 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)); } +#endif } #endif break; From ce5ec7a78f3cc5be9871e8dd09c6b8c042d289d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jan 2026 14:04:07 -1000 Subject: [PATCH 3/9] [spi] Fix display init failure by marking displays as write-only for half-duplex mode (#13431) --- esphome/components/epaper_spi/display.py | 2 +- esphome/components/ili9xxx/display.py | 2 +- esphome/components/max7219/display.py | 2 +- esphome/components/max7219digit/display.py | 2 +- esphome/components/mipi_rgb/display.py | 2 +- esphome/components/mipi_spi/display.py | 4 +--- esphome/components/pcd8544/display.py | 2 +- esphome/components/qspi_dbi/display.py | 2 +- esphome/components/spi/__init__.py | 7 ++++++- esphome/components/spi/spi_esp_idf.cpp | 5 ++++- esphome/components/ssd1306_spi/display.py | 2 +- esphome/components/ssd1322_spi/display.py | 2 +- esphome/components/ssd1325_spi/display.py | 2 +- esphome/components/ssd1327_spi/display.py | 2 +- esphome/components/ssd1331_spi/display.py | 2 +- esphome/components/ssd1351_spi/display.py | 2 +- esphome/components/st7567_spi/display.py | 2 +- esphome/components/st7701s/display.py | 2 +- esphome/components/st7735/display.py | 2 +- esphome/components/st7789v/display.py | 2 +- esphome/components/st7920/display.py | 2 +- esphome/components/waveshare_epaper/display.py | 2 +- 22 files changed, 30 insertions(+), 24 deletions(-) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index a77e291237..8cc7b2663c 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -190,7 +190,7 @@ async def to_code(config): # Rotation is handled by setting the transform display_config = {k: v for k, v in config.items() if k != CONF_ROTATION} await display.register_display(var, display_config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 9588bccd55..bfb2300f4f 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -223,7 +223,7 @@ async def to_code(config): var = cg.Pvariable(config[CONF_ID], rhs) await display.register_display(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) if init_sequences := config.get(CONF_INIT_SEQUENCE): diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index c9d10f3c45..a434125148 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -29,7 +29,7 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await spi.register_spi_device(var, config) + 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])) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index fef121ff10..e6d53efc5d 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -86,7 +86,7 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await spi.register_spi_device(var, config) + 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])) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 96e167b2e6..084fe6de14 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -260,7 +260,7 @@ async def to_code(config): cg.add(var.set_enable_pins(enable)) if CONF_SPI_ID in config: - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) sequence, madctl = model.get_sequence(config) cg.add(var.set_init_sequence(sequence)) cg.add(var.set_madctl(madctl)) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 69bf133c68..8dccfa3a92 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -443,6 +443,4 @@ async def to_code(config): ) cg.add(var.set_writer(lambda_)) await display.register_display(var, config) - await spi.register_spi_device(var, config) - # Displays are write-only, set the SPI device to write-only as well - cg.add(var.set_write_only(True)) + await spi.register_spi_device(var, config, write_only=True) diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index 2c24b133da..9d993c2105 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -44,7 +44,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index e4440c9b81..48d1f6d12e 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) chip = DriverChip.chips[config[CONF_MODEL]] if chip.initsequence: diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index e890567abf..931882be8d 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -39,6 +39,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core", "@clydebarrow"] spi_ns = cg.esphome_ns.namespace("spi") @@ -448,9 +449,13 @@ def spi_device_schema( ) -async def register_spi_device(var, config): +async def register_spi_device( + var: cg.Pvariable, config: ConfigType, write_only: bool = False +) -> None: parent = await cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) + if write_only: + cg.add(var.set_write_only(True)) if cs_pin := config.get(CONF_CS_PIN): pin = await cg.gpio_pin_expression(cs_pin) cg.add(var.set_cs_pin(pin)) diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index a1837fa58d..107b6a3f1a 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -195,8 +195,11 @@ class SPIDelegateHw : public SPIDelegate { config.post_cb = nullptr; if (this->bit_order_ == BIT_ORDER_LSB_FIRST) config.flags |= SPI_DEVICE_BIT_LSBFIRST; - if (this->write_only_) + if (this->write_only_) { config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; + ESP_LOGD(TAG, "SPI device with CS pin %d using half-duplex mode (write-only)", + Utility::get_pin_no(this->cs_pin_)); + } esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Add device failed - err %X", err); diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index 4af41073d4..26953b4f39 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ssd1306_base.setup_ssd1306(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1322_spi/display.py b/esphome/components/ssd1322_spi/display.py index 849e71abee..3d01caf874 100644 --- a/esphome/components/ssd1322_spi/display.py +++ b/esphome/components/ssd1322_spi/display.py @@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ssd1322_base.setup_ssd1322(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index e18db33c68..dbb9a14ac2 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ssd1325_base.setup_ssd1325(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1327_spi/display.py b/esphome/components/ssd1327_spi/display.py index b622c098ec..f052764a91 100644 --- a/esphome/components/ssd1327_spi/display.py +++ b/esphome/components/ssd1327_spi/display.py @@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ssd1327_base.setup_ssd1327(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1331_spi/display.py b/esphome/components/ssd1331_spi/display.py index 50895b3175..c16780302f 100644 --- a/esphome/components/ssd1331_spi/display.py +++ b/esphome/components/ssd1331_spi/display.py @@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ssd1331_base.setup_ssd1331(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py index bd7033c3d4..2a6e984029 100644 --- a/esphome/components/ssd1351_spi/display.py +++ b/esphome/components/ssd1351_spi/display.py @@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ssd1351_base.setup_ssd1351(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/st7567_spi/display.py b/esphome/components/st7567_spi/display.py index 305aa35024..02cd2c105c 100644 --- a/esphome/components/st7567_spi/display.py +++ b/esphome/components/st7567_spi/display.py @@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await st7567_base.setup_st7567(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index 3078158d25..a8b12dfa28 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -173,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) sequence = [] for seq in config[CONF_INIT_SEQUENCE]: diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index 2761214315..9dc69f27ff 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -99,7 +99,7 @@ async def to_code(config): config[CONF_INVERT_COLORS], ) await setup_st7735(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 8259eacf2d..c9f4199616 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -177,7 +177,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) cg.add(var.set_model_str(config[CONF_MODEL])) diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py index de7b2247dd..ef33fac6c6 100644 --- a/esphome/components/st7920/display.py +++ b/esphome/components/st7920/display.py @@ -28,7 +28,7 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index cea0b2be5e..5db7a1fc3d 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -239,7 +239,7 @@ async def to_code(config): raise NotImplementedError() await display.register_display(var, config) - await spi.register_spi_device(var, config) + await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) From 19c1d3aee70133d9e59564791c522bdac5b6c29b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:41:59 -0500 Subject: [PATCH 4/9] [esp32] Bump Arduino to 3.3.6, platform to 55.03.36 (#13438) Co-authored-by: Claude Opus 4.5 --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 23 +++++++++++++++-------- esphome/core/defines.h | 2 +- platformio.ini | 6 +++--- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 0204b50264..0a272d21ba 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -c0335c9688ce9defb4a7d4446b93460547e22df055668bacd7d963c770f0c65f +15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index e23a70a373..da49fdc070 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -348,7 +348,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 - return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" + # 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}" def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: @@ -383,11 +388,12 @@ def _is_framework_url(source: str) -> bool: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 5), - "latest": cv.Version(3, 3, 5), - "dev": cv.Version(3, 3, 5), + "recommended": cv.Version(3, 3, 6), + "latest": cv.Version(3, 3, 6), + "dev": cv.Version(3, 3, 6), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 5): cv.Version(55, 3, 35), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), @@ -405,6 +411,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { + cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), cv.Version(3, 3, 3): cv.Version(5, 5, 1), @@ -427,7 +434,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(5, 5, 2), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { - cv.Version(5, 5, 2): cv.Version(55, 3, 35), + cv.Version(5, 5, 2): cv.Version(55, 3, 36), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), @@ -444,9 +451,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 35), - "latest": cv.Version(55, 3, 35), - "dev": cv.Version(55, 3, 35), + "recommended": cv.Version(55, 3, 36), + "latest": cv.Version(55, 3, 36), + "dev": cv.Version(55, 3, 36), } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c229d1df7d..7c13823fba 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -234,7 +234,7 @@ #define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 5) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 6) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_MANUAL_IP diff --git a/platformio.ini b/platformio.ini index b109284933..e9a588e4fd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.5/esp32-3.3.5.zip + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -168,7 +168,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip platform_packages = pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz From 645832a0708573c8763ee50b7fc5564fabb619ea Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 22 Jan 2026 03:10:12 +0100 Subject: [PATCH 5/9] [nextion] Add configurable startup and queue timeout constants (#11098) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: Keith Burzinski --- esphome/components/nextion/base_component.py | 2 ++ esphome/components/nextion/display.py | 18 ++++++++++ esphome/components/nextion/nextion.cpp | 36 ++++++++++++-------- esphome/components/nextion/nextion.h | 29 ++++++++++++++-- tests/components/nextion/common.yaml | 2 ++ 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 392481e39a..86551cbe23 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -16,6 +16,7 @@ CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" CONF_FONT_ID = "font_id" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_MAX_COMMANDS_PER_LOOP = "max_commands_per_loop" +CONF_MAX_QUEUE_AGE = "max_queue_age" CONF_MAX_QUEUE_SIZE = "max_queue_size" CONF_ON_BUFFER_OVERFLOW = "on_buffer_overflow" CONF_ON_PAGE = "on_page" @@ -25,6 +26,7 @@ CONF_ON_WAKE = "on_wake" CONF_PRECISION = "precision" CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake" CONF_START_UP_PAGE = "start_up_page" +CONF_STARTUP_OVERRIDE_MS = "startup_override_ms" CONF_TFT_URL = "tft_url" CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" CONF_VARIABLE_NAME = "variable_name" diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 0b4ba3a171..ffc509fc64 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -23,6 +23,7 @@ from .base_component import ( CONF_DUMP_DEVICE_INFO, CONF_EXIT_REPARSE_ON_START, CONF_MAX_COMMANDS_PER_LOOP, + CONF_MAX_QUEUE_AGE, CONF_MAX_QUEUE_SIZE, CONF_ON_BUFFER_OVERFLOW, CONF_ON_PAGE, @@ -31,6 +32,7 @@ from .base_component import ( CONF_ON_WAKE, CONF_SKIP_CONNECTION_HANDSHAKE, CONF_START_UP_PAGE, + CONF_STARTUP_OVERRIDE_MS, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, @@ -65,6 +67,12 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean, cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, + cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=TimePeriod(milliseconds=0), max=TimePeriod(milliseconds=65535) + ), + ), cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation( @@ -100,6 +108,12 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, + cv.Optional(CONF_STARTUP_OVERRIDE_MS, default="8000ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=TimePeriod(milliseconds=0), max=TimePeriod(milliseconds=65535) + ), + ), cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, cv.Optional(CONF_TFT_URL): cv.url, cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any( @@ -138,6 +152,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await uart.register_uart_device(var, config) + cg.add(var.set_max_queue_age(config[CONF_MAX_QUEUE_AGE])) + if max_queue_size := config.get(CONF_MAX_QUEUE_SIZE): cg.add_define("USE_NEXTION_MAX_QUEUE_SIZE") cg.add(var.set_max_queue_size(max_queue_size)) @@ -146,6 +162,8 @@ async def to_code(config): cg.add_define("USE_NEXTION_COMMAND_SPACING") cg.add(var.set_command_spacing(command_spacing.total_milliseconds)) + cg.add(var.set_startup_override_ms(config[CONF_STARTUP_OVERRIDE_MS])) + if CONF_BRIGHTNESS in config: cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 354288e1a3..4bba33f961 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -152,21 +152,25 @@ void Nextion::dump_config() { #else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE ESP_LOGCONFIG(TAG, #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO - " Device Model: %s\n" - " FW Version: %s\n" - " Serial Number: %s\n" - " Flash Size: %s\n" + " Device Model: %s\n" + " FW Version: %s\n" + " Serial Number: %s\n" + " Flash Size: %s\n" + " Max queue age: %u ms\n" + " Startup override: %u ms\n", #endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START - " Exit reparse: YES\n" + " Exit reparse: YES\n" #endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START - " Wake On Touch: %s\n" - " Touch Timeout: %" PRIu16, + " Wake On Touch: %s\n" + " Touch Timeout: %" PRIu16, #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), - this->flash_size_.c_str(), + this->flash_size_.c_str(), this->max_q_age_ms_, + this->startup_override_ms_ #endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO - YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_); + YESNO(this->connection_state_.auto_wake_on_touch_), + this->touch_sleep_timeout_); #endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP @@ -174,21 +178,21 @@ void Nextion::dump_config() { #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP if (this->wake_up_page_ != 255) { - ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_); + ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_); } #ifdef USE_NEXTION_CONF_START_UP_PAGE if (this->start_up_page_ != 255) { - ESP_LOGCONFIG(TAG, " Start Up Page: %u", this->start_up_page_); + ESP_LOGCONFIG(TAG, " Start Up Page: %u", this->start_up_page_); } #endif // USE_NEXTION_CONF_START_UP_PAGE #ifdef USE_NEXTION_COMMAND_SPACING - ESP_LOGCONFIG(TAG, " Cmd spacing: %u ms", this->command_pacer_.get_spacing()); + ESP_LOGCONFIG(TAG, " Cmd spacing: %u ms", this->command_pacer_.get_spacing()); #endif // USE_NEXTION_COMMAND_SPACING #ifdef USE_NEXTION_MAX_QUEUE_SIZE - ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_); + ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_); #endif } @@ -336,7 +340,8 @@ void Nextion::loop() { if (this->started_ms_ == 0) this->started_ms_ = App.get_loop_component_start_time(); - if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { + if (this->startup_override_ms_ > 0 && + this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { ESP_LOGV(TAG, "Manual ready set"); this->connection_state_.nextion_reports_is_setup_ = true; } @@ -845,7 +850,8 @@ void Nextion::process_nextion_commands_() { const uint32_t ms = App.get_loop_component_start_time(); - if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { + if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && + this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { for (size_t i = 0; i < this->nextion_queue_.size(); i++) { NextionComponentBase *component = this->nextion_queue_[i]->component; if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) { diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 331e901578..c543e14bfe 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1309,6 +1309,30 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ bool is_connected() { return this->connection_state_.is_connected_; } + /** + * @brief Set the maximum age for queue items + * @param age_ms Maximum age in milliseconds before queue items are removed + */ + inline void set_max_queue_age(uint16_t age_ms) { this->max_q_age_ms_ = age_ms; } + + /** + * @brief Get the maximum age for queue items + * @return Maximum age in milliseconds + */ + inline uint16_t get_max_queue_age() const { return this->max_q_age_ms_; } + + /** + * @brief Set the startup override timeout + * @param timeout_ms Time in milliseconds to wait before forcing setup complete + */ + inline void set_startup_override_ms(uint16_t timeout_ms) { this->startup_override_ms_ = timeout_ms; } + + /** + * @brief Get the startup override timeout + * @return Startup override timeout in milliseconds + */ + inline uint16_t get_startup_override_ms() const { return this->startup_override_ms_; } + protected: #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP uint16_t max_commands_per_loop_{1000}; @@ -1479,9 +1503,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void reset_(bool reset_nextion = true); std::string command_data_; - const uint16_t startup_override_ms_ = 8000; - const uint16_t max_q_age_ms_ = 8000; uint32_t started_ms_ = 0; + + uint16_t startup_override_ms_ = 8000; ///< Timeout before forcing setup complete + uint16_t max_q_age_ms_ = 8000; ///< Maximum age for queue items in ms }; } // namespace nextion diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index f5ee12a51c..48a7292f43 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -277,6 +277,8 @@ display: command_spacing: 5ms max_commands_per_loop: 20 max_queue_size: 50 + startup_override_ms: 10000ms # Wait 10s for display ready + max_queue_age: 5000ms # Remove queue items after 5s on_sleep: then: lambda: 'ESP_LOGD("display","Display went to sleep");' From aa5092bdc272bf93bd7b675438608dda3f41b517 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jan 2026 18:35:43 -1000 Subject: [PATCH 6/9] [mqtt] Use stack buffers for discovery message formatting (#13216) 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> --- .../components/mqtt/custom_mqtt_device.cpp | 2 +- esphome/components/mqtt/mqtt_client.cpp | 12 ++++- esphome/components/mqtt/mqtt_component.cpp | 49 +++++++++++++------ esphome/components/mqtt/mqtt_fan.cpp | 2 +- esphome/components/mqtt/mqtt_number.cpp | 2 +- 5 files changed, 49 insertions(+), 18 deletions(-) diff --git a/esphome/components/mqtt/custom_mqtt_device.cpp b/esphome/components/mqtt/custom_mqtt_device.cpp index 7ff65bb42c..64521f5cf3 100644 --- a/esphome/components/mqtt/custom_mqtt_device.cpp +++ b/esphome/components/mqtt/custom_mqtt_device.cpp @@ -18,7 +18,7 @@ bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t num } bool CustomMQTTDevice::publish(const std::string &topic, int value) { char buffer[24]; - int len = snprintf(buffer, sizeof(buffer), "%d", value); + size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%d", value); return global_mqtt_client->publish(topic, buffer, len); } bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) { diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index de79b61358..e7364f3406 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -98,7 +98,17 @@ void MQTTClientComponent::send_device_info_() { uint8_t index = 0; for (auto &ip : network::get_ip_addresses()) { if (ip.is_set()) { - root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str(); + char key[8]; // "ip" + up to 3 digits + null + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + if (index == 0) { + key[0] = 'i'; + key[1] = 'p'; + key[2] = '\0'; + } else { + buf_append_printf(key, sizeof(key), 0, "ip%u", index); + } + ip.str_to(ip_buf); + root[key] = ip_buf; index++; } } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 2ba466af3b..cb8b92cad0 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -190,27 +190,37 @@ bool MQTTComponent::send_discovery_() { StringRef object_id = this->get_default_object_id_to_(object_id_buf); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; - snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_())); + buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32, + fnv1_hash(this->friendly_name_())); // 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]; char mac_buf[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac_buf); - snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash); + buf_append_printf(unique_id, sizeof(unique_id), 0, "%s-%s-%s", mac_buf, this->component_type(), + friendly_name_hash); root[MQTT_UNIQUE_ID] = unique_id; } else { // default to almost-unique ID. It's a hack but the only way to get that // gorgeous device registry view. - root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str(); + // "ESP" (3) + component_type (max 20) + object_id (max 128) + null + char unique_id_buf[3 + MQTT_COMPONENT_TYPE_MAX_LEN + OBJECT_ID_MAX_LEN + 1]; + buf_append_printf(unique_id_buf, sizeof(unique_id_buf), 0, "ESP%s%s", this->component_type(), + object_id.c_str()); + root[MQTT_UNIQUE_ID] = unique_id_buf; } const std::string &node_name = App.get_name(); - if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) - root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str(); + if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) { + // node_name (max 31) + "_" (1) + object_id (max 128) + null + char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1]; + buf_append_printf(object_id_full, sizeof(object_id_full), 0, "%s_%s", node_name.c_str(), object_id.c_str()); + root[MQTT_OBJECT_ID] = object_id_full; + } const std::string &friendly_name_ref = App.get_friendly_name(); const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; - std::string node_area = App.get_area(); + const char *node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); char mac[MAC_ADDRESS_BUFFER_SIZE]; @@ -221,18 +231,29 @@ bool MQTTComponent::send_discovery_() { device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")"; const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.'); device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1; - device_info[MQTT_DEVICE_MANUFACTURER] = - model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); + if (model == nullptr) { + device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME; + } else { + // Extract manufacturer (part before '.') using stack buffer to avoid heap allocation + // memcpy is used instead of strncpy since we know the exact length and strncpy + // would still require manual null-termination + char manufacturer[sizeof(ESPHOME_PROJECT_NAME)]; + size_t len = model - ESPHOME_PROJECT_NAME; + memcpy(manufacturer, ESPHOME_PROJECT_NAME, len); + manufacturer[len] = '\0'; + device_info[MQTT_DEVICE_MANUFACTURER] = manufacturer; + } #else static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")"; + // Buffer sized for format string expansion: ~4 bytes net growth from format specifier to 8 hex digits, plus + // safety margin + char version_buf[sizeof(ver_fmt) + 8]; #ifdef USE_ESP8266 - char fmt_buf[sizeof(ver_fmt)]; - strcpy_P(fmt_buf, ver_fmt); - const char *fmt = fmt_buf; + snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash()); #else - const char *fmt = ver_fmt; + snprintf(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash()); #endif - device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash()); + device_info[MQTT_DEVICE_SW_VERSION] = version_buf; device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; #if defined(USE_ESP8266) || defined(USE_ESP32) device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif"; @@ -246,7 +267,7 @@ bool MQTTComponent::send_discovery_() { device_info[MQTT_DEVICE_MANUFACTURER] = "Host"; #endif #endif - if (!node_area.empty()) { + if (node_area[0] != '\0') { device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index a6f0503588..0909090023 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -175,7 +175,7 @@ bool MQTTFanComponent::publish_state() { auto traits = this->state_->get_traits(); if (traits.supports_speed()) { char buf[12]; - int len = snprintf(buf, sizeof(buf), "%d", this->state_->speed); + size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed); bool success = this->publish(this->get_speed_level_state_topic(), buf, len); failed = failed || !success; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 8342210ee4..471c0d1208 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -75,7 +75,7 @@ bool MQTTNumberComponent::send_initial_state() { } bool MQTTNumberComponent::publish_state(float value) { char buffer[64]; - snprintf(buffer, sizeof(buffer), "%f", value); + buf_append_printf(buffer, sizeof(buffer), 0, "%f", value); return this->publish(this->get_state_topic_(), buffer); } From 99aa83564e9e2961b2d771da3b8262ed6df01a7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jan 2026 18:35:59 -1000 Subject: [PATCH 7/9] [mqtt] Reduce heap allocations in hot paths (#13362) 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 | 2 +- .../components/mqtt/mqtt_binary_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 101 ++++++++++++------ esphome/components/mqtt/mqtt_component.h | 69 ++++++++---- esphome/components/mqtt/mqtt_cover.cpp | 2 +- esphome/components/mqtt/mqtt_date.cpp | 2 +- esphome/components/mqtt/mqtt_datetime.cpp | 2 +- esphome/components/mqtt/mqtt_light.cpp | 2 +- esphome/components/mqtt/mqtt_number.cpp | 2 +- esphome/components/mqtt/mqtt_select.cpp | 2 +- esphome/components/mqtt/mqtt_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_text.cpp | 2 +- esphome/components/mqtt/mqtt_time.cpp | 2 +- esphome/components/mqtt/mqtt_valve.cpp | 2 +- esphome/core/automation.h | 57 ++++++++-- 15 files changed, 179 insertions(+), 72 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 6245d10882..715e6feed8 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -43,7 +43,7 @@ void MQTTAlarmControlPanelComponent::setup() { void MQTTAlarmControlPanelComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true) + LOG_MQTT_COMPONENT(true, true); ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32 "\n" " Requires Code to Disarm: %s\n" diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index a37043406b..7cbb5dcc0e 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -19,7 +19,7 @@ void MQTTBinarySensorComponent::setup() { void MQTTBinarySensorComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, false) + LOG_MQTT_COMPONENT(true, false); } MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor) : binary_sensor_(binary_sensor) { diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index cb8b92cad0..7607a4e817 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -27,20 +27,23 @@ inline char *append_char(char *p, char c) { // Max lengths for stack-based topic building. // These limits are enforced at Python config validation time in mqtt/__init__.py // using cv.Length() validators for topic_prefix and discovery_prefix. -// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h. +// 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 TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) - -// Stack buffer sizes - safe because all inputs are length-validated at config time -// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null -static constexpr size_t DEFAULT_TOPIC_MAX_LEN = - TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1; // 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; +// 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) { + char buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + if (state_topic) + ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str()); + if (command_topic) + ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str()); +} + void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; } @@ -69,19 +72,18 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove return std::string(buf, p - buf); } -std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const { +StringRef MQTTComponent::get_default_topic_for_to_(std::span buf, const char *suffix, + size_t suffix_len) const { const std::string &topic_prefix = global_mqtt_client->get_topic_prefix(); if (topic_prefix.empty()) { - // If the topic_prefix is null, the default topic should be null - return ""; + return StringRef(); // Empty topic_prefix means no default topic } 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[DEFAULT_TOPIC_MAX_LEN]; - char *p = buf; + char *p = buf.data(); p = append_str(p, topic_prefix.data(), topic_prefix.size()); p = append_char(p, '/'); @@ -89,21 +91,44 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con p = append_char(p, '/'); p = append_str(p, object_id.c_str(), object_id.size()); p = append_char(p, '/'); - p = append_str(p, suffix.data(), suffix.size()); + p = append_str(p, suffix, suffix_len); + *p = '\0'; - return std::string(buf, p - buf); + return StringRef(buf.data(), p - buf.data()); +} + +std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const { + char buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + StringRef ref = this->get_default_topic_for_to_(buf, suffix.data(), suffix.size()); + return std::string(ref.c_str(), ref.size()); +} + +StringRef MQTTComponent::get_state_topic_to_(std::span buf) const { + if (this->custom_state_topic_.has_value()) { + // Returns ref to existing data for static/value, uses buf only for lambda case + return this->custom_state_topic_.ref_or_copy_to(buf.data(), buf.size()); + } + return this->get_default_topic_for_to_(buf, "state", 5); +} + +StringRef MQTTComponent::get_command_topic_to_(std::span buf) const { + if (this->custom_command_topic_.has_value()) { + // Returns ref to existing data for static/value, uses buf only for lambda case + return this->custom_command_topic_.ref_or_copy_to(buf.data(), buf.size()); + } + return this->get_default_topic_for_to_(buf, "command", 7); } std::string MQTTComponent::get_state_topic_() const { - if (this->custom_state_topic_.has_value()) - return this->custom_state_topic_.value(); - return this->get_default_topic_for_("state"); + char buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + StringRef ref = this->get_state_topic_to_(buf); + return std::string(ref.c_str(), ref.size()); } std::string MQTTComponent::get_command_topic_() const { - if (this->custom_command_topic_.has_value()) - return this->custom_command_topic_.value(); - return this->get_default_topic_for_("command"); + char buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + StringRef ref = this->get_command_topic_to_(buf); + return std::string(ref.c_str(), ref.size()); } bool MQTTComponent::publish(const std::string &topic, const std::string &payload) { @@ -168,10 +193,14 @@ bool MQTTComponent::send_discovery_() { break; } - if (config.state_topic) - root[MQTT_STATE_TOPIC] = this->get_state_topic_(); - if (config.command_topic) - root[MQTT_COMMAND_TOPIC] = this->get_command_topic_(); + if (config.state_topic) { + char state_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + root[MQTT_STATE_TOPIC] = this->get_state_topic_to_(state_topic_buf); + } + if (config.command_topic) { + char command_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; + root[MQTT_COMMAND_TOPIC] = this->get_command_topic_to_(command_topic_buf); + } if (this->command_retain_) root[MQTT_COMMAND_RETAIN] = true; @@ -309,7 +338,9 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai } void MQTTComponent::disable_availability() { this->set_availability("", "", ""); } void MQTTComponent::call_setup() { - if (this->is_internal()) + // Cache is_internal result once during setup - topics don't change after this + this->is_internal_ = this->compute_is_internal_(); + if (this->is_internal_) return; this->setup(); @@ -361,26 +392,28 @@ StringRef MQTTComponent::get_default_object_id_to_(std::spanget_entity()->get_icon_ref(); } bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } -bool MQTTComponent::is_internal() { +bool MQTTComponent::compute_is_internal_() { if (this->custom_state_topic_.has_value()) { - // If the custom state_topic is null, return true as it is internal and should not publish + // If the custom state_topic is empty, return true as it is internal and should not publish // else, return false, as it is explicitly set to a topic, so it is not internal and should publish - return this->get_state_topic_().empty(); + // Using is_empty() avoids heap allocation for non-lambda cases + return this->custom_state_topic_.is_empty(); } if (this->custom_command_topic_.has_value()) { - // If the custom command_topic is null, return true as it is internal and should not publish + // If the custom command_topic is empty, return true as it is internal and should not publish // else, return false, as it is explicitly set to a topic, so it is not internal and should publish - return this->get_command_topic_().empty(); + // Using is_empty() avoids heap allocation for non-lambda cases + return this->custom_command_topic_.is_empty(); } - // No custom topics have been set - if (this->get_default_topic_for_("").empty()) { - // If the default topic prefix is null, then the component, by default, is internal and should not publish + // No custom topics have been set - check topic_prefix directly to avoid allocation + if (global_mqtt_client->get_topic_prefix().empty()) { + // If the default topic prefix is empty, then the component, by default, is internal and should not publish return true; } - // Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic + // Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic return this->get_entity()->is_internal(); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index dea91e3d5a..1a5e6db3af 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -20,17 +20,22 @@ struct SendDiscoveryConfig { bool command_topic{true}; ///< If the command topic should be included. Default to true. }; -// Max lengths for stack-based topic building (must match mqtt_component.cpp) +// Max lengths for stack-based topic building. +// These limits are enforced at Python config validation time in mqtt/__init__.py +// using cv.Length() validators for topic_prefix and discovery_prefix. +// This ensures the stack buffers are always large enough. static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20; static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32; +static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) +// Stack buffer size - safe because all inputs are length-validated at config time +// 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; -#define LOG_MQTT_COMPONENT(state_topic, command_topic) \ - if (state_topic) { \ - ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \ - } \ - if (command_topic) { \ - ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \ - } +class MQTTComponent; // Forward declaration +void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic); + +#define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic) // Macro to define component_type() with compile-time length verification // Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") @@ -74,6 +79,8 @@ static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32; * a clean separation. */ class MQTTComponent : public Component { + friend void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic); + public: /// Constructs a MQTTComponent. explicit MQTTComponent(); @@ -88,7 +95,8 @@ class MQTTComponent : public Component { virtual bool send_initial_state() = 0; - virtual bool is_internal(); + /// Returns cached is_internal result (computed once during setup). + bool is_internal() const { return this->is_internal_; } /// Set QOS for state messages. void set_qos(uint8_t qos); @@ -179,7 +187,16 @@ class MQTTComponent : public Component { /// Helper method to get the discovery topic for this component. std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const; - /** Get this components state/command/... topic. + /** Get this components state/command/... topic into a buffer. + * + * @param buf The buffer to write to (must be exactly MQTT_DEFAULT_TOPIC_MAX_LEN). + * @param suffix The suffix/key such as "state" or "command". + * @return StringRef pointing to the buffer with the topic. + */ + StringRef get_default_topic_for_to_(std::span buf, const char *suffix, + size_t suffix_len) const; + + /** Get this components state/command/... topic (allocates std::string). * * @param suffix The suffix/key such as "state" or "command". * @return The full topic. @@ -200,10 +217,20 @@ class MQTTComponent : public Component { /// Get whether the underlying Entity is disabled by default bool is_disabled_by_default_() const; - /// Get the MQTT topic that new states will be shared to. + /// Get the MQTT state topic into a buffer (no heap allocation for non-lambda custom topics). + /// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes. + /// @return StringRef pointing to the topic in the buffer. + StringRef get_state_topic_to_(std::span buf) const; + + /// Get the MQTT command topic into a buffer (no heap allocation for non-lambda custom topics). + /// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes. + /// @return StringRef pointing to the topic in the buffer. + StringRef get_command_topic_to_(std::span buf) const; + + /// Get the MQTT topic that new states will be shared to (allocates std::string). std::string get_state_topic_() const; - /// Get the MQTT topic for listening to commands. + /// Get the MQTT topic for listening to commands (allocates std::string). std::string get_command_topic_() const; bool is_connected_() const; @@ -221,12 +248,18 @@ class MQTTComponent : public Component { std::unique_ptr availability_; - bool command_retain_{false}; - bool retain_{true}; - uint8_t qos_{0}; - uint8_t subscribe_qos_{0}; - bool discovery_enabled_{true}; - bool resend_state_{false}; + // Packed bitfields - QoS values are 0-2, bools are flags + uint8_t qos_ : 2 {0}; + uint8_t subscribe_qos_ : 2 {0}; + bool command_retain_ : 1 {false}; + bool retain_ : 1 {true}; + bool discovery_enabled_ : 1 {true}; + bool resend_state_ : 1 {false}; + bool is_internal_ : 1 {false}; ///< Cached result of compute_is_internal_(), set during setup + + /// Compute is_internal status based on topics and entity state. + /// Called once during setup to cache the result. + bool compute_is_internal_(); }; } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index f2df6af236..493514c8fb 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -51,7 +51,7 @@ void MQTTCoverComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str()); auto traits = this->cover_->get_traits(); bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt(); - LOG_MQTT_COMPONENT(true, has_command_topic) + LOG_MQTT_COMPONENT(true, has_command_topic); if (traits.get_supports_position()) { ESP_LOGCONFIG(TAG, " Position State Topic: '%s'\n" diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp index dba7c1a671..cbe4045486 100644 --- a/esphome/components/mqtt/mqtt_date.cpp +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -36,7 +36,7 @@ void MQTTDateComponent::setup() { void MQTTDateComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true) + LOG_MQTT_COMPONENT(true, true); } MQTT_COMPONENT_TYPE(MQTTDateComponent, "date") diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp index 5f1cf19b97..f7b4ef0685 100644 --- a/esphome/components/mqtt/mqtt_datetime.cpp +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -47,7 +47,7 @@ void MQTTDateTimeComponent::setup() { void MQTTDateTimeComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true) + LOG_MQTT_COMPONENT(true, true); } MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime") diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index fac19f3210..e43cb63f4f 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -90,7 +90,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); } void MQTTJSONLightComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true) + LOG_MQTT_COMPONENT(true, true); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 471c0d1208..a014096c5f 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -30,7 +30,7 @@ void MQTTNumberComponent::setup() { void MQTTNumberComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, false) + LOG_MQTT_COMPONENT(true, false); } MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number") diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index 03ab82312b..2d830998ec 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -25,7 +25,7 @@ void MQTTSelectComponent::setup() { void MQTTSelectComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, false) + LOG_MQTT_COMPONENT(true, false); } MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select") diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index c14c889d47..f136b82355 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -28,7 +28,7 @@ void MQTTSensorComponent::dump_config() { if (this->get_expire_after() > 0) { ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000); } - LOG_MQTT_COMPONENT(true, false) + LOG_MQTT_COMPONENT(true, false); } MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index cee94965c6..fed9224b42 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -26,7 +26,7 @@ void MQTTTextComponent::setup() { void MQTTTextComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true) + LOG_MQTT_COMPONENT(true, true); } MQTT_COMPONENT_TYPE(MQTTTextComponent, "text") diff --git a/esphome/components/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp index b75325022a..8749c3b59e 100644 --- a/esphome/components/mqtt/mqtt_time.cpp +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -36,7 +36,7 @@ void MQTTTimeComponent::setup() { void MQTTTimeComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true) + LOG_MQTT_COMPONENT(true, true); } MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time") diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 2faaace46b..8e66a69c6f 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -39,7 +39,7 @@ void MQTTValveComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str()); auto traits = this->valve_->get_traits(); bool has_command_topic = traits.get_supports_position(); - LOG_MQTT_COMPONENT(true, has_command_topic) + LOG_MQTT_COMPONENT(true, has_command_topic); if (traits.get_supports_position()) { ESP_LOGCONFIG(TAG, " Position State Topic: '%s'\n" diff --git a/esphome/core/automation.h b/esphome/core/automation.h index eac469d0fc..31a2fc06f4 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/core/string_ref.h" #include #include #include @@ -190,15 +191,55 @@ template class TemplatableValue { /// Get the static string pointer (only valid if is_static_string() returns true) const char *get_static_string() const { return this->static_str_; } - protected: - enum : uint8_t { - NONE, - VALUE, - LAMBDA, - STATELESS_LAMBDA, - STATIC_STRING, // For const char* when T is std::string - avoids heap allocation - } type_; + /// Check if the string value is empty without allocating (for std::string specialization). + /// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation. + /// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate. + bool is_empty() const requires std::same_as { + switch (this->type_) { + case NONE: + return true; + case STATIC_STRING: + return this->static_str_ == nullptr || this->static_str_[0] == '\0'; + case VALUE: + return this->value_->empty(); + default: // LAMBDA/STATELESS_LAMBDA - must call value() + return this->value().empty(); + } + } + /// Get a StringRef to the string value without heap allocation when possible. + /// For STATIC_STRING/VALUE, returns reference to existing data (no allocation). + /// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer. + /// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used). + /// @param lambda_buf_size Size of the buffer. + /// @return StringRef pointing to the string data. + StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as { + switch (this->type_) { + case NONE: + return StringRef(); + case STATIC_STRING: + if (this->static_str_ == nullptr) + return StringRef(); + return StringRef(this->static_str_, strlen(this->static_str_)); + case VALUE: + return StringRef(this->value_->data(), this->value_->size()); + default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy + std::string result = this->value(); + size_t copy_len = std::min(result.size(), lambda_buf_size - 1); + memcpy(lambda_buf, result.data(), copy_len); + lambda_buf[copy_len] = '\0'; + return StringRef(lambda_buf, copy_len); + } + } + } + + protected : enum : uint8_t { + NONE, + VALUE, + LAMBDA, + STATELESS_LAMBDA, + STATIC_STRING, // For const char* when T is std::string - avoids heap allocation + } type_; // For std::string, use heap pointer to minimize union size (4 bytes vs 12+). // For other types, store value inline as before. using ValueStorage = std::conditional_t; From a9ce3df04c9f4db4b0d66d461d6380c45b7354d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jan 2026 18:36:12 -1000 Subject: [PATCH 8/9] [esp8266] Use SmallBufferWithHeapFallback in preferences (#13397) --- esphome/components/esp8266/preferences.cpp | 25 ++++------------------ 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 47987b4a95..35d1cd07f7 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -12,7 +12,6 @@ extern "C" { #include "preferences.h" #include -#include namespace esphome::esp8266 { @@ -143,16 +142,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { return false; const size_t buffer_size = static_cast(this->length_words) + 1; - uint32_t stack_buffer[PREF_BUFFER_WORDS]; - std::unique_ptr heap_buffer; - uint32_t *buffer; - - if (buffer_size <= PREF_BUFFER_WORDS) { - buffer = stack_buffer; - } else { - heap_buffer = make_unique(buffer_size); - buffer = heap_buffer.get(); - } + SmallBufferWithHeapFallback buffer_alloc(buffer_size); + uint32_t *buffer = buffer_alloc.get(); memset(buffer, 0, buffer_size * sizeof(uint32_t)); memcpy(buffer, data, len); @@ -167,16 +158,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { return false; const size_t buffer_size = static_cast(this->length_words) + 1; - uint32_t stack_buffer[PREF_BUFFER_WORDS]; - std::unique_ptr heap_buffer; - uint32_t *buffer; - - if (buffer_size <= PREF_BUFFER_WORDS) { - buffer = stack_buffer; - } else { - heap_buffer = make_unique(buffer_size); - buffer = heap_buffer.get(); - } + SmallBufferWithHeapFallback buffer_alloc(buffer_size); + uint32_t *buffer = buffer_alloc.get(); bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) : load_from_rtc(this->offset, buffer, buffer_size); From a1c4d5626857d8eecb656b8d588de99759090356 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jan 2026 18:37:13 -1000 Subject: [PATCH 9/9] [alarm_control_panel] Reduce heap allocations in arm/disarm methods (#13358) --- .../alarm_control_panel.cpp | 55 ++++++------------- .../alarm_control_panel/alarm_control_panel.h | 30 ++++++++-- .../alarm_control_panel_call.cpp | 6 +- .../alarm_control_panel_call.h | 3 +- .../alarm_control_panel/automation.h | 30 +--------- 5 files changed, 49 insertions(+), 75 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 248b5065ad..ab0a780cef 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function &&callback) this->ready_callback_.add(std::move(callback)); } -void AlarmControlPanel::arm_away(optional code) { +void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), + const char *code) { auto call = this->make_call(); - call.arm_away(); - if (code.has_value()) - call.set_code(code.value()); + (call.*arm_method)(); + if (code != nullptr) + call.set_code(code); call.perform(); } -void AlarmControlPanel::arm_home(optional code) { - auto call = this->make_call(); - call.arm_home(); - if (code.has_value()) - call.set_code(code.value()); - call.perform(); +void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); } + +void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); } + +void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); } + +void AlarmControlPanel::arm_vacation(const char *code) { + this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code); } -void AlarmControlPanel::arm_night(optional code) { - auto call = this->make_call(); - call.arm_night(); - if (code.has_value()) - call.set_code(code.value()); - call.perform(); +void AlarmControlPanel::arm_custom_bypass(const char *code) { + this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code); } -void AlarmControlPanel::arm_vacation(optional code) { - auto call = this->make_call(); - call.arm_vacation(); - if (code.has_value()) - call.set_code(code.value()); - call.perform(); -} - -void AlarmControlPanel::arm_custom_bypass(optional code) { - auto call = this->make_call(); - call.arm_custom_bypass(); - if (code.has_value()) - call.set_code(code.value()); - call.perform(); -} - -void AlarmControlPanel::disarm(optional code) { - auto call = this->make_call(); - call.disarm(); - if (code.has_value()) - call.set_code(code.value()); - call.perform(); -} +void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); } } // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index 340f15bcd6..e8dc197e26 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase { * * @param code The code */ - void arm_away(optional code = nullopt); + void arm_away(const char *code = nullptr); + void arm_away(const optional &code) { + this->arm_away(code.has_value() ? code.value().c_str() : nullptr); + } /** arm the alarm in home mode * * @param code The code */ - void arm_home(optional code = nullopt); + void arm_home(const char *code = nullptr); + void arm_home(const optional &code) { + this->arm_home(code.has_value() ? code.value().c_str() : nullptr); + } /** arm the alarm in night mode * * @param code The code */ - void arm_night(optional code = nullopt); + void arm_night(const char *code = nullptr); + void arm_night(const optional &code) { + this->arm_night(code.has_value() ? code.value().c_str() : nullptr); + } /** arm the alarm in vacation mode * * @param code The code */ - void arm_vacation(optional code = nullopt); + void arm_vacation(const char *code = nullptr); + void arm_vacation(const optional &code) { + this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr); + } /** arm the alarm in custom bypass mode * * @param code The code */ - void arm_custom_bypass(optional code = nullopt); + void arm_custom_bypass(const char *code = nullptr); + void arm_custom_bypass(const optional &code) { + this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr); + } /** disarm the alarm * * @param code The code */ - void disarm(optional code = nullopt); + void disarm(const char *code = nullptr); + void disarm(const optional &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); } /** Get the state * @@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase { protected: friend AlarmControlPanelCall; + // Helper to reduce code duplication for arm/disarm methods + void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code); // in order to store last panel state in flash ESPPreferenceObject pref_; // current state diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp index 5e98d58368..ba58ee3904 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel"; AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} -AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) { - this->code_ = code; +AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) { + if (code != nullptr) { + this->code_ = std::string(code); + } return *this; } diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h index cff00900dd..58764ea166 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -14,7 +14,8 @@ class AlarmControlPanelCall { public: AlarmControlPanelCall(AlarmControlPanel *parent); - AlarmControlPanelCall &set_code(const std::string &code); + AlarmControlPanelCall &set_code(const char *code); + AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); } AlarmControlPanelCall &arm_away(); AlarmControlPanelCall &arm_home(); AlarmControlPanelCall &arm_night(); diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index ce5ceadb47..4ff34de0d5 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -66,15 +66,7 @@ template class ArmAwayAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(const Ts &...x) override { - auto call = this->alarm_control_panel_->make_call(); - auto code = this->code_.optional_value(x...); - if (code.has_value()) { - call.set_code(code.value()); - } - call.arm_away(); - call.perform(); - } + void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); } protected: AlarmControlPanel *alarm_control_panel_; @@ -86,15 +78,7 @@ template class ArmHomeAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(const Ts &...x) override { - auto call = this->alarm_control_panel_->make_call(); - auto code = this->code_.optional_value(x...); - if (code.has_value()) { - call.set_code(code.value()); - } - call.arm_home(); - call.perform(); - } + void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); } protected: AlarmControlPanel *alarm_control_panel_; @@ -106,15 +90,7 @@ template class ArmNightAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(const Ts &...x) override { - auto call = this->alarm_control_panel_->make_call(); - auto code = this->code_.optional_value(x...); - if (code.has_value()) { - call.set_code(code.value()); - } - call.arm_night(); - call.perform(); - } + void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); } protected: AlarmControlPanel *alarm_control_panel_;