From 898c8a583692658fdfd4225490d60eac0339a46c Mon Sep 17 00:00:00 2001 From: Shivam Maurya <54358380+shvmm@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:31:00 +0530 Subject: [PATCH 001/251] [core] ESP32 chip revision text (#13647) --- esphome/core/application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 55eb25ce09..0e77be9ee4 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -210,7 +210,7 @@ void Application::loop() { #ifdef USE_ESP32 esp_chip_info_t chip_info; esp_chip_info(&chip_info); - ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, + ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, chip_info.revision % 100, chip_info.cores); #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) // Suggest optimization for chips that don't need the PSRAM cache workaround From a1a60c44da41c01be280d0eac59a27d7be2a2723 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 12:48:34 -0600 Subject: [PATCH 002/251] [web_server_base] Update ESPAsyncWebServer to 3.9.6 (#13639) --- .clang-tidy.hash | 2 +- esphome/components/web_server_base/__init__.py | 2 +- platformio.ini | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 1cb5f98c28..ab354259e3 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab +069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3 diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 6c756575d4..6326b4d6ff 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -53,4 +53,4 @@ async def to_code(config): "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] ) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json - cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5") + cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") diff --git a/platformio.ini b/platformio.ini index 0f5bf2f8fb..bb0de3c2b1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -114,7 +114,7 @@ lib_deps = ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp - ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base makuna/NeoPixelBus@2.7.3 ; neopixelbus ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) @@ -202,7 +202,7 @@ lib_deps = ${common:arduino.lib_deps} ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp bblanchon/ArduinoJson@7.4.2 ; json - ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -218,7 +218,7 @@ framework = arduino lib_compat_mode = soft lib_deps = bblanchon/ArduinoJson@7.4.2 ; json - ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.2 ; wireguard build_flags = ${common:arduino.build_flags} From 4e96b20b46f0ff59c75d549e0ad11f0d9fb287f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 12:49:14 -0600 Subject: [PATCH 003/251] [mqtt] Restore ESP8266 on_message defer to prevent stack overflow (#13648) --- esphome/components/mqtt/mqtt_client.cpp | 32 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index e7364f3406..a284b162dd 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -643,10 +643,34 @@ static bool topic_match(const char *message, const char *subscription) { } void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { - for (auto &subscription : this->subscriptions_) { - if (topic_match(topic.c_str(), subscription.topic.c_str())) - subscription.callback(topic, payload); - } +#ifdef USE_ESP8266 + // IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266. + // + // On ESP8266, this callback is invoked directly from the lwIP/AsyncTCP network stack + // which runs in the "sys" context with a very limited stack (~4KB). By the time we + // reach this function, the stack is already partially consumed by the network + // processing chain: tcp_input -> AsyncClient::_recv -> AsyncMqttClient::_onMessage -> here. + // + // MQTT subscription callbacks can trigger arbitrary user actions (automations, HTTP + // requests, sensor updates, etc.) which may have deep call stacks of their own. + // For example, an HTTP request action requires: DNS lookup -> TCP connect -> TLS + // handshake (if HTTPS) -> request formatting. This easily overflows the remaining + // system stack space, causing a LoadStoreAlignmentCause exception or silent corruption. + // + // By deferring to the main loop, we ensure callbacks execute with a fresh, full-size + // stack in the normal application context rather than the constrained network task. + // + // DO NOT REMOVE THIS DEFER without understanding the above. It may appear to work + // in simple tests but will cause crashes with complex automations. + this->defer([this, topic, payload]() { +#endif + for (auto &subscription : this->subscriptions_) { + if (topic_match(topic.c_str(), subscription.topic.c_str())) + subscription.callback(topic, payload); + } +#ifdef USE_ESP8266 + }); +#endif } // Setters From ca9ed369f97d792757cdd79d48841accb1a63226 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 30 Jan 2026 20:59:47 +0100 Subject: [PATCH 004/251] [pmsx003] support device-types `PMS1003`, `PMS3003`, `PMS9003M` (#13640) --- esphome/components/pmsx003/pmsx003.cpp | 28 +++--- esphome/components/pmsx003/pmsx003.h | 5 +- esphome/components/pmsx003/sensor.py | 120 ++++++++++++++++++++++--- 3 files changed, 127 insertions(+), 26 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index b0920de062..114ecf435e 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -181,17 +181,20 @@ optional PMSX003Component::check_byte_() { bool PMSX003Component::check_payload_length_(uint16_t payload_length) { // https://avaldebe.github.io/PyPMS/sensors/Plantower/ switch (this->type_) { - case Type::PMSX003: - // The expected payload length is typically 28 bytes. - // However, a 20-byte payload check was already present in the code. - // No official documentation was found confirming this. - // Retaining this check to avoid breaking existing behavior. + case Type::PMS1003: + return payload_length == 28; // 2*13+2 + case Type::PMS3003: // Data 7/8/9 not set/reserved + return payload_length == 20; // 2*9+2 + case Type::PMSX003: // Data 13 not set/reserved + // Deprecated: Length 20 is for PMS3003 backwards compatibility return payload_length == 28 || payload_length == 20; // 2*13+2 case Type::PMS5003S: - case Type::PMS5003T: - return payload_length == 28; // 2*13+2 (Data 13 not set/reserved) - case Type::PMS5003ST: - return payload_length == 36; // 2*17+2 (Data 16 not set/reserved) + case Type::PMS5003T: // Data 13 not set/reserved + return payload_length == 28; // 2*13+2 + case Type::PMS5003ST: // Data 16 not set/reserved + return payload_length == 36; // 2*17+2 + case Type::PMS9003M: + return payload_length == 28; // 2*13+2 } return false; } @@ -314,9 +317,10 @@ void PMSX003Component::parse_data_() { } // Firmware Version and Error Code - if (this->type_ == Type::PMS5003ST) { - const uint8_t firmware_version = this->data_[36]; - const uint8_t error_code = this->data_[37]; + if (this->type_ == Type::PMS1003 || this->type_ == Type::PMS5003ST || this->type_ == Type::PMS9003M) { + const uint8_t firmware_error_code_offset = (this->type_ == Type::PMS5003ST) ? 36 : 28; + const uint8_t firmware_version = this->data_[firmware_error_code_offset]; + const uint8_t error_code = this->data_[firmware_error_code_offset + 1]; ESP_LOGD(TAG, "Got Firmware Version: 0x%02X, Error Code: 0x%02X", firmware_version, error_code); } diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index f2d4e68db7..d559f2dec0 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -8,10 +8,13 @@ namespace esphome::pmsx003 { enum class Type : uint8_t { - PMSX003 = 0, + PMS1003 = 0, + PMS3003, + PMSX003, // PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component) PMS5003S, PMS5003T, PMS5003ST, + PMS9003M, }; enum class Command : uint8_t { diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 4f95b1dcdb..cdcedc85ac 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -40,33 +40,127 @@ pmsx003_ns = cg.esphome_ns.namespace("pmsx003") PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component) PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) -TYPE_PMSX003 = "PMSX003" +TYPE_PMS1003 = "PMS1003" +TYPE_PMS3003 = "PMS3003" +TYPE_PMSX003 = "PMSX003" # PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component) TYPE_PMS5003S = "PMS5003S" TYPE_PMS5003T = "PMS5003T" TYPE_PMS5003ST = "PMS5003ST" +TYPE_PMS9003M = "PMS9003M" Type = pmsx003_ns.enum("Type", is_class=True) PMSX003_TYPES = { + TYPE_PMS1003: Type.PMS1003, + TYPE_PMS3003: Type.PMS3003, TYPE_PMSX003: Type.PMSX003, TYPE_PMS5003S: Type.PMS5003S, TYPE_PMS5003T: Type.PMS5003T, TYPE_PMS5003ST: Type.PMS5003ST, + TYPE_PMS9003M: Type.PMS9003M, } SENSORS_TO_TYPE = { - CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], - CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST], - CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST], + CONF_PM_1_0_STD: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_2_5_STD: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_10_0_STD: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_1_0: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_2_5: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_10_0: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_0_3UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_0_5UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_1_0UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_2_5UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_5_0UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_10_0UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], CONF_FORMALDEHYDE: [TYPE_PMS5003S, TYPE_PMS5003ST], CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], From 5e3561d60bddad69629e2f558676687ac5259847 Mon Sep 17 00:00:00 2001 From: J0k3r2k1 <60352302+J0k3r2k1@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:33:45 +0100 Subject: [PATCH 005/251] [mipi_spi] Fix log_pin() FlashStringHelper compatibility (#13624) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/mipi_spi/mipi_spi.cpp | 39 +++++++++++++++++-- esphome/components/mipi_spi/mipi_spi.h | 39 ++++--------------- .../components/mipi_spi/test.esp8266-ard.yaml | 10 +++++ 3 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 tests/components/mipi_spi/test.esp8266-ard.yaml diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 272915b4e1..90f6324511 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -1,6 +1,39 @@ #include "mipi_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace mipi_spi {} // namespace mipi_spi -} // namespace esphome +namespace esphome::mipi_spi { + +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) { + ESP_LOGCONFIG(TAG, + "MIPI_SPI Display\n" + " Model: %s\n" + " Width: %d\n" + " Height: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s\n" + " Invert colors: %s\n" + " Color order: %s\n" + " Display pixels: %d bits\n" + " Endianness: %s\n" + " SPI Mode: %d\n" + " SPI Data rate: %uMHz\n" + " SPI Bus width: %d", + model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)), + YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB", + display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast(data_rate / 1000000), + bus_width); + LOG_PIN(" CS Pin: ", cs); + LOG_PIN(" Reset Pin: ", reset); + LOG_PIN(" DC Pin: ", dc); + if (offset_width != 0) + ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width); + if (offset_height != 0) + ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height); + if (brightness.has_value()) + ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value()); +} + +} // namespace esphome::mipi_spi diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index fd5bc97596..083ff9507f 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -63,6 +63,11 @@ enum BusType { BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer }; +// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width); + /** * Base class for MIPI SPI displays. * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. @@ -201,37 +206,9 @@ class MipiSpi : public display::Display, } void dump_config() override { - esph_log_config(TAG, - "MIPI_SPI Display\n" - " Model: %s\n" - " Width: %u\n" - " Height: %u", - this->model_, WIDTH, HEIGHT); - if constexpr (OFFSET_WIDTH != 0) - esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH); - if constexpr (OFFSET_HEIGHT != 0) - esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT); - esph_log_config(TAG, - " Swap X/Y: %s\n" - " Mirror X: %s\n" - " Mirror Y: %s\n" - " Invert colors: %s\n" - " Color order: %s\n" - " Display pixels: %d bits\n" - " Endianness: %s\n", - YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), - YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_), - this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); - if (this->brightness_.has_value()) - esph_log_config(TAG, " Brightness: %u", this->brightness_.value()); - log_pin(TAG, " CS Pin: ", this->cs_); - log_pin(TAG, " Reset Pin: ", this->reset_pin_); - log_pin(TAG, " DC Pin: ", this->dc_pin_); - esph_log_config(TAG, - " SPI Mode: %d\n" - " SPI Data rate: %dMHz\n" - " SPI Bus width: %d", - this->mode_, static_cast(this->data_rate_ / 1000000), BUS_TYPE); + internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_, + DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, + this->mode_, this->data_rate_, BUS_TYPE); } protected: diff --git a/tests/components/mipi_spi/test.esp8266-ard.yaml b/tests/components/mipi_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ef6197d852 --- /dev/null +++ b/tests/components/mipi_spi/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + dc_pin: GPIO15 + cs_pin: GPIO5 + enable_pin: GPIO4 + reset_pin: GPIO16 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml From 9dcb469460e1dddc6988bddb001fc0c60c17aa0f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:18:30 +1100 Subject: [PATCH 006/251] [core] Simplify generation of Lambda during `to_code()` (#13533) --- esphome/components/udp/__init__.py | 17 +- esphome/core/__init__.py | 4 + esphome/cpp_generator.py | 20 +- tests/unit_tests/test_cpp_generator.py | 277 +++++++++++++++++++++++++ 4 files changed, 310 insertions(+), 8 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 9be196d420..8252e35023 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -12,8 +12,8 @@ from esphome.components.packet_transport import ( ) import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID -from esphome.core import ID, Lambda -from esphome.cpp_generator import ExpressionStatement, MockObj +from esphome.core import ID +from esphome.cpp_generator import literal CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -24,6 +24,8 @@ udp_ns = cg.esphome_ns.namespace("udp") UDPComponent = udp_ns.class_("UDPComponent", cg.Component) UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action) trigger_args = cg.std_vector.template(cg.uint8) +trigger_argname = "data" +trigger_argtype = [(trigger_args, trigger_argname)] CONF_ADDRESSES = "addresses" CONF_LISTEN_ADDRESS = "listen_address" @@ -111,13 +113,14 @@ async def to_code(config): cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]])) if on_receive := config.get(CONF_ON_RECEIVE): on_receive = on_receive[0] - trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) + trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) trigger = await automation.build_automation( - trigger, [(trigger_args, "data")], on_receive + trigger_id, trigger_argtype, on_receive ) - trigger = Lambda(str(ExpressionStatement(trigger.trigger(MockObj("data"))))) - trigger = await cg.process_lambda(trigger, [(trigger_args, "data")]) - cg.add(var.add_listener(trigger)) + trigger_lambda = await cg.process_lambda( + trigger.trigger(literal(trigger_argname)), trigger_argtype + ) + cg.add(var.add_listener(trigger_lambda)) cg.add(var.set_should_listen()) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 9a7dd49609..5308ad241e 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -278,9 +278,13 @@ LAMBDA_PROG = re.compile(r"\bid\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)") class Lambda: def __init__(self, value): + from esphome.cpp_generator import Expression, statement + # pylint: disable=protected-access if isinstance(value, Lambda): self._value = value._value + elif isinstance(value, Expression): + self._value = str(statement(value)) else: self._value = value self._parts = None diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index cff0748c95..83f2d6cf81 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -462,6 +462,16 @@ def statement(expression: Expression | Statement) -> Statement: return ExpressionStatement(expression) +def literal(name: str) -> "MockObj": + """Create a literal name that will appear in the generated code + not surrounded by quotes. + + :param name: The name of the literal. + :return: The literal as a MockObj. + """ + return MockObj(name, "") + + def variable( id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True ) -> "MockObj": @@ -665,7 +675,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: async def process_lambda( - value: Lambda, + value: Lambda | Expression, parameters: TemplateArgsType, capture: str = "", return_type: SafeExpType = None, @@ -689,6 +699,14 @@ async def process_lambda( if value is None: return None + # Inadvertently passing a malformed parameters value will lead to the build process mysteriously hanging at the + # "Generating C++ source..." stage, so check here to save the developer's hair. + assert isinstance(parameters, list) and all( + isinstance(p, tuple) and len(p) == 2 for p in parameters + ) + if isinstance(value, Expression): + value = Lambda(value) + parts = value.parts[:] for i, id in enumerate(value.requires_ids): full_id, var = await get_variable_with_full_id(id) diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 2c9f760c8e..8755e6e2a1 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -347,3 +347,280 @@ class TestMockObj: assert isinstance(actual, cg.MockObj) assert actual.base == "foo.eek" assert actual.op == "." + + +class TestStatementFunction: + """Tests for the statement() function.""" + + def test_statement__expression_converted_to_statement(self): + """Test that expressions are converted to ExpressionStatement.""" + expr = cg.RawExpression("foo()") + result = cg.statement(expr) + + assert isinstance(result, cg.ExpressionStatement) + assert str(result) == "foo();" + + def test_statement__statement_unchanged(self): + """Test that statements are returned unchanged.""" + stmt = cg.RawStatement("foo()") + result = cg.statement(stmt) + + assert result is stmt + assert str(result) == "foo()" + + def test_statement__expression_statement_unchanged(self): + """Test that ExpressionStatement is returned unchanged.""" + stmt = cg.ExpressionStatement(42) + result = cg.statement(stmt) + + assert result is stmt + assert str(result) == "42;" + + def test_statement__line_comment_unchanged(self): + """Test that LineComment is returned unchanged.""" + stmt = cg.LineComment("This is a comment") + result = cg.statement(stmt) + + assert result is stmt + assert str(result) == "// This is a comment" + + +class TestLiteralFunction: + """Tests for the literal() function.""" + + def test_literal__creates_mockobj(self): + """Test that literal() creates a MockObj.""" + result = cg.literal("MY_CONSTANT") + + assert isinstance(result, cg.MockObj) + assert result.base == "MY_CONSTANT" + assert result.op == "" + + def test_literal__string_representation(self): + """Test that literal names appear unquoted in generated code.""" + result = cg.literal("nullptr") + + assert str(result) == "nullptr" + + def test_literal__can_be_used_in_expressions(self): + """Test that literals can be used as part of larger expressions.""" + null_lit = cg.literal("nullptr") + expr = cg.CallExpression(cg.RawExpression("my_func"), null_lit) + + assert str(expr) == "my_func(nullptr)" + + def test_literal__common_cpp_literals(self): + """Test common C++ literal values.""" + test_cases = [ + ("nullptr", "nullptr"), + ("true", "true"), + ("false", "false"), + ("NULL", "NULL"), + ("NAN", "NAN"), + ] + + for name, expected in test_cases: + result = cg.literal(name) + assert str(result) == expected + + +class TestLambdaConstructor: + """Tests for the Lambda class constructor in core/__init__.py.""" + + def test_lambda__from_string(self): + """Test Lambda constructor with string argument.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + 1;") + + assert lambda_obj.value == "return x + 1;" + assert str(lambda_obj) == "return x + 1;" + + def test_lambda__from_expression(self): + """Test Lambda constructor with Expression argument.""" + from esphome.core import Lambda + + expr = cg.RawExpression("x + 1") + lambda_obj = Lambda(expr) + + # Expression should be converted to statement (with semicolon) + assert lambda_obj.value == "x + 1;" + + def test_lambda__from_lambda(self): + """Test Lambda constructor with another Lambda argument.""" + from esphome.core import Lambda + + original = Lambda("return x + 1;") + copy = Lambda(original) + + assert copy.value == original.value + assert copy.value == "return x + 1;" + + def test_lambda__parts_parsing(self): + """Test that Lambda correctly parses parts with id() references.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return id(my_sensor).state;") + parts = lambda_obj.parts + + # Parts should be split by LAMBDA_PROG regex: text, id, op, text + assert len(parts) == 4 + assert parts[0] == "return " + assert parts[1] == "my_sensor" + assert parts[2] == "." + assert parts[3] == "state;" + + def test_lambda__requires_ids(self): + """Test that Lambda correctly extracts required IDs.""" + from esphome.core import ID, Lambda + + lambda_obj = Lambda("return id(sensor1).state + id(sensor2).value;") + ids = lambda_obj.requires_ids + + assert len(ids) == 2 + assert all(isinstance(id_obj, ID) for id_obj in ids) + assert ids[0].id == "sensor1" + assert ids[1].id == "sensor2" + + def test_lambda__no_ids(self): + """Test Lambda with no id() references.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return 42;") + ids = lambda_obj.requires_ids + + assert len(ids) == 0 + + def test_lambda__comment_removal(self): + """Test that comments are removed when parsing parts.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return id(sensor).state; // Get sensor state") + parts = lambda_obj.parts + + # Comment should be replaced with space, not affect parsing + assert "my_sensor" not in str(parts) + + def test_lambda__multiline_string(self): + """Test Lambda with multiline string.""" + from esphome.core import Lambda + + code = """if (id(sensor).state > 0) { + return true; +} +return false;""" + lambda_obj = Lambda(code) + + assert lambda_obj.value == code + assert "sensor" in [id_obj.id for id_obj in lambda_obj.requires_ids] + + +@pytest.mark.asyncio +class TestProcessLambda: + """Tests for the process_lambda() async function.""" + + async def test_process_lambda__none_value(self): + """Test that None returns None.""" + result = await cg.process_lambda(None, []) + + assert result is None + + async def test_process_lambda__with_expression(self): + """Test process_lambda with Expression argument.""" + + expr = cg.RawExpression("return x + 1") + result = await cg.process_lambda(expr, [(int, "x")]) + + assert isinstance(result, cg.LambdaExpression) + assert "x + 1" in str(result) + + async def test_process_lambda__simple_lambda_no_ids(self): + """Test process_lambda with simple Lambda without id() references.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + 1;") + result = await cg.process_lambda(lambda_obj, [(int, "x")]) + + assert isinstance(result, cg.LambdaExpression) + # Should have parameter + lambda_str = str(result) + assert "int32_t x" in lambda_str + assert "return x + 1;" in lambda_str + + async def test_process_lambda__with_return_type(self): + """Test process_lambda with return type specified.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x > 0;") + result = await cg.process_lambda(lambda_obj, [(int, "x")], return_type=bool) + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "-> bool" in lambda_str + + async def test_process_lambda__with_capture(self): + """Test process_lambda with capture specified.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return captured + x;") + result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="captured") + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "[captured]" in lambda_str + + async def test_process_lambda__empty_capture(self): + """Test process_lambda with empty capture (stateless lambda).""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + 1;") + result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="") + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "[]" in lambda_str + + async def test_process_lambda__no_parameters(self): + """Test process_lambda with no parameters.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return 42;") + result = await cg.process_lambda(lambda_obj, []) + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + # Should have empty parameter list + assert "()" in lambda_str + + async def test_process_lambda__multiple_parameters(self): + """Test process_lambda with multiple parameters.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + y + z;") + result = await cg.process_lambda( + lambda_obj, [(int, "x"), (float, "y"), (bool, "z")] + ) + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "int32_t x" in lambda_str + assert "float y" in lambda_str + assert "bool z" in lambda_str + + async def test_process_lambda__parameter_validation(self): + """Test that malformed parameters raise assertion error.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x;") + + # Test invalid parameter format (not list of tuples) + with pytest.raises(AssertionError): + await cg.process_lambda(lambda_obj, "invalid") + + # Test invalid tuple format (not 2-element tuples) + with pytest.raises(AssertionError): + await cg.process_lambda(lambda_obj, [(int, "x", "extra")]) + + # Test invalid tuple format (single element) + with pytest.raises(AssertionError): + await cg.process_lambda(lambda_obj, [(int,)]) From 0fd50b2381e6a137243d9a96ae493ba5d2c8f10c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 31 Jan 2026 01:21:52 -0600 Subject: [PATCH 007/251] [esp32] Disable unused per-tag log filtering, saving ~536 bytes RAM (#13662) --- esphome/components/esp32/__init__.py | 4 ++++ esphome/components/i2c/i2c.cpp | 9 --------- esphome/components/logger/logger_esp32.cpp | 3 --- esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 5 +++-- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a3154f9933..aed4ecad90 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1329,6 +1329,10 @@ async def to_code(config): # Disable dynamic log level control to save memory add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) + # Disable per-tag log level filtering since dynamic level control is disabled above + # This saves ~250 bytes of RAM (tag cache) and associated code + add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True) + # Reduce PHY TX power in the event of a brownout add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index c1e7336ce4..b9b5d79428 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -11,12 +11,6 @@ namespace i2c { static const char *const TAG = "i2c"; void I2CBus::i2c_scan_() { - // suppress logs from the IDF I2C library during the scan -#if defined(USE_ESP32) && defined(USE_LOGGER) - auto previous = esp_log_level_get("*"); - esp_log_level_set("*", ESP_LOG_NONE); -#endif - for (uint8_t address = 8; address != 120; address++) { auto err = write_readv(address, nullptr, 0, nullptr, 0); if (err == ERROR_OK) { @@ -27,9 +21,6 @@ void I2CBus::i2c_scan_() { // it takes 16sec to scan on nrf52. It prevents board reset. arch_feed_wdt(); } -#if defined(USE_ESP32) && defined(USE_LOGGER) - esp_log_level_set("*", previous); -#endif } ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) { diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 32ef752462..9defb6c166 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -114,9 +114,6 @@ void Logger::pre_setup() { global_logger = this; esp_log_set_vprintf(esp_idf_log_vprintf_); - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { - esp_log_level_set("*", ESP_LOG_VERBOSE); - } ESP_LOGI(TAG, "Log initialized"); } diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index 5c91150f30..224d6e3ab1 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -155,8 +155,9 @@ void USBCDCACMInstance::setup() { return; } - // Use a larger stack size for (very) verbose logging - const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; + // Use a larger stack size for very verbose logging + constexpr size_t stack_size = + ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; // Create a simple, unique task name per interface char task_name[] = "usb_tx_0"; From 891382a32ee35578571186c8407ce8cabe7deccd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:59:13 -0500 Subject: [PATCH 008/251] [max7219] Allocate buffer in constructor (#13660) Co-authored-by: Claude Opus 4.5 --- esphome/components/max7219/display.py | 3 +-- esphome/components/max7219/max7219.cpp | 17 ++++++++--------- esphome/components/max7219/max7219.h | 11 +++++------ 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index a434125148..abb20702bd 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -28,11 +28,10 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS]) await spi.register_spi_device(var, config, write_only=True) await display.register_display(var, config) - cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) cg.add(var.set_intensity(config[CONF_INTENSITY])) cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 157b317c02..d701e6fc86 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { static const char *const TAG = "max7219"; @@ -115,12 +114,14 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { }; float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) { + this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT + memset(this->buffer_, 0, this->num_chips_ * 8); +} + void MAX7219Component::setup() { this->spi_setup(); - this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT - for (uint8_t i = 0; i < this->num_chips_ * 8; i++) - this->buffer_[i] = 0; - // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); // let's use our own ASCII -> led pattern encoding @@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t intensity) { this->intensity_ = intensity; } } -void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; @@ -240,5 +240,4 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time } uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 58d871d54c..ef38628f28 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -6,8 +6,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { class MAX7219Component; @@ -17,6 +16,8 @@ class MAX7219Component : public PollingComponent, public spi::SPIDevice { public: + explicit MAX7219Component(uint8_t num_chips); + void set_writer(max7219_writer_t &&writer); void setup() override; @@ -30,7 +31,6 @@ class MAX7219Component : public PollingComponent, void display(); void set_intensity(uint8_t intensity); - void set_num_chips(uint8_t num_chips); void set_reverse(bool reverse) { this->reverse_ = reverse; }; /// Evaluate the printf-format and print the result at the given position. @@ -56,10 +56,9 @@ class MAX7219Component : public PollingComponent, uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most) bool intensity_changed_{}; // True if we need to re-send the intensity uint8_t num_chips_{1}; - uint8_t *buffer_; + uint8_t *buffer_{nullptr}; bool reverse_{false}; max7219_writer_t writer_{}; }; -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 From 1ff2f3b6a38386af88b61758fb904808ce4c2c0d Mon Sep 17 00:00:00 2001 From: Simon Fischer Date: Sat, 31 Jan 2026 16:48:27 +0100 Subject: [PATCH 009/251] [dlms_meter] Add dlms smart meter component (#8009) Co-authored-by: Thomas Rupprecht Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 --- CODEOWNERS | 1 + esphome/components/dlms_meter/__init__.py | 57 +++ esphome/components/dlms_meter/dlms.h | 71 +++ esphome/components/dlms_meter/dlms_meter.cpp | 468 ++++++++++++++++++ esphome/components/dlms_meter/dlms_meter.h | 96 ++++ esphome/components/dlms_meter/mbus.h | 69 +++ esphome/components/dlms_meter/obis.h | 94 ++++ .../components/dlms_meter/sensor/__init__.py | 124 +++++ .../dlms_meter/text_sensor/__init__.py | 37 ++ .../components/dlms_meter/common-generic.yaml | 11 + .../components/dlms_meter/common-netznoe.yaml | 17 + tests/components/dlms_meter/common.yaml | 27 + .../components/dlms_meter/test.esp32-ard.yaml | 4 + .../components/dlms_meter/test.esp32-idf.yaml | 4 + .../dlms_meter/test.esp8266-ard.yaml | 4 + .../common/uart_2400/esp32-ard.yaml | 11 + .../common/uart_2400/esp32-idf.yaml | 11 + .../common/uart_2400/esp8266-ard.yaml | 11 + 18 files changed, 1117 insertions(+) create mode 100644 esphome/components/dlms_meter/__init__.py create mode 100644 esphome/components/dlms_meter/dlms.h create mode 100644 esphome/components/dlms_meter/dlms_meter.cpp create mode 100644 esphome/components/dlms_meter/dlms_meter.h create mode 100644 esphome/components/dlms_meter/mbus.h create mode 100644 esphome/components/dlms_meter/obis.h create mode 100644 esphome/components/dlms_meter/sensor/__init__.py create mode 100644 esphome/components/dlms_meter/text_sensor/__init__.py create mode 100644 tests/components/dlms_meter/common-generic.yaml create mode 100644 tests/components/dlms_meter/common-netznoe.yaml create mode 100644 tests/components/dlms_meter/common.yaml create mode 100644 tests/components/dlms_meter/test.esp32-ard.yaml create mode 100644 tests/components/dlms_meter/test.esp32-idf.yaml create mode 100644 tests/components/dlms_meter/test.esp8266-ard.yaml create mode 100644 tests/test_build_components/common/uart_2400/esp32-ard.yaml create mode 100644 tests/test_build_components/common/uart_2400/esp32-idf.yaml create mode 100644 tests/test_build_components/common/uart_2400/esp8266-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index b7675f9406..1d165a6f57 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -134,6 +134,7 @@ esphome/components/dfplayer/* @glmnet esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter esphome/components/display_menu_base/* @numo68 +esphome/components/dlms_meter/* @SimonFischer04 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/ds2484/* @mrk-its diff --git a/esphome/components/dlms_meter/__init__.py b/esphome/components/dlms_meter/__init__.py new file mode 100644 index 0000000000..c22ab7b552 --- /dev/null +++ b/esphome/components/dlms_meter/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 + +CODEOWNERS = ["@SimonFischer04"] +DEPENDENCIES = ["uart"] + +CONF_DLMS_METER_ID = "dlms_meter_id" +CONF_DECRYPTION_KEY = "decryption_key" +CONF_PROVIDER = "provider" + +PROVIDERS = {"generic": 0, "netznoe": 1} + +dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter") +DlmsMeterComponent = dlms_meter_component_ns.class_( + "DlmsMeterComponent", cg.Component, uart.UARTDevice +) + + +def validate_key(value): + value = cv.string_strict(value) + if len(value) != 32: + raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)") + try: + return [int(value[i : i + 2], 16) for i in range(0, 32, 2)] + except ValueError as exc: + raise cv.Invalid("Decryption key must be hex values from 00 to FF") from exc + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DlmsMeterComponent), + cv.Required(CONF_DECRYPTION_KEY): validate_key, + cv.Optional(CONF_PROVIDER, default="generic"): cv.enum( + PROVIDERS, lower=True + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]), +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "dlms_meter", baud_rate=2400, require_rx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + key = ", ".join(str(b) for b in config[CONF_DECRYPTION_KEY]) + cg.add(var.set_decryption_key(cg.RawExpression(f"{{{key}}}"))) + cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]])) diff --git a/esphome/components/dlms_meter/dlms.h b/esphome/components/dlms_meter/dlms.h new file mode 100644 index 0000000000..a3d8f62ce6 --- /dev/null +++ b/esphome/components/dlms_meter/dlms.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +namespace esphome::dlms_meter { + +/* ++-------------------------------+ +| Ciphering Service | ++-------------------------------+ +| System Title Length | ++-------------------------------+ +| | +| | +| | +| System | +| Title | +| | +| | +| | ++-------------------------------+ +| Length | (1 or 3 Bytes) ++-------------------------------+ +| Security Control Byte | ++-------------------------------+ +| | +| Frame | +| Counter | +| | ++-------------------------------+ +| | +~ ~ + Encrypted Payload +~ ~ +| | ++-------------------------------+ + +Ciphering Service: 0xDB (General-Glo-Ciphering) +System Title Length: 0x08 +System Title: Unique ID of meter +Length: 1 Byte=Length <= 127, 3 Bytes=Length > 127 (0x82 & 2 Bytes length) +Security Control Byte: +- Bit 3…0: Security_Suite_Id +- Bit 4: "A" subfield: indicates that authentication is applied +- Bit 5: "E" subfield: indicates that encryption is applied +- Bit 6: Key_Set subfield: 0 = Unicast, 1 = Broadcast +- Bit 7: Indicates the use of compression. + */ + +static constexpr uint8_t DLMS_HEADER_LENGTH = 16; +static constexpr uint8_t DLMS_HEADER_EXT_OFFSET = 2; // Extra offset for extended length header +static constexpr uint8_t DLMS_CIPHER_OFFSET = 0; +static constexpr uint8_t DLMS_SYST_OFFSET = 1; +static constexpr uint8_t DLMS_LENGTH_OFFSET = 10; +static constexpr uint8_t TWO_BYTE_LENGTH = 0x82; +static constexpr uint8_t DLMS_LENGTH_CORRECTION = 5; // Header bytes included in length field +static constexpr uint8_t DLMS_SECBYTE_OFFSET = 11; +static constexpr uint8_t DLMS_FRAMECOUNTER_OFFSET = 12; +static constexpr uint8_t DLMS_FRAMECOUNTER_LENGTH = 4; +static constexpr uint8_t DLMS_PAYLOAD_OFFSET = 16; +static constexpr uint8_t GLO_CIPHERING = 0xDB; +static constexpr uint8_t DATA_NOTIFICATION = 0x0F; +static constexpr uint8_t TIMESTAMP_DATETIME = 0x0C; +static constexpr uint16_t MAX_MESSAGE_LENGTH = 512; // Maximum size of message (when having 2 bytes length in header). + +// Provider specific quirks +static constexpr uint8_t NETZ_NOE_MAGIC_BYTE = 0x81; // Magic length byte used by Netz NOE +static constexpr uint8_t NETZ_NOE_EXPECTED_MESSAGE_LENGTH = 0xF8; +static constexpr uint8_t NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE = 0x20; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp new file mode 100644 index 0000000000..6aa465143e --- /dev/null +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -0,0 +1,468 @@ +#include "dlms_meter.h" + +#include + +#if defined(USE_ESP8266_FRAMEWORK_ARDUINO) +#include +#elif defined(USE_ESP32) +#include "mbedtls/esp_config.h" +#include "mbedtls/gcm.h" +#endif + +namespace esphome::dlms_meter { + +static constexpr const char *TAG = "dlms_meter"; + +void DlmsMeterComponent::dump_config() { + const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic"; + ESP_LOGCONFIG(TAG, + "DLMS Meter:\n" + " Provider: %s\n" + " Read Timeout: %u ms", + provider_name, this->read_timeout_); +#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_); + DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, ) +#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_); + DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, ) +} + +void DlmsMeterComponent::loop() { + // Read while data is available, netznoe uses two frames so allow 2x max frame length + while (this->available()) { + if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) { + ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes"); + break; + } + uint8_t c; + this->read_byte(&c); + this->receive_buffer_.push_back(c); + this->last_read_ = millis(); + } + + if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) { + this->mbus_payload_.clear(); + if (!this->parse_mbus_(this->mbus_payload_)) + return; + + uint16_t message_length; + uint8_t systitle_length; + uint16_t header_offset; + if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset)) + return; + + if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) { + ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length); + this->receive_buffer_.clear(); + return; + } + + // Decrypt in place and then decode the OBIS codes + if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset)) + return; + this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length); + } +} + +bool DlmsMeterComponent::parse_mbus_(std::vector &mbus_payload) { + ESP_LOGV(TAG, "Parsing M-Bus frames"); + uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames + + while (frame_offset < this->receive_buffer_.size()) { + // Ensure enough bytes remain for the minimal intro header before accessing indices + if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) { + ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH, + (this->receive_buffer_.size() - frame_offset)); + this->receive_buffer_.clear(); + return false; + } + + // Check start bytes + if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME || + this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) { + ESP_LOGE(TAG, "MBUS: Start bytes do not match"); + this->receive_buffer_.clear(); + return false; + } + + // Both length bytes must be identical + if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] != + this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) { + ESP_LOGE(TAG, "MBUS: Length bytes do not match"); + this->receive_buffer_.clear(); + return false; + } + + uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame + + // Check if received data is enough for the given frame length + if (this->receive_buffer_.size() - frame_offset < + frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte + ESP_LOGE(TAG, "MBUS: Frame too big for received data"); + this->receive_buffer_.clear(); + return false; + } + + // Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte + size_t required_total = + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes + if (this->receive_buffer_.size() - frame_offset < required_total) { + ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total, + this->receive_buffer_.size() - frame_offset); + this->receive_buffer_.clear(); + return false; + } + + if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] != + STOP_BYTE) { + ESP_LOGE(TAG, "MBUS: Invalid stop byte"); + this->receive_buffer_.clear(); + return false; + } + + // Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte + uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored + for (uint16_t i = 0; i < frame_length; i++) { + checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i]; + } + if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) { + ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum, + this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]); + this->receive_buffer_.clear(); + return false; + } + + mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH], + &this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]); + + frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH; + } + return true; +} + +bool DlmsMeterComponent::parse_dlms_(const std::vector &mbus_payload, uint16_t &message_length, + uint8_t &systitle_length, uint16_t &header_offset) { + ESP_LOGV(TAG, "Parsing DLMS header"); + if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) { + ESP_LOGE(TAG, "DLMS: Payload too short"); + this->receive_buffer_.clear(); + return false; + } + + if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB) + ESP_LOGE(TAG, "DLMS: Unsupported cipher"); + this->receive_buffer_.clear(); + return false; + } + + systitle_length = mbus_payload[DLMS_SYST_OFFSET]; + + if (systitle_length != 0x08) { // Only system titles with length of 8 are supported + ESP_LOGE(TAG, "DLMS: Unsupported system title length"); + this->receive_buffer_.clear(); + return false; + } + + message_length = mbus_payload[DLMS_LENGTH_OFFSET]; + header_offset = 0; + + if (this->provider_ == PROVIDER_NETZNOE) { + // for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next + // byte. Check some bytes to see if received data still matches expectation + if (message_length == NETZ_NOE_MAGIC_BYTE && + mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH && + mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) { + message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1]; + header_offset = 1; + } else { + ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN"); + } + } else { + if (message_length == TWO_BYTE_LENGTH) { + message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]); + header_offset = DLMS_HEADER_EXT_OFFSET; + } + } + if (message_length < DLMS_LENGTH_CORRECTION) { + ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length); + this->receive_buffer_.clear(); + return false; + } + message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length + + if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) { + ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(), + DLMS_HEADER_LENGTH, header_offset, message_length); + ESP_LOGE(TAG, "DLMS: Message has invalid length"); + this->receive_buffer_.clear(); + return false; + } + + if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 && + mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != + 0x20) { // Only certain security suite is supported (0x21 || 0x20) + ESP_LOGE(TAG, "DLMS: Unsupported security control byte"); + this->receive_buffer_.clear(); + return false; + } + + return true; +} + +bool DlmsMeterComponent::decrypt_(std::vector &mbus_payload, uint16_t message_length, uint8_t systitle_length, + uint16_t header_offset) { + ESP_LOGV(TAG, "Decrypting payload"); + uint8_t iv[12]; // Reserve space for the IV, always 12 bytes + // Copy system title to IV (System title is before length; no header offset needed!) + // Add 1 to the offset in order to skip the system title length byte + memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length); + memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET], + DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV + + uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET]; + +#if defined(USE_ESP8266_FRAMEWORK_ARDUINO) + br_gcm_context gcm_ctx; + br_aes_ct_ctr_keys bc; + br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size()); + br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32); + br_gcm_reset(&gcm_ctx, iv, sizeof(iv)); + br_gcm_flip(&gcm_ctx); + br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length); +#elif defined(USE_ESP32) + size_t outlen = 0; + mbedtls_gcm_context gcm_ctx; + mbedtls_gcm_init(&gcm_ctx); + mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8); + mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv)); + auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen); + mbedtls_gcm_free(&gcm_ctx); + if (ret != 0) { + ESP_LOGE(TAG, "Decryption failed with error: %d", ret); + this->receive_buffer_.clear(); + return false; + } +#else +#error "Invalid Platform" +#endif + + if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) { + ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid"); + this->receive_buffer_.clear(); + return false; + } + ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length); + return true; +} + +void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) { + ESP_LOGV(TAG, "Decoding payload"); + MeterData data{}; + uint16_t current_position = DECODER_START_OFFSET; + bool power_factor_found = false; + + while (current_position + OBIS_CODE_OFFSET <= message_length) { + if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) { + ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]); + this->receive_buffer_.clear(); + return; + } + + uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET]; + if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) { + ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length); + this->receive_buffer_.clear(); + return; + } + if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code"); + this->receive_buffer_.clear(); + return; + } + + uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET]; + uint8_t obis_medium = obis_code[OBIS_A]; + uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]); + + bool timestamp_found = false; + bool meter_number_found = false; + if (this->provider_ == PROVIDER_NETZNOE) { + // Do not advance Position when reading the Timestamp at DECODER_START_OFFSET + if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) { + timestamp_found = true; + } else if (power_factor_found) { + meter_number_found = true; + power_factor_found = false; + } else { + current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position + } + } else { + current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type + } + if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY && + obis_medium != Medium::ABSTRACT) { + ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium); + this->receive_buffer_.clear(); + return; + } + + if (current_position >= message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for data type"); + this->receive_buffer_.clear(); + return; + } + + float value = 0.0f; + uint8_t value_size = 0; + uint8_t data_type = plaintext[current_position]; + current_position++; + + switch (data_type) { + case DataType::DOUBLE_LONG_UNSIGNED: { + value_size = 4; + if (current_position + value_size > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED"); + this->receive_buffer_.clear(); + return; + } + value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1], + plaintext[current_position + 2], plaintext[current_position + 3]); + current_position += value_size; + break; + } + case DataType::LONG_UNSIGNED: { + value_size = 2; + if (current_position + value_size > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED"); + this->receive_buffer_.clear(); + return; + } + value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]); + current_position += value_size; + break; + } + case DataType::OCTET_STRING: { + uint8_t data_length = plaintext[current_position]; + current_position++; // Advance past string length + if (current_position + data_length > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING"); + this->receive_buffer_.clear(); + return; + } + // Handle timestamp (normal OBIS code or NETZNOE special case) + if (obis_cd == OBIS_TIMESTAMP || timestamp_found) { + if (data_length < 8) { + ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length); + this->receive_buffer_.clear(); + return; + } + uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]); + uint8_t month = plaintext[current_position + 2]; + uint8_t day = plaintext[current_position + 3]; + uint8_t hour = plaintext[current_position + 5]; + uint8_t minute = plaintext[current_position + 6]; + uint8_t second = plaintext[current_position + 7]; + if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) { + ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute, + second); + this->receive_buffer_.clear(); + return; + } + snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, + minute, second); + } else if (meter_number_found) { + snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]); + } + current_position += data_length; + break; + } + default: + ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type); + this->receive_buffer_.clear(); + return; + } + + // Skip break after data + if (this->provider_ == PROVIDER_NETZNOE) { + // Don't skip the break on the first timestamp, as there's none + if (!timestamp_found) { + current_position += 2; + } + } else { + current_position += 2; + } + + // Check for additional data (scaler-unit structure) + if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) { + // Apply scaler: real_value = raw_value × 10^scaler + if (current_position + 1 < message_length) { + int8_t scaler = static_cast(plaintext[current_position + 1]); + if (scaler != 0) { + value *= powf(10.0f, scaler); + } + } + + // on EVN Meters there is no additional break + if (this->provider_ == PROVIDER_NETZNOE) { + current_position += 4; + } else { + current_position += 6; + } + } + + // Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED) + if (value_size > 0) { + switch (obis_cd) { + case OBIS_VOLTAGE_L1: + data.voltage_l1 = value; + break; + case OBIS_VOLTAGE_L2: + data.voltage_l2 = value; + break; + case OBIS_VOLTAGE_L3: + data.voltage_l3 = value; + break; + case OBIS_CURRENT_L1: + data.current_l1 = value; + break; + case OBIS_CURRENT_L2: + data.current_l2 = value; + break; + case OBIS_CURRENT_L3: + data.current_l3 = value; + break; + case OBIS_ACTIVE_POWER_PLUS: + data.active_power_plus = value; + break; + case OBIS_ACTIVE_POWER_MINUS: + data.active_power_minus = value; + break; + case OBIS_ACTIVE_ENERGY_PLUS: + data.active_energy_plus = value; + break; + case OBIS_ACTIVE_ENERGY_MINUS: + data.active_energy_minus = value; + break; + case OBIS_REACTIVE_ENERGY_PLUS: + data.reactive_energy_plus = value; + break; + case OBIS_REACTIVE_ENERGY_MINUS: + data.reactive_energy_minus = value; + break; + case OBIS_POWER_FACTOR: + data.power_factor = value; + power_factor_found = true; + break; + default: + ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd); + } + } + } + + this->receive_buffer_.clear(); + + ESP_LOGI(TAG, "Received valid data"); + this->publish_sensors(data); + this->status_clear_warning(); +} + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/dlms_meter.h b/esphome/components/dlms_meter/dlms_meter.h new file mode 100644 index 0000000000..c50e6f6b4d --- /dev/null +++ b/esphome/components/dlms_meter/dlms_meter.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/uart/uart.h" + +#include "mbus.h" +#include "dlms.h" +#include "obis.h" + +#include +#include + +namespace esphome::dlms_meter { + +#ifndef DLMS_METER_SENSOR_LIST +#define DLMS_METER_SENSOR_LIST(F, SEP) +#endif + +#ifndef DLMS_METER_TEXT_SENSOR_LIST +#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP) +#endif + +struct MeterData { + float voltage_l1 = 0.0f; // Voltage L1 + float voltage_l2 = 0.0f; // Voltage L2 + float voltage_l3 = 0.0f; // Voltage L3 + float current_l1 = 0.0f; // Current L1 + float current_l2 = 0.0f; // Current L2 + float current_l3 = 0.0f; // Current L3 + float active_power_plus = 0.0f; // Active power taken from grid + float active_power_minus = 0.0f; // Active power put into grid + float active_energy_plus = 0.0f; // Active energy taken from grid + float active_energy_minus = 0.0f; // Active energy put into grid + float reactive_energy_plus = 0.0f; // Reactive energy taken from grid + float reactive_energy_minus = 0.0f; // Reactive energy put into grid + char timestamp[27]{}; // Text sensor for the timestamp value + + // Netz NOE + float power_factor = 0.0f; // Power Factor + char meternumber[13]{}; // Text sensor for the meterNumber value +}; + +// Provider constants +enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 }; + +class DlmsMeterComponent : public Component, public uart::UARTDevice { + public: + DlmsMeterComponent() = default; + + void dump_config() override; + void loop() override; + + void set_decryption_key(const std::array &key) { this->decryption_key_ = key; } + void set_provider(uint32_t provider) { this->provider_ = provider; } + + void publish_sensors(MeterData &data) { +#define DLMS_METER_PUBLISH_SENSOR(s) \ + if (this->s##_sensor_ != nullptr) \ + s##_sensor_->publish_state(data.s); + DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, ) + +#define DLMS_METER_PUBLISH_TEXT_SENSOR(s) \ + if (this->s##_text_sensor_ != nullptr) \ + s##_text_sensor_->publish_state(data.s); + DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_PUBLISH_TEXT_SENSOR, ) + } + + DLMS_METER_SENSOR_LIST(SUB_SENSOR, ) + DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR, ) + + protected: + bool parse_mbus_(std::vector &mbus_payload); + bool parse_dlms_(const std::vector &mbus_payload, uint16_t &message_length, uint8_t &systitle_length, + uint16_t &header_offset); + bool decrypt_(std::vector &mbus_payload, uint16_t message_length, uint8_t systitle_length, + uint16_t header_offset); + void decode_obis_(uint8_t *plaintext, uint16_t message_length); + + std::vector receive_buffer_; // Stores the packet currently being received + std::vector mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn + uint32_t last_read_ = 0; // Timestamp when data was last read + uint32_t read_timeout_ = 1000; // Time to wait after last byte before considering data complete + + uint32_t provider_ = PROVIDER_GENERIC; // Provider of the meter / your grid operator + std::array decryption_key_; +}; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/mbus.h b/esphome/components/dlms_meter/mbus.h new file mode 100644 index 0000000000..293d43a55b --- /dev/null +++ b/esphome/components/dlms_meter/mbus.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +namespace esphome::dlms_meter { + +/* ++----------------------------------------------------+ - +| Start Character [0x68] | \ ++----------------------------------------------------+ | +| Data Length (L) | | ++----------------------------------------------------+ | +| Data Length Repeat (L) | | ++----------------------------------------------------+ > M-Bus Data link layer +| Start Character Repeat [0x68] | | ++----------------------------------------------------+ | +| Control/Function Field (C) | | ++----------------------------------------------------+ | +| Address Field (A) | / ++----------------------------------------------------+ - +| Control Information Field (CI) | \ ++----------------------------------------------------+ | +| Source Transport Service Access Point (STSAP) | > DLMS/COSEM M-Bus transport layer ++----------------------------------------------------+ | +| Destination Transport Service Access Point (DTSAP) | / ++----------------------------------------------------+ - +| | \ +~ ~ | + Data > DLMS/COSEM Application Layer +~ ~ | +| | / ++----------------------------------------------------+ - +| Checksum | \ ++----------------------------------------------------+ > M-Bus Data link layer +| Stop Character [0x16] | / ++----------------------------------------------------+ - + +Data_Length = L - C - A - CI +Each line (except Data) is one Byte + +Possible Values found in publicly available docs: +- C: 0x53/0x73 (SND_UD) +- A: FF (Broadcast) +- CI: 0x00-0x1F/0x60/0x61/0x7C/0x7D +- STSAP: 0x01 (Management Logical Device ID 1 of the meter) +- DTSAP: 0x67 (Consumer Information Push Client ID 103) + */ + +// MBUS start bytes for different telegram formats: +// - Single Character: 0xE5 (length=1) +// - Short Frame: 0x10 (length=5) +// - Control Frame: 0x68 (length=9) +// - Long Frame: 0x68 (length=9+data_length) +// This component currently only uses Long Frame. +static constexpr uint8_t START_BYTE_SINGLE_CHARACTER = 0xE5; +static constexpr uint8_t START_BYTE_SHORT_FRAME = 0x10; +static constexpr uint8_t START_BYTE_CONTROL_FRAME = 0x68; +static constexpr uint8_t START_BYTE_LONG_FRAME = 0x68; +static constexpr uint8_t MBUS_HEADER_INTRO_LENGTH = 4; // Header length for the intro (0x68, length, length, 0x68) +static constexpr uint8_t MBUS_FULL_HEADER_LENGTH = 9; // Total header length +static constexpr uint8_t MBUS_FOOTER_LENGTH = 2; // Footer after frame +static constexpr uint8_t MBUS_MAX_FRAME_LENGTH = 250; // Maximum size of frame +static constexpr uint8_t MBUS_START1_OFFSET = 0; // Offset of first start byte +static constexpr uint8_t MBUS_LENGTH1_OFFSET = 1; // Offset of first length byte +static constexpr uint8_t MBUS_LENGTH2_OFFSET = 2; // Offset of (duplicated) second length byte +static constexpr uint8_t MBUS_START2_OFFSET = 3; // Offset of (duplicated) second start byte +static constexpr uint8_t STOP_BYTE = 0x16; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/obis.h b/esphome/components/dlms_meter/obis.h new file mode 100644 index 0000000000..1bb960e61e --- /dev/null +++ b/esphome/components/dlms_meter/obis.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +namespace esphome::dlms_meter { + +// Data types as per specification +enum DataType { + NULL_DATA = 0x00, + BOOLEAN = 0x03, + BIT_STRING = 0x04, + DOUBLE_LONG = 0x05, + DOUBLE_LONG_UNSIGNED = 0x06, + OCTET_STRING = 0x09, + VISIBLE_STRING = 0x0A, + UTF8_STRING = 0x0C, + BINARY_CODED_DECIMAL = 0x0D, + INTEGER = 0x0F, + LONG = 0x10, + UNSIGNED = 0x11, + LONG_UNSIGNED = 0x12, + LONG64 = 0x14, + LONG64_UNSIGNED = 0x15, + ENUM = 0x16, + FLOAT32 = 0x17, + FLOAT64 = 0x18, + DATE_TIME = 0x19, + DATE = 0x1A, + TIME = 0x1B, + + ARRAY = 0x01, + STRUCTURE = 0x02, + COMPACT_ARRAY = 0x13 +}; + +enum Medium { + ABSTRACT = 0x00, + ELECTRICITY = 0x01, + HEAT_COST_ALLOCATOR = 0x04, + COOLING = 0x05, + HEAT = 0x06, + GAS = 0x07, + COLD_WATER = 0x08, + HOT_WATER = 0x09, + OIL = 0x10, + COMPRESSED_AIR = 0x11, + NITROGEN = 0x12 +}; + +// Data structure +static constexpr uint8_t DECODER_START_OFFSET = 20; // Skip header, timestamp and break block +static constexpr uint8_t OBIS_TYPE_OFFSET = 0; +static constexpr uint8_t OBIS_LENGTH_OFFSET = 1; +static constexpr uint8_t OBIS_CODE_OFFSET = 2; +static constexpr uint8_t OBIS_CODE_LENGTH_STANDARD = 0x06; // 6-byte OBIS code (A.B.C.D.E.F) +static constexpr uint8_t OBIS_CODE_LENGTH_EXTENDED = 0x0C; // 12-byte extended OBIS code +static constexpr uint8_t OBIS_A = 0; +static constexpr uint8_t OBIS_B = 1; +static constexpr uint8_t OBIS_C = 2; +static constexpr uint8_t OBIS_D = 3; +static constexpr uint8_t OBIS_E = 4; +static constexpr uint8_t OBIS_F = 5; + +// Metadata +static constexpr uint16_t OBIS_TIMESTAMP = 0x0100; +static constexpr uint16_t OBIS_SERIAL_NUMBER = 0x6001; +static constexpr uint16_t OBIS_DEVICE_NAME = 0x2A00; + +// Voltage +static constexpr uint16_t OBIS_VOLTAGE_L1 = 0x2007; +static constexpr uint16_t OBIS_VOLTAGE_L2 = 0x3407; +static constexpr uint16_t OBIS_VOLTAGE_L3 = 0x4807; + +// Current +static constexpr uint16_t OBIS_CURRENT_L1 = 0x1F07; +static constexpr uint16_t OBIS_CURRENT_L2 = 0x3307; +static constexpr uint16_t OBIS_CURRENT_L3 = 0x4707; + +// Power +static constexpr uint16_t OBIS_ACTIVE_POWER_PLUS = 0x0107; +static constexpr uint16_t OBIS_ACTIVE_POWER_MINUS = 0x0207; + +// Active energy +static constexpr uint16_t OBIS_ACTIVE_ENERGY_PLUS = 0x0108; +static constexpr uint16_t OBIS_ACTIVE_ENERGY_MINUS = 0x0208; + +// Reactive energy +static constexpr uint16_t OBIS_REACTIVE_ENERGY_PLUS = 0x0308; +static constexpr uint16_t OBIS_REACTIVE_ENERGY_MINUS = 0x0408; + +// Netz NOE specific +static constexpr uint16_t OBIS_POWER_FACTOR = 0x0D07; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/sensor/__init__.py b/esphome/components/dlms_meter/sensor/__init__.py new file mode 100644 index 0000000000..27fd44f008 --- /dev/null +++ b/esphome/components/dlms_meter/sensor/__init__.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, + UNIT_WATT_HOURS, +) + +from .. import CONF_DLMS_METER_ID, DlmsMeterComponent + +AUTO_LOAD = ["dlms_meter"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), + cv.Optional("voltage_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_power_plus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_power_minus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_energy_plus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("active_energy_minus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("reactive_energy_plus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("reactive_energy_minus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + # Netz NOE + cv.Optional("power_factor"): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) + + sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf[CONF_ID] + if id and id.type == sensor.Sensor: + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) + sensors.append(f"F({key})") + + if sensors: + cg.add_define( + "DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors)) + ) diff --git a/esphome/components/dlms_meter/text_sensor/__init__.py b/esphome/components/dlms_meter/text_sensor/__init__.py new file mode 100644 index 0000000000..4d2373f4f9 --- /dev/null +++ b/esphome/components/dlms_meter/text_sensor/__init__.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ID + +from .. import CONF_DLMS_METER_ID, DlmsMeterComponent + +AUTO_LOAD = ["dlms_meter"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), + cv.Optional("timestamp"): text_sensor.text_sensor_schema(), + # Netz NOE + cv.Optional("meternumber"): text_sensor.text_sensor_schema(), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) + + text_sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf[CONF_ID] + if id and id.type == text_sensor.TextSensor: + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) + text_sensors.append(f"F({key})") + + if text_sensors: + cg.add_define( + "DLMS_METER_TEXT_SENSOR_LIST(F, sep)", + cg.RawExpression(" sep ".join(text_sensors)), + ) diff --git a/tests/components/dlms_meter/common-generic.yaml b/tests/components/dlms_meter/common-generic.yaml new file mode 100644 index 0000000000..edb1c66f0f --- /dev/null +++ b/tests/components/dlms_meter/common-generic.yaml @@ -0,0 +1,11 @@ +dlms_meter: + decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key! + +sensor: + - platform: dlms_meter + reactive_energy_plus: + name: "Reactive energy taken from grid" + reactive_energy_minus: + name: "Reactive energy put into grid" + +<<: !include common.yaml diff --git a/tests/components/dlms_meter/common-netznoe.yaml b/tests/components/dlms_meter/common-netznoe.yaml new file mode 100644 index 0000000000..db064b64f9 --- /dev/null +++ b/tests/components/dlms_meter/common-netznoe.yaml @@ -0,0 +1,17 @@ +dlms_meter: + decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key! + provider: netznoe # (optional) key - only set if using evn + +sensor: + - platform: dlms_meter + # EVN + power_factor: + name: "Power Factor" + +text_sensor: + - platform: dlms_meter + # EVN + meternumber: + name: "meterNumber" + +<<: !include common.yaml diff --git a/tests/components/dlms_meter/common.yaml b/tests/components/dlms_meter/common.yaml new file mode 100644 index 0000000000..6aa4e1b0ff --- /dev/null +++ b/tests/components/dlms_meter/common.yaml @@ -0,0 +1,27 @@ +sensor: + - platform: dlms_meter + voltage_l1: + name: "Voltage L1" + voltage_l2: + name: "Voltage L2" + voltage_l3: + name: "Voltage L3" + current_l1: + name: "Current L1" + current_l2: + name: "Current L2" + current_l3: + name: "Current L3" + active_power_plus: + name: "Active power taken from grid" + active_power_minus: + name: "Active power put into grid" + active_energy_plus: + name: "Active energy taken from grid" + active_energy_minus: + name: "Active energy put into grid" + +text_sensor: + - platform: dlms_meter + timestamp: + name: "timestamp" diff --git a/tests/components/dlms_meter/test.esp32-ard.yaml b/tests/components/dlms_meter/test.esp32-ard.yaml new file mode 100644 index 0000000000..4f8a06c31b --- /dev/null +++ b/tests/components/dlms_meter/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml + +<<: !include common-generic.yaml diff --git a/tests/components/dlms_meter/test.esp32-idf.yaml b/tests/components/dlms_meter/test.esp32-idf.yaml new file mode 100644 index 0000000000..f993515fce --- /dev/null +++ b/tests/components/dlms_meter/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml + +<<: !include common-netznoe.yaml diff --git a/tests/components/dlms_meter/test.esp8266-ard.yaml b/tests/components/dlms_meter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..2ce7955c9f --- /dev/null +++ b/tests/components/dlms_meter/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml + +<<: !include common-generic.yaml diff --git a/tests/test_build_components/common/uart_2400/esp32-ard.yaml b/tests/test_build_components/common/uart_2400/esp32-ard.yaml new file mode 100644 index 0000000000..e0b6571104 --- /dev/null +++ b/tests/test_build_components/common/uart_2400/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests - 2400 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 2400 diff --git a/tests/test_build_components/common/uart_2400/esp32-idf.yaml b/tests/test_build_components/common/uart_2400/esp32-idf.yaml new file mode 100644 index 0000000000..7bded8c91d --- /dev/null +++ b/tests/test_build_components/common/uart_2400/esp32-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 IDF tests - 2400 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 2400 diff --git a/tests/test_build_components/common/uart_2400/esp8266-ard.yaml b/tests/test_build_components/common/uart_2400/esp8266-ard.yaml new file mode 100644 index 0000000000..6c9a4a558d --- /dev/null +++ b/tests/test_build_components/common/uart_2400/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests - 2400 baud + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 2400 From 29f8d70b35d16cb8296d1a0496e4b4ce0679008f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:41:08 +0100 Subject: [PATCH 010/251] [thermostat] Avoid heap allocation for triggers (#13692) --- .../thermostat/thermostat_climate.cpp | 220 +++++++----------- .../thermostat/thermostat_climate.h | 212 +++++++---------- 2 files changed, 170 insertions(+), 262 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 44087969b5..02e01db549 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -499,7 +499,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } bool action_ready = false; - Trigger<> *trig = this->idle_action_trigger_, *trig_fan = nullptr; + Trigger<> *trig = &this->idle_action_trigger_, *trig_fan = nullptr; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: @@ -529,10 +529,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); if (this->supports_fan_with_cooling_) { this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); - trig_fan = this->fan_only_action_trigger_; + trig_fan = &this->fan_only_action_trigger_; } this->cooling_max_runtime_exceeded_ = false; - trig = this->cool_action_trigger_; + trig = &this->cool_action_trigger_; ESP_LOGVV(TAG, "Switching to COOLING action"); action_ready = true; } @@ -543,10 +543,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); if (this->supports_fan_with_heating_) { this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); - trig_fan = this->fan_only_action_trigger_; + trig_fan = &this->fan_only_action_trigger_; } this->heating_max_runtime_exceeded_ = false; - trig = this->heat_action_trigger_; + trig = &this->heat_action_trigger_; ESP_LOGVV(TAG, "Switching to HEATING action"); action_ready = true; } @@ -558,7 +558,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } else { this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); } - trig = this->fan_only_action_trigger_; + trig = &this->fan_only_action_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); action_ready = true; } @@ -567,7 +567,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu if (this->drying_action_ready_()) { this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); - trig = this->dry_action_trigger_; + trig = &this->dry_action_trigger_; ESP_LOGVV(TAG, "Switching to DRYING action"); action_ready = true; } @@ -634,14 +634,14 @@ void ThermostatClimate::trigger_supplemental_action_() { if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME)) { this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); } - trig = this->supplemental_cool_action_trigger_; + trig = &this->supplemental_cool_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental COOLING action"); break; case climate::CLIMATE_ACTION_HEATING: if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME)) { this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); } - trig = this->supplemental_heat_action_trigger_; + trig = &this->supplemental_heat_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental HEATING action"); break; default: @@ -660,24 +660,24 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction return; } - Trigger<> *trig = this->humidity_control_off_action_trigger_; + Trigger<> *trig = &this->humidity_control_off_action_trigger_; switch (action) { case THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF: - // trig = this->humidity_control_off_action_trigger_; + // trig = &this->humidity_control_off_action_trigger_; ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action"); break; case THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY: - trig = this->humidity_control_dehumidify_action_trigger_; + trig = &this->humidity_control_dehumidify_action_trigger_; ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action"); break; case THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY: - trig = this->humidity_control_humidify_action_trigger_; + trig = &this->humidity_control_humidify_action_trigger_; ESP_LOGVV(TAG, "Switching to HUMIDIFY action"); break; case THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE: default: action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF; - // trig = this->humidity_control_off_action_trigger_; + // trig = &this->humidity_control_off_action_trigger_; } if (this->prev_humidity_control_trigger_ != nullptr) { @@ -703,53 +703,53 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->publish_state(); if (this->fan_mode_ready_()) { - Trigger<> *trig = this->fan_mode_auto_trigger_; + Trigger<> *trig = &this->fan_mode_auto_trigger_; switch (fan_mode) { case climate::CLIMATE_FAN_ON: - trig = this->fan_mode_on_trigger_; + trig = &this->fan_mode_on_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ON mode"); break; case climate::CLIMATE_FAN_OFF: - trig = this->fan_mode_off_trigger_; + trig = &this->fan_mode_off_trigger_; ESP_LOGVV(TAG, "Switching to FAN_OFF mode"); break; case climate::CLIMATE_FAN_AUTO: - // trig = this->fan_mode_auto_trigger_; + // trig = &this->fan_mode_auto_trigger_; ESP_LOGVV(TAG, "Switching to FAN_AUTO mode"); break; case climate::CLIMATE_FAN_LOW: - trig = this->fan_mode_low_trigger_; + trig = &this->fan_mode_low_trigger_; ESP_LOGVV(TAG, "Switching to FAN_LOW mode"); break; case climate::CLIMATE_FAN_MEDIUM: - trig = this->fan_mode_medium_trigger_; + trig = &this->fan_mode_medium_trigger_; ESP_LOGVV(TAG, "Switching to FAN_MEDIUM mode"); break; case climate::CLIMATE_FAN_HIGH: - trig = this->fan_mode_high_trigger_; + trig = &this->fan_mode_high_trigger_; ESP_LOGVV(TAG, "Switching to FAN_HIGH mode"); break; case climate::CLIMATE_FAN_MIDDLE: - trig = this->fan_mode_middle_trigger_; + trig = &this->fan_mode_middle_trigger_; ESP_LOGVV(TAG, "Switching to FAN_MIDDLE mode"); break; case climate::CLIMATE_FAN_FOCUS: - trig = this->fan_mode_focus_trigger_; + trig = &this->fan_mode_focus_trigger_; ESP_LOGVV(TAG, "Switching to FAN_FOCUS mode"); break; case climate::CLIMATE_FAN_DIFFUSE: - trig = this->fan_mode_diffuse_trigger_; + trig = &this->fan_mode_diffuse_trigger_; ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode"); break; case climate::CLIMATE_FAN_QUIET: - trig = this->fan_mode_quiet_trigger_; + trig = &this->fan_mode_quiet_trigger_; ESP_LOGVV(TAG, "Switching to FAN_QUIET mode"); break; default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value fan_mode = climate::CLIMATE_FAN_AUTO; - // trig = this->fan_mode_auto_trigger_; + // trig = &this->fan_mode_auto_trigger_; } if (this->prev_fan_mode_trigger_ != nullptr) { this->prev_fan_mode_trigger_->stop_action(); @@ -775,25 +775,25 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ this->prev_mode_trigger_->stop_action(); this->prev_mode_trigger_ = nullptr; } - Trigger<> *trig = this->off_mode_trigger_; + Trigger<> *trig = &this->off_mode_trigger_; switch (mode) { case climate::CLIMATE_MODE_AUTO: - trig = this->auto_mode_trigger_; + trig = &this->auto_mode_trigger_; break; case climate::CLIMATE_MODE_HEAT_COOL: - trig = this->heat_cool_mode_trigger_; + trig = &this->heat_cool_mode_trigger_; break; case climate::CLIMATE_MODE_COOL: - trig = this->cool_mode_trigger_; + trig = &this->cool_mode_trigger_; break; case climate::CLIMATE_MODE_HEAT: - trig = this->heat_mode_trigger_; + trig = &this->heat_mode_trigger_; break; case climate::CLIMATE_MODE_FAN_ONLY: - trig = this->fan_only_mode_trigger_; + trig = &this->fan_only_mode_trigger_; break; case climate::CLIMATE_MODE_DRY: - trig = this->dry_mode_trigger_; + trig = &this->dry_mode_trigger_; break; case climate::CLIMATE_MODE_OFF: default: @@ -824,25 +824,25 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo this->prev_swing_mode_trigger_->stop_action(); this->prev_swing_mode_trigger_ = nullptr; } - Trigger<> *trig = this->swing_mode_off_trigger_; + Trigger<> *trig = &this->swing_mode_off_trigger_; switch (swing_mode) { case climate::CLIMATE_SWING_BOTH: - trig = this->swing_mode_both_trigger_; + trig = &this->swing_mode_both_trigger_; break; case climate::CLIMATE_SWING_HORIZONTAL: - trig = this->swing_mode_horizontal_trigger_; + trig = &this->swing_mode_horizontal_trigger_; break; case climate::CLIMATE_SWING_OFF: - // trig = this->swing_mode_off_trigger_; + // trig = &this->swing_mode_off_trigger_; break; case climate::CLIMATE_SWING_VERTICAL: - trig = this->swing_mode_vertical_trigger_; + trig = &this->swing_mode_vertical_trigger_; break; default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value swing_mode = climate::CLIMATE_SWING_OFF; - // trig = this->swing_mode_off_trigger_; + // trig = &this->swing_mode_off_trigger_; } if (trig != nullptr) { trig->trigger(); @@ -1024,10 +1024,8 @@ void ThermostatClimate::check_humidity_change_trigger_() { this->prev_target_humidity_ = this->target_humidity; } // trigger the action - Trigger<> *trig = this->humidity_change_trigger_; - if (trig != nullptr) { - trig->trigger(); - } + Trigger<> *trig = &this->humidity_change_trigger_; + trig->trigger(); } void ThermostatClimate::check_temperature_change_trigger_() { @@ -1050,10 +1048,8 @@ void ThermostatClimate::check_temperature_change_trigger_() { } } // trigger the action - Trigger<> *trig = this->temperature_change_trigger_; - if (trig != nullptr) { - trig->trigger(); - } + Trigger<> *trig = &this->temperature_change_trigger_; + trig->trigger(); } bool ThermostatClimate::cooling_required_() { @@ -1202,12 +1198,10 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { if (config != nullptr) { ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) { - // Fire any preset changed trigger if defined - Trigger<> *trig = this->preset_change_trigger_; + // Fire preset changed trigger + Trigger<> *trig = &this->preset_change_trigger_; this->set_preset_(preset); - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->refresh(); ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset))); @@ -1234,13 +1228,11 @@ void ThermostatClimate::change_custom_preset_(const char *custom_preset, size_t ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); if (this->change_preset_internal_(*config) || !this->has_custom_preset() || this->get_custom_preset() != custom_preset) { - // Fire any preset changed trigger if defined - Trigger<> *trig = this->preset_change_trigger_; + // Fire preset changed trigger + Trigger<> *trig = &this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally this->set_custom_preset_(custom_preset); - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->refresh(); ESP_LOGI(TAG, "Custom preset %s applied", custom_preset); @@ -1305,41 +1297,7 @@ void ThermostatClimate::set_custom_preset_config(std::initializer_listcustom_preset_config_ = presets; } -ThermostatClimate::ThermostatClimate() - : cool_action_trigger_(new Trigger<>()), - supplemental_cool_action_trigger_(new Trigger<>()), - cool_mode_trigger_(new Trigger<>()), - dry_action_trigger_(new Trigger<>()), - dry_mode_trigger_(new Trigger<>()), - heat_action_trigger_(new Trigger<>()), - supplemental_heat_action_trigger_(new Trigger<>()), - heat_mode_trigger_(new Trigger<>()), - heat_cool_mode_trigger_(new Trigger<>()), - auto_mode_trigger_(new Trigger<>()), - idle_action_trigger_(new Trigger<>()), - off_mode_trigger_(new Trigger<>()), - fan_only_action_trigger_(new Trigger<>()), - fan_only_mode_trigger_(new Trigger<>()), - fan_mode_on_trigger_(new Trigger<>()), - fan_mode_off_trigger_(new Trigger<>()), - fan_mode_auto_trigger_(new Trigger<>()), - fan_mode_low_trigger_(new Trigger<>()), - fan_mode_medium_trigger_(new Trigger<>()), - fan_mode_high_trigger_(new Trigger<>()), - fan_mode_middle_trigger_(new Trigger<>()), - fan_mode_focus_trigger_(new Trigger<>()), - fan_mode_diffuse_trigger_(new Trigger<>()), - fan_mode_quiet_trigger_(new Trigger<>()), - swing_mode_both_trigger_(new Trigger<>()), - swing_mode_off_trigger_(new Trigger<>()), - swing_mode_horizontal_trigger_(new Trigger<>()), - swing_mode_vertical_trigger_(new Trigger<>()), - humidity_change_trigger_(new Trigger<>()), - temperature_change_trigger_(new Trigger<>()), - preset_change_trigger_(new Trigger<>()), - humidity_control_dehumidify_action_trigger_(new Trigger<>()), - humidity_control_humidify_action_trigger_(new Trigger<>()), - humidity_control_off_action_trigger_(new Trigger<>()) {} +ThermostatClimate::ThermostatClimate() = default; void ThermostatClimate::set_default_preset(const char *custom_preset) { // Find the preset in custom_preset_config_ and store pointer from there @@ -1513,49 +1471,49 @@ void ThermostatClimate::set_supports_humidification(bool supports_humidification } } -Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } -Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const { - return this->supplemental_cool_action_trigger_; +Trigger<> *ThermostatClimate::get_cool_action_trigger() { return &this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() { + return &this->supplemental_cool_action_trigger_; } -Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } -Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } -Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } -Trigger<> *ThermostatClimate::get_supplemental_heat_action_trigger() const { - return this->supplemental_heat_action_trigger_; +Trigger<> *ThermostatClimate::get_dry_action_trigger() { return &this->dry_action_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_action_trigger() { return &this->fan_only_action_trigger_; } +Trigger<> *ThermostatClimate::get_heat_action_trigger() { return &this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_heat_action_trigger() { + return &this->supplemental_heat_action_trigger_; } -Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } -Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } -Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } -Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } -Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } -Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } -Trigger<> *ThermostatClimate::get_heat_cool_mode_trigger() const { return this->heat_cool_mode_trigger_; } -Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_quiet_trigger() const { return this->fan_mode_quiet_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_change_trigger() const { return this->humidity_change_trigger_; } -Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } -Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() const { - return this->humidity_control_dehumidify_action_trigger_; +Trigger<> *ThermostatClimate::get_idle_action_trigger() { return &this->idle_action_trigger_; } +Trigger<> *ThermostatClimate::get_auto_mode_trigger() { return &this->auto_mode_trigger_; } +Trigger<> *ThermostatClimate::get_cool_mode_trigger() { return &this->cool_mode_trigger_; } +Trigger<> *ThermostatClimate::get_dry_mode_trigger() { return &this->dry_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() { return &this->fan_only_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_mode_trigger() { return &this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_cool_mode_trigger() { return &this->heat_cool_mode_trigger_; } +Trigger<> *ThermostatClimate::get_off_mode_trigger() { return &this->off_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() { return &this->fan_mode_on_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() { return &this->fan_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() { return &this->fan_mode_auto_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() { return &this->fan_mode_low_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() { return &this->fan_mode_medium_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() { return &this->fan_mode_high_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() { return &this->fan_mode_middle_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() { return &this->fan_mode_focus_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() { return &this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_quiet_trigger() { return &this->fan_mode_quiet_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() { return &this->swing_mode_both_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() { return &this->swing_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() { return &this->swing_mode_horizontal_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() { return &this->swing_mode_vertical_trigger_; } +Trigger<> *ThermostatClimate::get_humidity_change_trigger() { return &this->humidity_change_trigger_; } +Trigger<> *ThermostatClimate::get_temperature_change_trigger() { return &this->temperature_change_trigger_; } +Trigger<> *ThermostatClimate::get_preset_change_trigger() { return &this->preset_change_trigger_; } +Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() { + return &this->humidity_control_dehumidify_action_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() const { - return this->humidity_control_humidify_action_trigger_; +Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() { + return &this->humidity_control_humidify_action_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() const { - return this->humidity_control_off_action_trigger_; +Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() { + return &this->humidity_control_off_action_trigger_; } void ThermostatClimate::dump_config() { diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index d37c9a68a6..4268d5c582 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -146,40 +146,40 @@ class ThermostatClimate : public climate::Climate, public Component { void set_preset_config(std::initializer_list presets); void set_custom_preset_config(std::initializer_list presets); - Trigger<> *get_cool_action_trigger() const; - Trigger<> *get_supplemental_cool_action_trigger() const; - Trigger<> *get_dry_action_trigger() const; - Trigger<> *get_fan_only_action_trigger() const; - Trigger<> *get_heat_action_trigger() const; - Trigger<> *get_supplemental_heat_action_trigger() const; - Trigger<> *get_idle_action_trigger() const; - Trigger<> *get_auto_mode_trigger() const; - Trigger<> *get_cool_mode_trigger() const; - Trigger<> *get_dry_mode_trigger() const; - Trigger<> *get_fan_only_mode_trigger() const; - Trigger<> *get_heat_mode_trigger() const; - Trigger<> *get_heat_cool_mode_trigger() const; - Trigger<> *get_off_mode_trigger() const; - Trigger<> *get_fan_mode_on_trigger() const; - Trigger<> *get_fan_mode_off_trigger() const; - Trigger<> *get_fan_mode_auto_trigger() const; - Trigger<> *get_fan_mode_low_trigger() const; - Trigger<> *get_fan_mode_medium_trigger() const; - Trigger<> *get_fan_mode_high_trigger() const; - Trigger<> *get_fan_mode_middle_trigger() const; - Trigger<> *get_fan_mode_focus_trigger() const; - Trigger<> *get_fan_mode_diffuse_trigger() const; - Trigger<> *get_fan_mode_quiet_trigger() const; - Trigger<> *get_swing_mode_both_trigger() const; - Trigger<> *get_swing_mode_horizontal_trigger() const; - Trigger<> *get_swing_mode_off_trigger() const; - Trigger<> *get_swing_mode_vertical_trigger() const; - Trigger<> *get_humidity_change_trigger() const; - Trigger<> *get_temperature_change_trigger() const; - Trigger<> *get_preset_change_trigger() const; - Trigger<> *get_humidity_control_dehumidify_action_trigger() const; - Trigger<> *get_humidity_control_humidify_action_trigger() const; - Trigger<> *get_humidity_control_off_action_trigger() const; + Trigger<> *get_cool_action_trigger(); + Trigger<> *get_supplemental_cool_action_trigger(); + Trigger<> *get_dry_action_trigger(); + Trigger<> *get_fan_only_action_trigger(); + Trigger<> *get_heat_action_trigger(); + Trigger<> *get_supplemental_heat_action_trigger(); + Trigger<> *get_idle_action_trigger(); + Trigger<> *get_auto_mode_trigger(); + Trigger<> *get_cool_mode_trigger(); + Trigger<> *get_dry_mode_trigger(); + Trigger<> *get_fan_only_mode_trigger(); + Trigger<> *get_heat_mode_trigger(); + Trigger<> *get_heat_cool_mode_trigger(); + Trigger<> *get_off_mode_trigger(); + Trigger<> *get_fan_mode_on_trigger(); + Trigger<> *get_fan_mode_off_trigger(); + Trigger<> *get_fan_mode_auto_trigger(); + Trigger<> *get_fan_mode_low_trigger(); + Trigger<> *get_fan_mode_medium_trigger(); + Trigger<> *get_fan_mode_high_trigger(); + Trigger<> *get_fan_mode_middle_trigger(); + Trigger<> *get_fan_mode_focus_trigger(); + Trigger<> *get_fan_mode_diffuse_trigger(); + Trigger<> *get_fan_mode_quiet_trigger(); + Trigger<> *get_swing_mode_both_trigger(); + Trigger<> *get_swing_mode_horizontal_trigger(); + Trigger<> *get_swing_mode_off_trigger(); + Trigger<> *get_swing_mode_vertical_trigger(); + Trigger<> *get_humidity_change_trigger(); + Trigger<> *get_temperature_change_trigger(); + Trigger<> *get_preset_change_trigger(); + Trigger<> *get_humidity_control_dehumidify_action_trigger(); + Trigger<> *get_humidity_control_humidify_action_trigger(); + Trigger<> *get_humidity_control_off_action_trigger(); /// Get current hysteresis values float cool_deadband(); float cool_overrun(); @@ -417,115 +417,65 @@ class ThermostatClimate : public climate::Climate, public Component { /// The sensor used for getting the current humidity sensor::Sensor *humidity_sensor_{nullptr}; - /// The trigger to call when the controller should switch to cooling action/mode. - /// - /// A null value for this attribute means that the controller has no cooling action - /// For example electric heat, where only heating (power on) and not-heating - /// (power off) is possible. - Trigger<> *cool_action_trigger_{nullptr}; - Trigger<> *supplemental_cool_action_trigger_{nullptr}; - Trigger<> *cool_mode_trigger_{nullptr}; + /// Trigger for cooling action/mode + Trigger<> cool_action_trigger_; + Trigger<> supplemental_cool_action_trigger_; + Trigger<> cool_mode_trigger_; - /// The trigger to call when the controller should switch to dry (dehumidification) mode. - /// - /// In dry mode, the controller is assumed to have both heating and cooling disabled, - /// although the system may use its cooling mechanism to achieve drying. - Trigger<> *dry_action_trigger_{nullptr}; - Trigger<> *dry_mode_trigger_{nullptr}; + /// Trigger for dry (dehumidification) mode + Trigger<> dry_action_trigger_; + Trigger<> dry_mode_trigger_; - /// The trigger to call when the controller should switch to heating action/mode. - /// - /// A null value for this attribute means that the controller has no heating action - /// For example window blinds, where only cooling (blinds closed) and not-cooling - /// (blinds open) is possible. - Trigger<> *heat_action_trigger_{nullptr}; - Trigger<> *supplemental_heat_action_trigger_{nullptr}; - Trigger<> *heat_mode_trigger_{nullptr}; + /// Trigger for heating action/mode + Trigger<> heat_action_trigger_; + Trigger<> supplemental_heat_action_trigger_; + Trigger<> heat_mode_trigger_; - /// The trigger to call when the controller should switch to heat/cool mode. - /// - /// In heat/cool mode, the controller will enable heating/cooling as necessary and switch - /// to idle when the temperature is within the thresholds/set points. - Trigger<> *heat_cool_mode_trigger_{nullptr}; + /// Trigger for heat/cool mode + Trigger<> heat_cool_mode_trigger_; - /// The trigger to call when the controller should switch to auto mode. - /// - /// In auto mode, the controller will enable heating/cooling as supported/necessary and switch - /// to idle when the temperature is within the thresholds/set points. - Trigger<> *auto_mode_trigger_{nullptr}; + /// Trigger for auto mode + Trigger<> auto_mode_trigger_; - /// The trigger to call when the controller should switch to idle action/off mode. - /// - /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. - Trigger<> *idle_action_trigger_{nullptr}; - Trigger<> *off_mode_trigger_{nullptr}; + /// Trigger for idle action/off mode + Trigger<> idle_action_trigger_; + Trigger<> off_mode_trigger_; - /// The trigger to call when the controller should switch to fan-only action/mode. - /// - /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. - /// The system should activate the fan only. - Trigger<> *fan_only_action_trigger_{nullptr}; - Trigger<> *fan_only_mode_trigger_{nullptr}; + /// Trigger for fan-only action/mode + Trigger<> fan_only_action_trigger_; + Trigger<> fan_only_mode_trigger_; - /// The trigger to call when the controller should switch on the fan. - Trigger<> *fan_mode_on_trigger_{nullptr}; + /// Fan mode triggers + Trigger<> fan_mode_on_trigger_; + Trigger<> fan_mode_off_trigger_; + Trigger<> fan_mode_auto_trigger_; + Trigger<> fan_mode_low_trigger_; + Trigger<> fan_mode_medium_trigger_; + Trigger<> fan_mode_high_trigger_; + Trigger<> fan_mode_middle_trigger_; + Trigger<> fan_mode_focus_trigger_; + Trigger<> fan_mode_diffuse_trigger_; + Trigger<> fan_mode_quiet_trigger_; - /// The trigger to call when the controller should switch off the fan. - Trigger<> *fan_mode_off_trigger_{nullptr}; + /// Swing mode triggers + Trigger<> swing_mode_both_trigger_; + Trigger<> swing_mode_off_trigger_; + Trigger<> swing_mode_horizontal_trigger_; + Trigger<> swing_mode_vertical_trigger_; - /// The trigger to call when the controller should switch the fan to "auto" mode. - Trigger<> *fan_mode_auto_trigger_{nullptr}; + /// Trigger for target humidity changes + Trigger<> humidity_change_trigger_; - /// The trigger to call when the controller should switch the fan to "low" speed. - Trigger<> *fan_mode_low_trigger_{nullptr}; + /// Trigger for target temperature changes + Trigger<> temperature_change_trigger_; - /// The trigger to call when the controller should switch the fan to "medium" speed. - Trigger<> *fan_mode_medium_trigger_{nullptr}; + /// Trigger for preset mode changes + Trigger<> preset_change_trigger_; - /// The trigger to call when the controller should switch the fan to "high" speed. - Trigger<> *fan_mode_high_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "middle" position. - Trigger<> *fan_mode_middle_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "focus" position. - Trigger<> *fan_mode_focus_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "diffuse" position. - Trigger<> *fan_mode_diffuse_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "quiet" position. - Trigger<> *fan_mode_quiet_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "both". - Trigger<> *swing_mode_both_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "off". - Trigger<> *swing_mode_off_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "horizontal". - Trigger<> *swing_mode_horizontal_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "vertical". - Trigger<> *swing_mode_vertical_trigger_{nullptr}; - - /// The trigger to call when the target humidity changes. - Trigger<> *humidity_change_trigger_{nullptr}; - - /// The trigger to call when the target temperature(s) change(es). - Trigger<> *temperature_change_trigger_{nullptr}; - - /// The trigger to call when the preset mode changes - Trigger<> *preset_change_trigger_{nullptr}; - - /// The trigger to call when dehumidification is required - Trigger<> *humidity_control_dehumidify_action_trigger_{nullptr}; - - /// The trigger to call when humidification is required - Trigger<> *humidity_control_humidify_action_trigger_{nullptr}; - - /// The trigger to call when (de)humidification should stop - Trigger<> *humidity_control_off_action_trigger_{nullptr}; + /// Humidity control triggers + Trigger<> humidity_control_dehumidify_action_trigger_; + Trigger<> humidity_control_humidify_action_trigger_; + Trigger<> humidity_control_off_action_trigger_; /// A reference to the trigger that was previously active. /// From bfeb4471784983af79c076d953ed6382df870176 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:43:16 +0100 Subject: [PATCH 011/251] [sx127x] Avoid heap allocation for packet trigger (#13698) --- esphome/components/sx127x/sx127x.cpp | 2 +- esphome/components/sx127x/sx127x.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 3185574b1a..caf68b6d51 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -300,7 +300,7 @@ void SX127x::call_listeners_(const std::vector &packet, float rssi, flo for (auto &listener : this->listeners_) { listener->on_packet(packet, rssi, snr); } - this->packet_trigger_->trigger(packet, rssi, snr); + this->packet_trigger_.trigger(packet, rssi, snr); } void SX127x::loop() { diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h index 0600b51201..be7b6d8d9f 100644 --- a/esphome/components/sx127x/sx127x.h +++ b/esphome/components/sx127x/sx127x.h @@ -83,7 +83,7 @@ class SX127x : public Component, void configure(); SX127xError transmit_packet(const std::vector &packet); void register_listener(SX127xListener *listener) { this->listeners_.push_back(listener); } - Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; + Trigger, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: void configure_fsk_ook_(); @@ -94,7 +94,7 @@ class SX127x : public Component, void write_register_(uint8_t reg, uint8_t value); void call_listeners_(const std::vector &packet, float rssi, float snr); uint8_t read_register_(uint8_t reg); - Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + Trigger, float, float> packet_trigger_; std::vector listeners_; std::vector packet_; std::vector sync_value_; From 75ee9a718ac7dd02b445648847229bfcbffe6642 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:43:30 +0100 Subject: [PATCH 012/251] [sx126x] Avoid heap allocation for packet trigger (#13699) --- esphome/components/sx126x/sx126x.cpp | 2 +- esphome/components/sx126x/sx126x.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index 707d6f1fbf..64cd24b171 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -343,7 +343,7 @@ void SX126x::call_listeners_(const std::vector &packet, float rssi, flo for (auto &listener : this->listeners_) { listener->on_packet(packet, rssi, snr); } - this->packet_trigger_->trigger(packet, rssi, snr); + this->packet_trigger_.trigger(packet, rssi, snr); } void SX126x::loop() { diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index 850d7d4c77..a758d63795 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -97,7 +97,7 @@ class SX126x : public Component, void configure(); SX126xError transmit_packet(const std::vector &packet); void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); } - Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; + Trigger, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: void configure_fsk_ook_(); @@ -111,7 +111,7 @@ class SX126x : public Component, void read_register_(uint16_t reg, uint8_t *data, uint8_t size); void call_listeners_(const std::vector &packet, float rssi, float snr); void wait_busy_(); - Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + Trigger, float, float> packet_trigger_; std::vector listeners_; std::vector packet_; std::vector sync_value_; From 78ed898f0bf521a2bea95461640c167aa6ac5715 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:43:52 +0100 Subject: [PATCH 013/251] [current_based] Avoid heap allocation for cover triggers (#13700) --- .../current_based/current_based_cover.cpp | 10 +++++----- .../current_based/current_based_cover.h | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index cb3f65c9cd..402cf9fee7 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -66,7 +66,7 @@ void CurrentBasedCover::loop() { if (this->current_operation == COVER_OPERATION_OPENING) { if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction this->direction_idle_(); - this->malfunction_trigger_->trigger(); + this->malfunction_trigger_.trigger(); ESP_LOGI(TAG, "'%s' - Malfunction detected during opening. Current flow detected in close circuit", this->name_.c_str()); } else if (this->is_opening_blocked_()) { // Blocked @@ -87,7 +87,7 @@ void CurrentBasedCover::loop() { } else if (this->current_operation == COVER_OPERATION_CLOSING) { if (this->malfunction_detection_ && this->is_opening_()) { // Malfunction this->direction_idle_(); - this->malfunction_trigger_->trigger(); + this->malfunction_trigger_.trigger(); ESP_LOGI(TAG, "'%s' - Malfunction detected during closing. Current flow detected in open circuit", this->name_.c_str()); } else if (this->is_closing_blocked_()) { // Blocked @@ -221,15 +221,15 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index b172e762b0..f7993f1550 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -16,9 +16,9 @@ class CurrentBasedCover : public cover::Cover, public Component { void dump_config() override; float get_setup_priority() const override; - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } - Trigger<> *get_open_trigger() const { return this->open_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; } void set_open_moving_current_threshold(float open_moving_current_threshold) { this->open_moving_current_threshold_ = open_moving_current_threshold; @@ -28,7 +28,7 @@ class CurrentBasedCover : public cover::Cover, public Component { } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; } void set_close_moving_current_threshold(float close_moving_current_threshold) { this->close_moving_current_threshold_ = close_moving_current_threshold; @@ -44,7 +44,7 @@ class CurrentBasedCover : public cover::Cover, public Component { void set_malfunction_detection(bool malfunction_detection) { this->malfunction_detection_ = malfunction_detection; } void set_start_sensing_delay(uint32_t start_sensing_delay) { this->start_sensing_delay_ = start_sensing_delay; } - Trigger<> *get_malfunction_trigger() const { return this->malfunction_trigger_; } + Trigger<> *get_malfunction_trigger() { return &this->malfunction_trigger_; } cover::CoverTraits get_traits() override; @@ -64,23 +64,23 @@ class CurrentBasedCover : public cover::Cover, public Component { void recompute_position_(); - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; sensor::Sensor *open_sensor_{nullptr}; - Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; float open_moving_current_threshold_; float open_obstacle_current_threshold_{FLT_MAX}; uint32_t open_duration_; sensor::Sensor *close_sensor_{nullptr}; - Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> close_trigger_; float close_moving_current_threshold_; float close_obstacle_current_threshold_{FLT_MAX}; uint32_t close_duration_; uint32_t max_duration_{UINT32_MAX}; bool malfunction_detection_{true}; - Trigger<> *malfunction_trigger_{new Trigger<>()}; + Trigger<> malfunction_trigger_; uint32_t start_sensing_delay_; float obstacle_rollback_; From 01ffeba2c2930e633c6faf14d993cb040db97e85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:44:08 +0100 Subject: [PATCH 014/251] [api] Avoid heap allocation for homeassistant action triggers (#13695) --- .../components/api/homeassistant_service.h | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 9bffe18764..8ee23c75fe 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -136,12 +136,10 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response = true; } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - Trigger *get_success_trigger_with_response() const { - return this->success_trigger_with_response_; - } + Trigger *get_success_trigger_with_response() { return &this->success_trigger_with_response_; } #endif - Trigger *get_success_trigger() const { return this->success_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger *get_success_trigger() { return &this->success_trigger_; } + Trigger *get_error_trigger() { return &this->error_trigger_; } #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES void play(const Ts &...x) override { @@ -187,14 +185,14 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response) { - this->success_trigger_with_response_->trigger(response.get_json(), args...); + this->success_trigger_with_response_.trigger(response.get_json(), args...); } else #endif { - this->success_trigger_->trigger(args...); + this->success_trigger_.trigger(args...); } } else { - this->error_trigger_->trigger(response.get_error_message(), args...); + this->error_trigger_.trigger(response.get_error_message(), args...); } }, captured_args); @@ -251,10 +249,10 @@ template class HomeAssistantServiceCallAction : public Action response_template_{""}; - Trigger *success_trigger_with_response_ = new Trigger(); + Trigger success_trigger_with_response_; #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - Trigger *success_trigger_ = new Trigger(); - Trigger *error_trigger_ = new Trigger(); + Trigger success_trigger_; + Trigger error_trigger_; #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES struct Flags { From 8a8c1290dbfc87639bc4c297b94377857aa2d077 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:45:01 +0100 Subject: [PATCH 015/251] [endstop] Avoid heap allocation for cover triggers (#13702) --- esphome/components/endstop/endstop_cover.cpp | 6 +++--- esphome/components/endstop/endstop_cover.h | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 2c281ea2e6..e28f024136 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -141,15 +141,15 @@ void EndstopCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index 6ae15de8c1..6f72b2b805 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -15,9 +15,9 @@ class EndstopCover : public cover::Cover, public Component { void dump_config() override; float get_setup_priority() const override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } void set_open_endstop(binary_sensor::BinarySensor *open_endstop) { this->open_endstop_ = open_endstop; } void set_close_endstop(binary_sensor::BinarySensor *close_endstop) { this->close_endstop_ = close_endstop; } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } @@ -39,11 +39,11 @@ class EndstopCover : public cover::Cover, public Component { binary_sensor::BinarySensor *open_endstop_; binary_sensor::BinarySensor *close_endstop_; - Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; uint32_t open_duration_; - Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> close_trigger_; uint32_t close_duration_; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; uint32_t max_duration_{UINT32_MAX}; Trigger<> *prev_command_trigger_{nullptr}; From d49d8095dfdb24c8e54a3e7f12e1a7e6ffe0f8b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:46:41 +0100 Subject: [PATCH 016/251] [template] Avoid heap allocation for valve triggers (#13697) --- .../template/valve/template_valve.cpp | 35 ++++++++----------- .../template/valve/template_valve.h | 20 +++++------ 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 4e772f9253..2817e1a132 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -7,12 +7,7 @@ using namespace esphome::valve; static const char *const TAG = "template.valve"; -TemplateValve::TemplateValve() - : open_trigger_(new Trigger<>()), - close_trigger_(new Trigger<>), - stop_trigger_(new Trigger<>()), - toggle_trigger_(new Trigger<>()), - position_trigger_(new Trigger()) {} +TemplateValve::TemplateValve() = default; void TemplateValve::setup() { switch (this->restore_mode_) { @@ -56,10 +51,10 @@ void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimi void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } -Trigger<> *TemplateValve::get_close_trigger() const { return this->close_trigger_; } -Trigger<> *TemplateValve::get_stop_trigger() const { return this->stop_trigger_; } -Trigger<> *TemplateValve::get_toggle_trigger() const { return this->toggle_trigger_; } +Trigger<> *TemplateValve::get_open_trigger() { return &this->open_trigger_; } +Trigger<> *TemplateValve::get_close_trigger() { return &this->close_trigger_; } +Trigger<> *TemplateValve::get_stop_trigger() { return &this->stop_trigger_; } +Trigger<> *TemplateValve::get_toggle_trigger() { return &this->toggle_trigger_; } void TemplateValve::dump_config() { LOG_VALVE("", "Template Valve", this); @@ -72,14 +67,14 @@ void TemplateValve::dump_config() { void TemplateValve::control(const ValveCall &call) { if (call.get_stop()) { this->stop_prev_trigger_(); - this->stop_trigger_->trigger(); - this->prev_command_trigger_ = this->stop_trigger_; + this->stop_trigger_.trigger(); + this->prev_command_trigger_ = &this->stop_trigger_; this->publish_state(); } if (call.get_toggle().has_value()) { this->stop_prev_trigger_(); - this->toggle_trigger_->trigger(); - this->prev_command_trigger_ = this->toggle_trigger_; + this->toggle_trigger_.trigger(); + this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } if (call.get_position().has_value()) { @@ -87,13 +82,13 @@ void TemplateValve::control(const ValveCall &call) { this->stop_prev_trigger_(); if (pos == VALVE_OPEN) { - this->open_trigger_->trigger(); - this->prev_command_trigger_ = this->open_trigger_; + this->open_trigger_.trigger(); + this->prev_command_trigger_ = &this->open_trigger_; } else if (pos == VALVE_CLOSED) { - this->close_trigger_->trigger(); - this->prev_command_trigger_ = this->close_trigger_; + this->close_trigger_.trigger(); + this->prev_command_trigger_ = &this->close_trigger_; } else { - this->position_trigger_->trigger(pos); + this->position_trigger_.trigger(pos); } if (this->optimistic_) { @@ -113,7 +108,7 @@ ValveTraits TemplateValve::get_traits() { return traits; } -Trigger *TemplateValve::get_position_trigger() const { return this->position_trigger_; } +Trigger *TemplateValve::get_position_trigger() { return &this->position_trigger_; } void TemplateValve::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateValve::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 4205682a2a..76c4630aa0 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -18,11 +18,11 @@ class TemplateValve final : public valve::Valve, public Component { TemplateValve(); template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } - Trigger<> *get_open_trigger() const; - Trigger<> *get_close_trigger() const; - Trigger<> *get_stop_trigger() const; - Trigger<> *get_toggle_trigger() const; - Trigger *get_position_trigger() const; + Trigger<> *get_open_trigger(); + Trigger<> *get_close_trigger(); + Trigger<> *get_stop_trigger(); + Trigger<> *get_toggle_trigger(); + Trigger *get_position_trigger(); void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void set_has_stop(bool has_stop); @@ -45,14 +45,14 @@ class TemplateValve final : public valve::Valve, public Component { TemplateLambda state_f_; bool assumed_state_{false}; bool optimistic_{false}; - Trigger<> *open_trigger_; - Trigger<> *close_trigger_; + Trigger<> open_trigger_; + Trigger<> close_trigger_; bool has_stop_{false}; bool has_toggle_{false}; - Trigger<> *stop_trigger_; - Trigger<> *toggle_trigger_; + Trigger<> stop_trigger_; + Trigger<> toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; - Trigger *position_trigger_; + Trigger position_trigger_; bool has_position_{false}; }; From 2f0abd5c3f5e9b1ae8bffe62a8ad320b7ad32a2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:46:55 +0100 Subject: [PATCH 017/251] [template] Avoid heap allocation for cover triggers (#13696) --- .../template/cover/template_cover.cpp | 40 ++++++++----------- .../template/cover/template_cover.h | 24 +++++------ 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 9c8a8fc9bc..7f5d68623f 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -7,13 +7,7 @@ using namespace esphome::cover; static const char *const TAG = "template.cover"; -TemplateCover::TemplateCover() - : open_trigger_(new Trigger<>()), - close_trigger_(new Trigger<>), - stop_trigger_(new Trigger<>()), - toggle_trigger_(new Trigger<>()), - position_trigger_(new Trigger()), - tilt_trigger_(new Trigger()) {} +TemplateCover::TemplateCover() = default; void TemplateCover::setup() { switch (this->restore_mode_) { case COVER_NO_RESTORE: @@ -62,22 +56,22 @@ void TemplateCover::loop() { void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } -Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } -Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } -Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } +Trigger<> *TemplateCover::get_open_trigger() { return &this->open_trigger_; } +Trigger<> *TemplateCover::get_close_trigger() { return &this->close_trigger_; } +Trigger<> *TemplateCover::get_stop_trigger() { return &this->stop_trigger_; } +Trigger<> *TemplateCover::get_toggle_trigger() { return &this->toggle_trigger_; } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::control(const CoverCall &call) { if (call.get_stop()) { this->stop_prev_trigger_(); - this->stop_trigger_->trigger(); - this->prev_command_trigger_ = this->stop_trigger_; + this->stop_trigger_.trigger(); + this->prev_command_trigger_ = &this->stop_trigger_; this->publish_state(); } if (call.get_toggle().has_value()) { this->stop_prev_trigger_(); - this->toggle_trigger_->trigger(); - this->prev_command_trigger_ = this->toggle_trigger_; + this->toggle_trigger_.trigger(); + this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } if (call.get_position().has_value()) { @@ -85,13 +79,13 @@ void TemplateCover::control(const CoverCall &call) { this->stop_prev_trigger_(); if (pos == COVER_OPEN) { - this->open_trigger_->trigger(); - this->prev_command_trigger_ = this->open_trigger_; + this->open_trigger_.trigger(); + this->prev_command_trigger_ = &this->open_trigger_; } else if (pos == COVER_CLOSED) { - this->close_trigger_->trigger(); - this->prev_command_trigger_ = this->close_trigger_; + this->close_trigger_.trigger(); + this->prev_command_trigger_ = &this->close_trigger_; } else { - this->position_trigger_->trigger(pos); + this->position_trigger_.trigger(pos); } if (this->optimistic_) { @@ -101,7 +95,7 @@ void TemplateCover::control(const CoverCall &call) { if (call.get_tilt().has_value()) { auto tilt = *call.get_tilt(); - this->tilt_trigger_->trigger(tilt); + this->tilt_trigger_.trigger(tilt); if (this->optimistic_) { this->tilt = tilt; @@ -119,8 +113,8 @@ CoverTraits TemplateCover::get_traits() { traits.set_supports_tilt(this->has_tilt_); return traits; } -Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } -Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } +Trigger *TemplateCover::get_position_trigger() { return &this->position_trigger_; } +Trigger *TemplateCover::get_tilt_trigger() { return &this->tilt_trigger_; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 9c4a787283..20c092cda7 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -19,12 +19,12 @@ class TemplateCover final : public cover::Cover, public Component { template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } template void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward(f)); } - Trigger<> *get_open_trigger() const; - Trigger<> *get_close_trigger() const; - Trigger<> *get_stop_trigger() const; - Trigger<> *get_toggle_trigger() const; - Trigger *get_position_trigger() const; - Trigger *get_tilt_trigger() const; + Trigger<> *get_open_trigger(); + Trigger<> *get_close_trigger(); + Trigger<> *get_stop_trigger(); + Trigger<> *get_toggle_trigger(); + Trigger *get_position_trigger(); + Trigger *get_tilt_trigger(); void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void set_has_stop(bool has_stop); @@ -49,16 +49,16 @@ class TemplateCover final : public cover::Cover, public Component { TemplateLambda tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; - Trigger<> *open_trigger_; - Trigger<> *close_trigger_; + Trigger<> open_trigger_; + Trigger<> close_trigger_; bool has_stop_{false}; bool has_toggle_{false}; - Trigger<> *stop_trigger_; - Trigger<> *toggle_trigger_; + Trigger<> stop_trigger_; + Trigger<> toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; - Trigger *position_trigger_; + Trigger position_trigger_; bool has_position_{false}; - Trigger *tilt_trigger_; + Trigger tilt_trigger_; bool has_tilt_{false}; }; From 7d717a78dc0f68cfd6d47135dfa896ec65021e86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:47:21 +0100 Subject: [PATCH 018/251] [template] Avoid heap allocation for number set trigger (#13694) --- esphome/components/template/number/template_number.cpp | 2 +- esphome/components/template/number/template_number.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 885265cf5d..64c2deb281 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -36,7 +36,7 @@ void TemplateNumber::update() { } void TemplateNumber::control(float value) { - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) this->publish_state(value); diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 42c27fc3ca..e51e858ccf 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -17,7 +17,7 @@ class TemplateNumber final : public number::Number, public PollingComponent { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { optimistic_ = optimistic; } void set_initial_value(float initial_value) { initial_value_ = initial_value; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } @@ -27,7 +27,7 @@ class TemplateNumber final : public number::Number, public PollingComponent { bool optimistic_{false}; float initial_value_{NAN}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; From e420964b939b78edd7475d7ad89eaeef37f22020 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:47:34 +0100 Subject: [PATCH 019/251] [template.switch] Avoid heap allocation for triggers (#13691) --- .../components/template/switch/template_switch.cpp | 14 +++++++------- .../components/template/switch/template_switch.h | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index cfa8798e75..05288b2d4e 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -5,7 +5,7 @@ namespace esphome::template_ { static const char *const TAG = "template.switch"; -TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} +TemplateSwitch::TemplateSwitch() = default; void TemplateSwitch::loop() { auto s = this->f_(); @@ -19,11 +19,11 @@ void TemplateSwitch::write_state(bool state) { } if (state) { - this->prev_trigger_ = this->turn_on_trigger_; - this->turn_on_trigger_->trigger(); + this->prev_trigger_ = &this->turn_on_trigger_; + this->turn_on_trigger_.trigger(); } else { - this->prev_trigger_ = this->turn_off_trigger_; - this->turn_off_trigger_->trigger(); + this->prev_trigger_ = &this->turn_off_trigger_; + this->turn_off_trigger_.trigger(); } if (this->optimistic_) @@ -32,8 +32,8 @@ void TemplateSwitch::write_state(bool state) { void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } -Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } -Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } +Trigger<> *TemplateSwitch::get_turn_on_trigger() { return &this->turn_on_trigger_; } +Trigger<> *TemplateSwitch::get_turn_off_trigger() { return &this->turn_off_trigger_; } void TemplateSwitch::setup() { if (!this->f_.has_value()) this->disable_loop(); diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 91b7b396f6..1714b4f72b 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -15,8 +15,8 @@ class TemplateSwitch final : public switch_::Switch, public Component { void dump_config() override; template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } - Trigger<> *get_turn_on_trigger() const; - Trigger<> *get_turn_off_trigger() const; + Trigger<> *get_turn_on_trigger(); + Trigger<> *get_turn_off_trigger(); void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void loop() override; @@ -31,9 +31,9 @@ class TemplateSwitch final : public switch_::Switch, public Component { TemplateLambda f_; bool optimistic_{false}; bool assumed_state_{false}; - Trigger<> *turn_on_trigger_; - Trigger<> *turn_off_trigger_; - Trigger<> *prev_trigger_{nullptr}; + Trigger<> turn_on_trigger_; + Trigger<> turn_off_trigger_; + Trigger<> *prev_trigger_{nullptr}; // Points to one of the above }; } // namespace esphome::template_ From 4ab552d7504fb2070b3b7eb58ae9eafe7e8c32ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:47:49 +0100 Subject: [PATCH 020/251] [http_request] Avoid heap allocation for triggers (#13690) --- .../components/http_request/http_request.h | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index fb39ca504c..79098a6b72 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -332,13 +332,13 @@ template class HttpRequestSendAction : public Action { void set_json(std::function json_func) { this->json_func_ = json_func; } #ifdef USE_HTTP_REQUEST_RESPONSE - Trigger, std::string &, Ts...> *get_success_trigger_with_response() const { - return this->success_trigger_with_response_; + Trigger, std::string &, Ts...> *get_success_trigger_with_response() { + return &this->success_trigger_with_response_; } #endif - Trigger, Ts...> *get_success_trigger() const { return this->success_trigger_; } + Trigger, Ts...> *get_success_trigger() { return &this->success_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger *get_error_trigger() { return &this->error_trigger_; } void set_max_response_buffer_size(size_t max_response_buffer_size) { this->max_response_buffer_size_ = max_response_buffer_size; @@ -372,7 +372,7 @@ template class HttpRequestSendAction : public Action { auto captured_args = std::make_tuple(x...); if (container == nullptr) { - std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); }, + std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); }, captured_args); return; } @@ -406,14 +406,14 @@ template class HttpRequestSendAction : public Action { } std::apply( [this, &container, &response_body](Ts... captured_args_inner) { - this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...); + this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...); }, captured_args); } else #endif { std::apply([this, &container]( - Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); }, + Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); }, captured_args); } container->end(); @@ -433,12 +433,10 @@ template class HttpRequestSendAction : public Action { std::map> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE - Trigger, std::string &, Ts...> *success_trigger_with_response_ = - new Trigger, std::string &, Ts...>(); + Trigger, std::string &, Ts...> success_trigger_with_response_; #endif - Trigger, Ts...> *success_trigger_ = - new Trigger, Ts...>(); - Trigger *error_trigger_ = new Trigger(); + Trigger, Ts...> success_trigger_; + Trigger error_trigger_; size_t max_response_buffer_size_{SIZE_MAX}; }; From 652c02b9abfacf6b1ad3d161948e8b9ccaf57ae1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:49:46 +0100 Subject: [PATCH 021/251] [bang_bang] Avoid heap allocation for climate triggers (#13701) --- .../components/bang_bang/bang_bang_climate.cpp | 15 +++++++-------- esphome/components/bang_bang/bang_bang_climate.h | 16 ++++++---------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index f26377a38a..6871e9df5d 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -6,8 +6,7 @@ namespace bang_bang { static const char *const TAG = "bang_bang.climate"; -BangBangClimate::BangBangClimate() - : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} +BangBangClimate::BangBangClimate() = default; void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { @@ -160,13 +159,13 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - trig = this->idle_trigger_; + trig = &this->idle_trigger_; break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_trigger_; + trig = &this->cool_trigger_; break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_trigger_; + trig = &this->heat_trigger_; break; default: trig = nullptr; @@ -204,9 +203,9 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } -Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } -Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } -Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } +Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; } +Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; } +Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 2e7da93a07..d0ddef2848 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -30,9 +30,9 @@ class BangBangClimate : public climate::Climate, public Component { void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); - Trigger<> *get_idle_trigger() const; - Trigger<> *get_cool_trigger() const; - Trigger<> *get_heat_trigger() const; + Trigger<> *get_idle_trigger(); + Trigger<> *get_cool_trigger(); + Trigger<> *get_heat_trigger(); protected: /// Override control to change settings of the climate device. @@ -57,17 +57,13 @@ class BangBangClimate : public climate::Climate, public Component { * * In idle mode, the controller is assumed to have both heating and cooling disabled. */ - Trigger<> *idle_trigger_{nullptr}; + Trigger<> idle_trigger_; /** The trigger to call when the controller should switch to cooling mode. */ - Trigger<> *cool_trigger_{nullptr}; + Trigger<> cool_trigger_; /** The trigger to call when the controller should switch to heating mode. - * - * A null value for this attribute means that the controller has no heating action - * For example window blinds, where only cooling (blinds closed) and not-cooling - * (blinds open) is possible. */ - Trigger<> *heat_trigger_{nullptr}; + Trigger<> heat_trigger_; /** A reference to the trigger that was previously active. * * This is so that the previous trigger can be stopped before enabling a new one. From 8791c24072da87ac2067f3b68e489cd8e83ed0f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:50:01 +0100 Subject: [PATCH 022/251] [api] Avoid heap allocation for client connected/disconnected triggers (#13688) --- esphome/components/api/api_server.cpp | 2 +- esphome/components/api/api_server.h | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ed97c3b9a2..c56449455d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -211,7 +211,7 @@ void APIServer::loop() { #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER // Fire trigger after client is removed so api.connected reflects the true state - this->client_disconnected_trigger_->trigger(client_name, client_peername); + this->client_disconnected_trigger_.trigger(client_name, client_peername); #endif // Don't increment client_index since we need to process the swapped element } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 93421ef801..6ab3cdc576 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -227,12 +227,10 @@ class APIServer : public Component, #endif #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger *get_client_connected_trigger() { return &this->client_connected_trigger_; } #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *get_client_disconnected_trigger() const { - return this->client_disconnected_trigger_; - } + Trigger *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; } #endif protected: @@ -253,10 +251,10 @@ class APIServer : public Component, // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *client_connected_trigger_ = new Trigger(); + Trigger client_connected_trigger_; #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *client_disconnected_trigger_ = new Trigger(); + Trigger client_disconnected_trigger_; #endif // 4-byte aligned types From 09b76d5e4a4405dbf9fe2f7ffb0a93af544dcc83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:50:16 +0100 Subject: [PATCH 023/251] [voice_assistant] Avoid heap allocation for triggers (#13689) --- .../voice_assistant/voice_assistant.cpp | 58 ++++++------ .../voice_assistant/voice_assistant.h | 92 +++++++++---------- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index e2516d5fb8..7f5fbe62e1 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -197,7 +197,7 @@ void VoiceAssistant::loop() { switch (this->state_) { case State::IDLE: { if (this->continuous_ && this->desired_state_ == State::IDLE) { - this->idle_trigger_->trigger(); + this->idle_trigger_.trigger(); this->set_state_(State::START_MICROPHONE, State::START_PIPELINE); } else { this->deallocate_buffers_(); @@ -254,7 +254,7 @@ void VoiceAssistant::loop() { if (this->api_client_ == nullptr || !this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE)) { ESP_LOGW(TAG, "Could not request start"); - this->error_trigger_->trigger("not-connected", "Could not request start"); + this->error_trigger_.trigger("not-connected", "Could not request start"); this->continuous_ = false; this->set_state_(State::IDLE, State::IDLE); break; @@ -384,7 +384,7 @@ void VoiceAssistant::loop() { this->wait_for_stream_end_ = false; this->stream_ended_ = false; - this->tts_stream_end_trigger_->trigger(); + this->tts_stream_end_trigger_.trigger(); } #endif if (this->continue_conversation_) { @@ -425,7 +425,7 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr return; } this->api_client_ = nullptr; - this->client_disconnected_trigger_->trigger(); + this->client_disconnected_trigger_.trigger(); return; } @@ -440,7 +440,7 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr } this->api_client_ = client; - this->client_connected_trigger_->trigger(); + this->client_connected_trigger_.trigger(); } static const LogString *voice_assistant_state_to_string(State state) { @@ -491,7 +491,7 @@ void VoiceAssistant::set_state_(State state, State desired_state) { void VoiceAssistant::failed_to_start() { ESP_LOGE(TAG, "Failed to start server. See Home Assistant logs for more details."); - this->error_trigger_->trigger("failed-to-start", "Failed to start server. See Home Assistant logs for more details."); + this->error_trigger_.trigger("failed-to-start", "Failed to start server. See Home Assistant logs for more details."); this->set_state_(State::STOP_MICROPHONE, State::IDLE); } @@ -637,18 +637,18 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } #endif - this->defer([this]() { this->start_trigger_->trigger(); }); + this->defer([this]() { this->start_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_END: { ESP_LOGD(TAG, "Wake word detected"); - this->defer([this]() { this->wake_word_detected_trigger_->trigger(); }); + this->defer([this]() { this->wake_word_detected_trigger_.trigger(); }); break; } case api::enums::VOICE_ASSISTANT_STT_START: ESP_LOGD(TAG, "STT started"); - this->defer([this]() { this->listening_trigger_->trigger(); }); + this->defer([this]() { this->listening_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_END: { std::string text; @@ -665,12 +665,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { text += "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); - this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); + this->defer([this, text]() { this->stt_end_trigger_.trigger(text); }); break; } case api::enums::VOICE_ASSISTANT_INTENT_START: ESP_LOGD(TAG, "Intent started"); - this->defer([this]() { this->intent_start_trigger_->trigger(); }); + this->defer([this]() { this->intent_start_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { ESP_LOGD(TAG, "Intent progress"); @@ -693,7 +693,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } #endif - this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); + this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_.trigger(tts_url_for_trigger); }); break; } case api::enums::VOICE_ASSISTANT_INTENT_END: { @@ -704,7 +704,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->continue_conversation_ = (arg.value == "1"); } } - this->defer([this]() { this->intent_end_trigger_->trigger(); }); + this->defer([this]() { this->intent_end_trigger_.trigger(); }); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -724,7 +724,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { - this->tts_start_trigger_->trigger(text); + this->tts_start_trigger_.trigger(text); #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { this->speaker_->start(); @@ -756,7 +756,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage #endif - this->tts_end_trigger_->trigger(url); + this->tts_end_trigger_.trigger(url); }); State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; if (new_state != this->state_) { @@ -776,7 +776,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { // No TTS start event ("nevermind") this->set_state_(State::IDLE, State::IDLE); } - this->defer([this]() { this->end_trigger_->trigger(); }); + this->defer([this]() { this->end_trigger_.trigger(); }); break; } case api::enums::VOICE_ASSISTANT_ERROR: { @@ -796,7 +796,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. this->defer([this, code, message]() { this->request_stop(); - this->error_trigger_->trigger(code, message); + this->error_trigger_.trigger(code, message); }); return; } @@ -805,7 +805,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->signal_stop_(); this->set_state_(State::STOP_MICROPHONE, State::IDLE); } - this->defer([this, code, message]() { this->error_trigger_->trigger(code, message); }); + this->defer([this, code, message]() { this->error_trigger_.trigger(code, message); }); break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { @@ -813,7 +813,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (this->speaker_ != nullptr) { this->wait_for_stream_end_ = true; ESP_LOGD(TAG, "TTS stream start"); - this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); + this->defer([this] { this->tts_stream_start_trigger_.trigger(); }); } #endif break; @@ -829,12 +829,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_STT_VAD_START: ESP_LOGD(TAG, "Starting STT by VAD"); - this->defer([this]() { this->stt_vad_start_trigger_->trigger(); }); + this->defer([this]() { this->stt_vad_start_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_VAD_END: ESP_LOGD(TAG, "STT by VAD end"); this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); - this->defer([this]() { this->stt_vad_end_trigger_->trigger(); }); + this->defer([this]() { this->stt_vad_end_trigger_.trigger(); }); break; default: ESP_LOGD(TAG, "Unhandled event type: %" PRId32, msg.event_type); @@ -876,17 +876,17 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_TIMER_STARTED: - this->timer_started_trigger_->trigger(timer); + this->timer_started_trigger_.trigger(timer); break; case api::enums::VOICE_ASSISTANT_TIMER_UPDATED: - this->timer_updated_trigger_->trigger(timer); + this->timer_updated_trigger_.trigger(timer); break; case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED: - this->timer_cancelled_trigger_->trigger(timer); + this->timer_cancelled_trigger_.trigger(timer); this->timers_.erase(timer.id); break; case api::enums::VOICE_ASSISTANT_TIMER_FINISHED: - this->timer_finished_trigger_->trigger(timer); + this->timer_finished_trigger_.trigger(timer); this->timers_.erase(timer.id); break; } @@ -910,13 +910,13 @@ void VoiceAssistant::timer_tick_() { } res.push_back(timer); } - this->timer_tick_trigger_->trigger(res); + this->timer_tick_trigger_.trigger(res); } void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) { #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { - this->tts_start_trigger_->trigger(msg.text); + this->tts_start_trigger_.trigger(msg.text); this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; @@ -939,8 +939,8 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE); } - this->tts_end_trigger_->trigger(msg.media_id); - this->end_trigger_->trigger(); + this->tts_end_trigger_.trigger(msg.media_id); + this->end_trigger_.trigger(); } #endif } diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index d61a8fbbc1..2a5f3a55a7 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -195,38 +195,38 @@ class VoiceAssistant : public Component { void set_conversation_timeout(uint32_t conversation_timeout) { this->conversation_timeout_ = conversation_timeout; } void reset_conversation_id(); - Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } - Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } - Trigger *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } - Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } - Trigger<> *get_end_trigger() const { return this->end_trigger_; } - Trigger<> *get_start_trigger() const { return this->start_trigger_; } - Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } - Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } + Trigger<> *get_intent_end_trigger() { return &this->intent_end_trigger_; } + Trigger<> *get_intent_start_trigger() { return &this->intent_start_trigger_; } + Trigger *get_intent_progress_trigger() { return &this->intent_progress_trigger_; } + Trigger<> *get_listening_trigger() { return &this->listening_trigger_; } + Trigger<> *get_end_trigger() { return &this->end_trigger_; } + Trigger<> *get_start_trigger() { return &this->start_trigger_; } + Trigger<> *get_stt_vad_end_trigger() { return &this->stt_vad_end_trigger_; } + Trigger<> *get_stt_vad_start_trigger() { return &this->stt_vad_start_trigger_; } #ifdef USE_SPEAKER - Trigger<> *get_tts_stream_start_trigger() const { return this->tts_stream_start_trigger_; } - Trigger<> *get_tts_stream_end_trigger() const { return this->tts_stream_end_trigger_; } + Trigger<> *get_tts_stream_start_trigger() { return &this->tts_stream_start_trigger_; } + Trigger<> *get_tts_stream_end_trigger() { return &this->tts_stream_end_trigger_; } #endif - Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } - Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } - Trigger<> *get_idle_trigger() const { return this->idle_trigger_; } + Trigger<> *get_wake_word_detected_trigger() { return &this->wake_word_detected_trigger_; } + Trigger *get_stt_end_trigger() { return &this->stt_end_trigger_; } + Trigger *get_tts_end_trigger() { return &this->tts_end_trigger_; } + Trigger *get_tts_start_trigger() { return &this->tts_start_trigger_; } + Trigger *get_error_trigger() { return &this->error_trigger_; } + Trigger<> *get_idle_trigger() { return &this->idle_trigger_; } - Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } - Trigger<> *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } + Trigger<> *get_client_connected_trigger() { return &this->client_connected_trigger_; } + Trigger<> *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; } void client_subscription(api::APIConnection *client, bool subscribe); api::APIConnection *get_api_connection() const { return this->api_client_; } void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; } - Trigger *get_timer_started_trigger() const { return this->timer_started_trigger_; } - Trigger *get_timer_updated_trigger() const { return this->timer_updated_trigger_; } - Trigger *get_timer_cancelled_trigger() const { return this->timer_cancelled_trigger_; } - Trigger *get_timer_finished_trigger() const { return this->timer_finished_trigger_; } - Trigger> *get_timer_tick_trigger() const { return this->timer_tick_trigger_; } + Trigger *get_timer_started_trigger() { return &this->timer_started_trigger_; } + Trigger *get_timer_updated_trigger() { return &this->timer_updated_trigger_; } + Trigger *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; } + Trigger *get_timer_finished_trigger() { return &this->timer_finished_trigger_; } + Trigger> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; } void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; } const std::unordered_map &get_timers() const { return this->timers_; } @@ -243,37 +243,37 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; - Trigger<> *intent_end_trigger_ = new Trigger<>(); - Trigger<> *intent_start_trigger_ = new Trigger<>(); - Trigger<> *listening_trigger_ = new Trigger<>(); - Trigger<> *end_trigger_ = new Trigger<>(); - Trigger<> *start_trigger_ = new Trigger<>(); - Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); - Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); + Trigger<> intent_end_trigger_; + Trigger<> intent_start_trigger_; + Trigger<> listening_trigger_; + Trigger<> end_trigger_; + Trigger<> start_trigger_; + Trigger<> stt_vad_start_trigger_; + Trigger<> stt_vad_end_trigger_; #ifdef USE_SPEAKER - Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); - Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); + Trigger<> tts_stream_start_trigger_; + Trigger<> tts_stream_end_trigger_; #endif - Trigger *intent_progress_trigger_ = new Trigger(); - Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); - Trigger *stt_end_trigger_ = new Trigger(); - Trigger *tts_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); - Trigger *error_trigger_ = new Trigger(); - Trigger<> *idle_trigger_ = new Trigger<>(); + Trigger intent_progress_trigger_; + Trigger<> wake_word_detected_trigger_; + Trigger stt_end_trigger_; + Trigger tts_end_trigger_; + Trigger tts_start_trigger_; + Trigger error_trigger_; + Trigger<> idle_trigger_; - Trigger<> *client_connected_trigger_ = new Trigger<>(); - Trigger<> *client_disconnected_trigger_ = new Trigger<>(); + Trigger<> client_connected_trigger_; + Trigger<> client_disconnected_trigger_; api::APIConnection *api_client_{nullptr}; std::unordered_map timers_; void timer_tick_(); - Trigger *timer_started_trigger_ = new Trigger(); - Trigger *timer_finished_trigger_ = new Trigger(); - Trigger *timer_updated_trigger_ = new Trigger(); - Trigger *timer_cancelled_trigger_ = new Trigger(); - Trigger> *timer_tick_trigger_ = new Trigger>(); + Trigger timer_started_trigger_; + Trigger timer_finished_trigger_; + Trigger timer_updated_trigger_; + Trigger timer_cancelled_trigger_; + Trigger> timer_tick_trigger_; bool has_timers_{false}; bool timer_tick_running_{false}; From 18c152723c1b607fba022b0b0d1d702cdf5020cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 04:53:46 +0100 Subject: [PATCH 024/251] [sprinkler] Avoid heap allocation for triggers (#13705) --- esphome/components/sprinkler/sprinkler.cpp | 16 ++++++---------- esphome/components/sprinkler/sprinkler.h | 12 ++++++------ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 2a60eb042b..eae6ecbf31 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -29,7 +29,7 @@ void SprinklerControllerNumber::setup() { } void SprinklerControllerNumber::control(float value) { - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); this->publish_state(value); @@ -39,8 +39,7 @@ void SprinklerControllerNumber::control(float value) { void SprinklerControllerNumber::dump_config() { LOG_NUMBER("", "Sprinkler Controller Number", this); } -SprinklerControllerSwitch::SprinklerControllerSwitch() - : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} +SprinklerControllerSwitch::SprinklerControllerSwitch() = default; void SprinklerControllerSwitch::loop() { // Loop is only enabled when f_ has a value (see setup()) @@ -56,11 +55,11 @@ void SprinklerControllerSwitch::write_state(bool state) { } if (state) { - this->prev_trigger_ = this->turn_on_trigger_; - this->turn_on_trigger_->trigger(); + this->prev_trigger_ = &this->turn_on_trigger_; + this->turn_on_trigger_.trigger(); } else { - this->prev_trigger_ = this->turn_off_trigger_; - this->turn_off_trigger_->trigger(); + this->prev_trigger_ = &this->turn_off_trigger_; + this->turn_off_trigger_.trigger(); } this->publish_state(state); @@ -69,9 +68,6 @@ void SprinklerControllerSwitch::write_state(bool state) { void SprinklerControllerSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float SprinklerControllerSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } -Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } - void SprinklerControllerSwitch::setup() { this->state = this->get_initial_state_with_restore_mode().value_or(false); // Disable loop if no state lambda is set - nothing to poll diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 04efa28031..a3cdef5b1a 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -76,7 +76,7 @@ class SprinklerControllerNumber : public number::Number, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::PROCESSOR; } - Trigger *get_set_trigger() const { return set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_initial_value(float initial_value) { initial_value_ = initial_value; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } @@ -84,7 +84,7 @@ class SprinklerControllerNumber : public number::Number, public Component { void control(float value) override; float initial_value_{NAN}; bool restore_value_{true}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; ESPPreferenceObject pref_; }; @@ -97,8 +97,8 @@ class SprinklerControllerSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - Trigger<> *get_turn_on_trigger() const; - Trigger<> *get_turn_off_trigger() const; + Trigger<> *get_turn_on_trigger() { return &this->turn_on_trigger_; } + Trigger<> *get_turn_off_trigger() { return &this->turn_off_trigger_; } void loop() override; float get_setup_priority() const override; @@ -107,8 +107,8 @@ class SprinklerControllerSwitch : public switch_::Switch, public Component { void write_state(bool state) override; optional()>> f_; - Trigger<> *turn_on_trigger_; - Trigger<> *turn_off_trigger_; + Trigger<> turn_on_trigger_; + Trigger<> turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; }; From 379652f63107cc8b14e6994788e78d0582944b53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 05:10:08 +0100 Subject: [PATCH 025/251] [thermostat] Remove dead null checks for triggers (#13706) --- .../thermostat/thermostat_climate.cpp | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 02e01db549..c666419701 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -586,9 +586,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } this->action = action; this->prev_action_trigger_ = trig; - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); // if enabled, call the fan_only action with cooling/heating actions if (trig_fan != nullptr) { ESP_LOGVV(TAG, "Calling FAN_ONLY action with HEATING/COOLING action"); @@ -686,9 +684,7 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction } this->humidification_action = action; this->prev_humidity_control_trigger_ = trig; - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); } void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state) { @@ -756,9 +752,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->prev_fan_mode_trigger_ = nullptr; } this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->prev_fan_mode_ = fan_mode; this->prev_fan_mode_trigger_ = trig; } @@ -802,9 +796,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ mode = climate::CLIMATE_MODE_OFF; // trig = this->off_mode_trigger_; } - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->mode = mode; this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; @@ -844,9 +836,7 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo swing_mode = climate::CLIMATE_SWING_OFF; // trig = &this->swing_mode_off_trigger_; } - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->swing_mode = swing_mode; this->prev_swing_mode_ = swing_mode; this->prev_swing_mode_trigger_ = trig; From f0801ecac01779cc78000fdb144e7bfd82623c40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 05:14:11 +0100 Subject: [PATCH 026/251] [template.lock] Avoid heap allocation for triggers (#13704) --- .../components/template/lock/template_lock.cpp | 18 +++++++----------- .../components/template/lock/template_lock.h | 12 ++++++------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index de8f9b762c..dbc4501ce7 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -7,8 +7,7 @@ using namespace esphome::lock; static const char *const TAG = "template.lock"; -TemplateLock::TemplateLock() - : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} +TemplateLock::TemplateLock() = default; void TemplateLock::setup() { if (!this->f_.has_value()) @@ -28,11 +27,11 @@ void TemplateLock::control(const lock::LockCall &call) { auto state = *call.get_state(); if (state == LOCK_STATE_LOCKED) { - this->prev_trigger_ = this->lock_trigger_; - this->lock_trigger_->trigger(); + this->prev_trigger_ = &this->lock_trigger_; + this->lock_trigger_.trigger(); } else if (state == LOCK_STATE_UNLOCKED) { - this->prev_trigger_ = this->unlock_trigger_; - this->unlock_trigger_->trigger(); + this->prev_trigger_ = &this->unlock_trigger_; + this->unlock_trigger_.trigger(); } if (this->optimistic_) @@ -42,14 +41,11 @@ void TemplateLock::open_latch() { if (this->prev_trigger_ != nullptr) { this->prev_trigger_->stop_action(); } - this->prev_trigger_ = this->open_trigger_; - this->open_trigger_->trigger(); + this->prev_trigger_ = &this->open_trigger_; + this->open_trigger_.trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } -Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } -Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; } void TemplateLock::dump_config() { LOG_LOCK("", "Template Lock", this); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index f4396c2c5d..03e3e86d88 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -15,9 +15,9 @@ class TemplateLock final : public lock::Lock, public Component { void dump_config() override; template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } - Trigger<> *get_lock_trigger() const; - Trigger<> *get_unlock_trigger() const; - Trigger<> *get_open_trigger() const; + Trigger<> *get_lock_trigger() { return &this->lock_trigger_; } + Trigger<> *get_unlock_trigger() { return &this->unlock_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } void set_optimistic(bool optimistic); void loop() override; @@ -29,9 +29,9 @@ class TemplateLock final : public lock::Lock, public Component { TemplateLambda f_; bool optimistic_{false}; - Trigger<> *lock_trigger_; - Trigger<> *unlock_trigger_; - Trigger<> *open_trigger_; + Trigger<> lock_trigger_; + Trigger<> unlock_trigger_; + Trigger<> open_trigger_; Trigger<> *prev_trigger_{nullptr}; }; From dbd740172192f2d2acefb9cf883f2e544e174abe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 05:15:13 +0100 Subject: [PATCH 027/251] [feedback] Avoid heap allocation for cover triggers (#13693) --- esphome/components/feedback/feedback_cover.cpp | 6 +++--- esphome/components/feedback/feedback_cover.h | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index e419ee6229..ffb19fa091 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -335,18 +335,18 @@ void FeedbackCover::start_direction_(CoverOperation dir) { switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; #ifdef USE_BINARY_SENSOR obstacle = this->open_obstacle_; #endif break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; #ifdef USE_BINARY_SENSOR obstacle = this->close_obstacle_; #endif diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 199d3b520a..6be8939413 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -17,9 +17,9 @@ class FeedbackCover : public cover::Cover, public Component { void loop() override; void dump_config() override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } #ifdef USE_BINARY_SENSOR void set_open_endstop(binary_sensor::BinarySensor *open_endstop); @@ -61,9 +61,9 @@ class FeedbackCover : public cover::Cover, public Component { binary_sensor::BinarySensor *close_obstacle_{nullptr}; #endif - Trigger<> *open_trigger_{new Trigger<>()}; - Trigger<> *close_trigger_{new Trigger<>()}; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; + Trigger<> close_trigger_; + Trigger<> stop_trigger_; uint32_t open_duration_{0}; uint32_t close_duration_{0}; From 1362ff6cba18351ac7fd89899168e00cb233c700 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 05:15:33 +0100 Subject: [PATCH 028/251] [speaker.media_player] Avoid heap allocation for triggers (#13707) --- esphome/components/mixer/speaker/automation.h | 1 + .../speaker/media_player/speaker_media_player.cpp | 6 +++--- .../speaker/media_player/speaker_media_player.h | 12 ++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index 2234936628..2fb2f49373 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "mixer_speaker.h" #ifdef USE_ESP32 diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 172bc980a8..94f555c26e 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -519,9 +519,9 @@ void SpeakerMediaPlayer::set_mute_state_(bool mute_state) { if (old_mute_state != mute_state) { if (mute_state) { - this->defer([this]() { this->mute_trigger_->trigger(); }); + this->defer([this]() { this->mute_trigger_.trigger(); }); } else { - this->defer([this]() { this->unmute_trigger_->trigger(); }); + this->defer([this]() { this->unmute_trigger_.trigger(); }); } } } @@ -550,7 +550,7 @@ void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { this->set_mute_state_(false); } - this->defer([this, volume]() { this->volume_trigger_->trigger(volume); }); + this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); } } // namespace speaker diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 065926d0cf..722f98ceea 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -84,9 +84,9 @@ class SpeakerMediaPlayer : public Component, this->media_format_ = media_format; } - Trigger<> *get_mute_trigger() const { return this->mute_trigger_; } - Trigger<> *get_unmute_trigger() const { return this->unmute_trigger_; } - Trigger *get_volume_trigger() const { return this->volume_trigger_; } + Trigger<> *get_mute_trigger() { return &this->mute_trigger_; } + Trigger<> *get_unmute_trigger() { return &this->unmute_trigger_; } + Trigger *get_volume_trigger() { return &this->volume_trigger_; } void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue); @@ -154,9 +154,9 @@ class SpeakerMediaPlayer : public Component, // Used to save volume/mute state for restoration on reboot ESPPreferenceObject pref_; - Trigger<> *mute_trigger_ = new Trigger<>(); - Trigger<> *unmute_trigger_ = new Trigger<>(); - Trigger *volume_trigger_ = new Trigger(); + Trigger<> mute_trigger_; + Trigger<> unmute_trigger_; + Trigger volume_trigger_; }; } // namespace speaker From 56110d4495055523d8a837d7d68ea71285109a9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 05:15:50 +0100 Subject: [PATCH 029/251] [time_based] Avoid heap allocation for cover triggers (#13703) --- esphome/components/time_based/time_based_cover.cpp | 6 +++--- esphome/components/time_based/time_based_cover.h | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 1eb591fe6e..0aef4b8e85 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -132,15 +132,15 @@ void TimeBasedCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index 42cf66c2ab..d2457cae7a 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -14,9 +14,9 @@ class TimeBasedCover : public cover::Cover, public Component { void dump_config() override; float get_setup_priority() const override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; } cover::CoverTraits get_traits() override; @@ -34,11 +34,11 @@ class TimeBasedCover : public cover::Cover, public Component { void recompute_position_(); - Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; uint32_t open_duration_; - Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> close_trigger_; uint32_t close_duration_; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; Trigger<> *prev_command_trigger_{nullptr}; uint32_t last_recompute_time_{0}; From 6727fe9040a1ce760ebcb86c54e1ae8686b06df9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 05:18:17 +0100 Subject: [PATCH 030/251] [remote_transmitter] Avoid heap allocation for triggers (#13708) --- .../components/remote_transmitter/remote_transmitter.cpp | 4 ++-- .../components/remote_transmitter/remote_transmitter.h | 8 ++++---- .../remote_transmitter/remote_transmitter_esp32.cpp | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index f20789fb9f..d35541e2e1 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -83,7 +83,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); this->target_time_ = 0; - this->transmit_trigger_->trigger(); + this->transmit_trigger_.trigger(); for (uint32_t i = 0; i < send_times; i++) { InterruptLock lock; for (int32_t item : this->temp_.get_data()) { @@ -102,7 +102,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) this->target_time_ += send_wait; } - this->complete_trigger_->trigger(); + this->complete_trigger_.trigger(); } } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index dd6a849e4c..65bd2ac8b2 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -57,8 +57,8 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } #endif - Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; - Trigger<> *get_complete_trigger() const { return this->complete_trigger_; }; + Trigger<> *get_transmit_trigger() { return &this->transmit_trigger_; } + Trigger<> *get_complete_trigger() { return &this->complete_trigger_; } protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; @@ -96,8 +96,8 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #endif uint8_t carrier_duty_percent_; - Trigger<> *transmit_trigger_{new Trigger<>()}; - Trigger<> *complete_trigger_{new Trigger<>()}; + Trigger<> transmit_trigger_; + Trigger<> complete_trigger_; }; } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 59c85c99a8..89d97895b2 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -203,7 +203,7 @@ void RemoteTransmitterComponent::wait_for_rmt_() { this->status_set_warning(); } - this->complete_trigger_->trigger(); + this->complete_trigger_.trigger(); } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) @@ -264,7 +264,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen return; } - this->transmit_trigger_->trigger(); + this->transmit_trigger_.trigger(); rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); @@ -333,7 +333,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen ESP_LOGE(TAG, "Empty data"); return; } - this->transmit_trigger_->trigger(); + this->transmit_trigger_.trigger(); for (uint32_t i = 0; i < send_times; i++) { rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); @@ -354,7 +354,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) delayMicroseconds(send_wait); } - this->complete_trigger_->trigger(); + this->complete_trigger_.trigger(); } #endif From bc9fc6622553f0e58bcf5f8301fa2dd43da72f7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 05:30:46 +0100 Subject: [PATCH 031/251] [template.datetime] Avoid heap allocation for triggers (#13710) --- esphome/components/template/datetime/template_date.cpp | 2 +- esphome/components/template/datetime/template_date.h | 4 ++-- esphome/components/template/datetime/template_datetime.cpp | 2 +- esphome/components/template/datetime/template_datetime.h | 4 ++-- esphome/components/template/datetime/template_time.cpp | 2 +- esphome/components/template/datetime/template_time.h | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index be1d875a7e..8a5f11b876 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -62,7 +62,7 @@ void TemplateDate::control(const datetime::DateCall &call) { if (has_day) value.day_of_month = *call.get_day(); - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 0379a9bc67..acf823a34d 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -22,7 +22,7 @@ class TemplateDate final : public datetime::DateEntity, public PollingComponent void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } @@ -34,7 +34,7 @@ class TemplateDate final : public datetime::DateEntity, public PollingComponent bool optimistic_{false}; ESPTime initial_value_{}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index e134f2b654..269a1d06ca 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -80,7 +80,7 @@ void TemplateDateTime::control(const datetime::DateTimeCall &call) { if (has_second) value.second = *call.get_second(); - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index b7eb490933..575065a3dd 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -22,7 +22,7 @@ class TemplateDateTime final : public datetime::DateTimeEntity, public PollingCo void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } @@ -34,7 +34,7 @@ class TemplateDateTime final : public datetime::DateTimeEntity, public PollingCo bool optimistic_{false}; ESPTime initial_value_{}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 586e126e3b..9c81687116 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -62,7 +62,7 @@ void TemplateTime::control(const datetime::TimeCall &call) { if (has_second) value.second = *call.get_second(); - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_hour) diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index cb83b1b3e5..924b53cc71 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -22,7 +22,7 @@ class TemplateTime final : public datetime::TimeEntity, public PollingComponent void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } @@ -34,7 +34,7 @@ class TemplateTime final : public datetime::TimeEntity, public PollingComponent bool optimistic_{false}; ESPTime initial_value_{}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; From b5b9a895619e10dfe7784874133c7a8ebd444af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 07:34:34 +0100 Subject: [PATCH 032/251] [light] Avoid heap allocation for AutomationLightEffect trigger (#13713) --- esphome/components/light/base_light_effects.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 2eeae574e7..cdb9f1f666 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -138,20 +138,20 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: AutomationLightEffect(const char *name) : LightEffect(name) {} - void stop() override { this->trig_->stop_action(); } + void stop() override { this->trig_.stop_action(); } void apply() override { - if (!this->trig_->is_action_running()) { - this->trig_->trigger(); + if (!this->trig_.is_action_running()) { + this->trig_.trigger(); } } - Trigger<> *get_trig() const { return trig_; } + Trigger<> *get_trig() { return &this->trig_; } /// Get the current effect index for use in automations. /// Useful for automations that need to know which effect is running. uint32_t get_current_index() const { return this->get_index(); } protected: - Trigger<> *trig_{new Trigger<>}; + Trigger<> trig_; }; struct StrobeLightEffectColor { From 61e33217cd1995ae9fca8caba309268dd5b36a09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 07:34:50 +0100 Subject: [PATCH 033/251] [cc1101] Avoid heap allocation for trigger (#13715) --- esphome/components/cc1101/cc1101.cpp | 2 +- esphome/components/cc1101/cc1101.h | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 46cd89e0e8..b6973da78d 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -156,7 +156,7 @@ void CC1101Component::call_listeners_(const std::vector &packet, float for (auto &listener : this->listeners_) { listener->on_packet(packet, freq_offset, rssi, lqi); } - this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi); + this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi); } void CC1101Component::loop() { diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 6e3f01af90..e55071e7e3 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -79,7 +79,7 @@ class CC1101Component : public Component, // Packet mode operations CC1101Error transmit_packet(const std::vector &packet); void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); } - Trigger, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; } + Trigger, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; } protected: uint16_t chip_id_{0}; @@ -96,8 +96,7 @@ class CC1101Component : public Component, // Packet handling void call_listeners_(const std::vector &packet, float freq_offset, float rssi, uint8_t lqi); - Trigger, float, float, uint8_t> *packet_trigger_{ - new Trigger, float, float, uint8_t>()}; + Trigger, float, float, uint8_t> packet_trigger_; std::vector packet_; std::vector listeners_; From 420de987bcd19edd991592632da82784c371ddc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 07:35:03 +0100 Subject: [PATCH 034/251] [micro_wake_word] Avoid heap allocation for trigger (#13714) --- esphome/components/micro_wake_word/micro_wake_word.cpp | 2 +- esphome/components/micro_wake_word/micro_wake_word.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index d7e80efc84..b93bf1b556 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -325,7 +325,7 @@ void MicroWakeWord::loop() { ESP_LOGD(TAG, "Detected '%s' with sliding average probability is %.2f and max probability is %.2f", detection_event.wake_word->c_str(), (detection_event.average_probability / uint8_to_float_divisor), (detection_event.max_probability / uint8_to_float_divisor)); - this->wake_word_detected_trigger_->trigger(*detection_event.wake_word); + this->wake_word_detected_trigger_.trigger(*detection_event.wake_word); if (this->stop_after_detection_) { this->stop(); } diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index b427e4dfcb..44d5d89372 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -60,7 +60,7 @@ class MicroWakeWord : public Component void set_stop_after_detection(bool stop_after_detection) { this->stop_after_detection_ = stop_after_detection; } - Trigger *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } + Trigger *get_wake_word_detected_trigger() { return &this->wake_word_detected_trigger_; } void add_wake_word_model(WakeWordModel *model); @@ -78,7 +78,7 @@ class MicroWakeWord : public Component protected: microphone::MicrophoneSource *microphone_source_{nullptr}; - Trigger *wake_word_detected_trigger_ = new Trigger(); + Trigger wake_word_detected_trigger_; State state_{State::STOPPED}; std::weak_ptr ring_buffer_; From c0e5ae4298162b1c971121255de9bf5358be93d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 07:35:21 +0100 Subject: [PATCH 035/251] [template.text] Avoid heap allocation for trigger (#13711) --- esphome/components/template/text/template_text.cpp | 2 +- esphome/components/template/text/template_text.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index 70b8dce312..af134e6ed4 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -47,7 +47,7 @@ void TemplateText::update() { } void TemplateText::control(const std::string &value) { - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) this->publish_state(value); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index e5e5e4f4a8..88c6afdf2c 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -68,7 +68,7 @@ class TemplateText final : public text::Text, public PollingComponent { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; } /// Prevent accidental use of std::string which would dangle @@ -79,7 +79,7 @@ class TemplateText final : public text::Text, public PollingComponent { void control(const std::string &value) override; bool optimistic_ = false; const char *initial_value_{nullptr}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_{}; TemplateTextSaverBase *pref_ = nullptr; From 61140059524d837dc9ac40bc0dfe6acc7a027216 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 07:36:08 +0100 Subject: [PATCH 036/251] [template.water_heater] Avoid heap allocation for trigger (#13712) --- .../template/water_heater/template_water_heater.cpp | 4 ++-- .../components/template/water_heater/template_water_heater.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index e89c96ca48..f888edb1df 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -5,7 +5,7 @@ namespace esphome::template_ { static const char *const TAG = "template.water_heater"; -TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {} +TemplateWaterHeater::TemplateWaterHeater() = default; void TemplateWaterHeater::setup() { if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || @@ -78,7 +78,7 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { } } - this->set_trigger_->trigger(); + this->set_trigger_.trigger(); if (this->optimistic_) { this->publish_state(); diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index c2a2dcbb23..f1cf00a115 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -28,7 +28,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { this->supported_modes_ = modes; } - Trigger<> *get_set_trigger() const { return this->set_trigger_; } + Trigger<> *get_set_trigger() { return &this->set_trigger_; } void setup() override; void loop() override; @@ -42,7 +42,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { water_heater::WaterHeaterTraits traits() override; // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller - Trigger<> *set_trigger_; + Trigger<> set_trigger_; TemplateLambda current_temperature_f_; TemplateLambda mode_f_; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; From 62f34bea83c1423a8b894cebf04c3cc40bfc78f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 07:36:27 +0100 Subject: [PATCH 037/251] [template.output] Avoid heap allocation for triggers (#13709) --- esphome/components/template/output/template_output.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index e536660b02..6fe8e53855 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -8,22 +8,22 @@ namespace esphome::template_ { class TemplateBinaryOutput final : public output::BinaryOutput { public: - Trigger *get_trigger() const { return trigger_; } + Trigger *get_trigger() { return &this->trigger_; } protected: - void write_state(bool state) override { this->trigger_->trigger(state); } + void write_state(bool state) override { this->trigger_.trigger(state); } - Trigger *trigger_ = new Trigger(); + Trigger trigger_; }; class TemplateFloatOutput final : public output::FloatOutput { public: - Trigger *get_trigger() const { return trigger_; } + Trigger *get_trigger() { return &this->trigger_; } protected: - void write_state(float state) override { this->trigger_->trigger(state); } + void write_state(float state) override { this->trigger_.trigger(state); } - Trigger *trigger_ = new Trigger(); + Trigger trigger_; }; } // namespace esphome::template_ From 18991686ab389d153c2af110daee17d8124fcf31 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Mon, 2 Feb 2026 10:48:08 -0500 Subject: [PATCH 038/251] [mqtt] resolve warnings related to use of ip.str() (#13719) --- esphome/components/mqtt/mqtt_backend_esp32.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index bd2d2a67b2..adba0cf004 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -139,7 +139,8 @@ class MQTTBackendESP32 final : public MQTTBackend { this->lwt_retain_ = retain; } void set_server(network::IPAddress ip, uint16_t port) final { - this->host_ = ip.str(); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + this->host_ = ip.str_to(ip_buf); this->port_ = port; } void set_server(const char *host, uint16_t port) final { From aa8ccfc32b0e2f7c347911f27dc8dce922f2b16b Mon Sep 17 00:00:00 2001 From: Roger Fachini Date: Mon, 2 Feb 2026 08:00:11 -0800 Subject: [PATCH 039/251] [ethernet] Add on_connect and on_disconnect triggers (#13677) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/ethernet/__init__.py | 18 +++++++++++++++++- .../components/ethernet/ethernet_component.cpp | 9 +++++++++ .../components/ethernet/ethernet_component.h | 13 +++++++++++++ esphome/core/defines.h | 2 ++ tests/components/ethernet/common-dm9051.yaml | 4 ++++ tests/components/ethernet/common-dp83848.yaml | 4 ++++ tests/components/ethernet/common-ip101.yaml | 4 ++++ tests/components/ethernet/common-jl1101.yaml | 4 ++++ tests/components/ethernet/common-ksz8081.yaml | 4 ++++ .../components/ethernet/common-ksz8081rna.yaml | 4 ++++ tests/components/ethernet/common-lan8670.yaml | 4 ++++ tests/components/ethernet/common-lan8720.yaml | 4 ++++ tests/components/ethernet/common-openeth.yaml | 4 ++++ tests/components/ethernet/common-rtl8201.yaml | 4 ++++ tests/components/ethernet/common-w5500.yaml | 4 ++++ 15 files changed, 85 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 8d4a1aaf8e..23436cc5be 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,6 +1,6 @@ import logging -from esphome import pins +from esphome import automation, pins import esphome.codegen as cg from esphome.components.esp32 import ( VARIANT_ESP32, @@ -35,6 +35,8 @@ from esphome.const import ( CONF_MODE, CONF_MOSI_PIN, CONF_NUMBER, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, CONF_PAGE_ID, CONF_PIN, CONF_POLLING_INTERVAL, @@ -237,6 +239,8 @@ BASE_SCHEMA = cv.Schema( cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA) @@ -430,6 +434,18 @@ async def to_code(config): if CORE.using_arduino: cg.add_library("WiFi", None) + if on_connect_config := config.get(CONF_ON_CONNECT): + cg.add_define("USE_ETHERNET_CONNECT_TRIGGER") + await automation.build_automation( + var.get_connect_trigger(), [], on_connect_config + ) + + if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER") + await automation.build_automation( + var.get_disconnect_trigger(), [], on_disconnect_config + ) + CORE.add_job(final_step) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 70f8ce1204..af7fed608b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -309,6 +309,9 @@ void EthernetComponent::loop() { this->dump_connect_params_(); this->status_clear_warning(); +#ifdef USE_ETHERNET_CONNECT_TRIGGER + this->connect_trigger_.trigger(); +#endif } else if (now - this->connect_begin_ > 15000) { ESP_LOGW(TAG, "Connecting failed; reconnecting"); this->start_connect_(); @@ -318,10 +321,16 @@ void EthernetComponent::loop() { if (!this->started_) { ESP_LOGI(TAG, "Stopped connection"); this->state_ = EthernetComponentState::STOPPED; +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif } else if (!this->connected_) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = EthernetComponentState::CONNECTING; this->start_connect_(); +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif } else { this->finish_connect_(); // When connected and stable, disable the loop to save CPU cycles diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 34380047d1..5a2869c5a7 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/automation.h" #include "esphome/components/network/ip_address.h" #ifdef USE_ESP32 @@ -119,6 +120,12 @@ class EthernetComponent : public Component { void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } #endif +#ifdef USE_ETHERNET_CONNECT_TRIGGER + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } +#endif +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } +#endif protected: static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); @@ -190,6 +197,12 @@ class EthernetComponent : public Component { StaticVector ip_state_listeners_; #endif +#ifdef USE_ETHERNET_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e98cdd0ba0..1edc648084 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -240,6 +240,8 @@ #define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_MANUAL_IP #define USE_ETHERNET_IP_STATE_LISTENERS +#define USE_ETHERNET_CONNECT_TRIGGER +#define USE_ETHERNET_DISCONNECT_TRIGGER #define ESPHOME_ETHERNET_IP_STATE_LISTENERS 2 #endif diff --git a/tests/components/ethernet/common-dm9051.yaml b/tests/components/ethernet/common-dm9051.yaml index 4526e7732d..bb8c74b820 100644 --- a/tests/components/ethernet/common-dm9051.yaml +++ b/tests/components/ethernet/common-dm9051.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml index f9069c5fb9..809613c79d 100644 --- a/tests/components/ethernet/common-dp83848.yaml +++ b/tests/components/ethernet/common-dp83848.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml index cea7a5cc35..41716a7850 100644 --- a/tests/components/ethernet/common-ip101.yaml +++ b/tests/components/ethernet/common-ip101.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml index 7b0a2dfdc4..d70a576c81 100644 --- a/tests/components/ethernet/common-jl1101.yaml +++ b/tests/components/ethernet/common-jl1101.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml index 65541832c2..e2add8d370 100644 --- a/tests/components/ethernet/common-ksz8081.yaml +++ b/tests/components/ethernet/common-ksz8081.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml index f04cba15b2..1bb404f720 100644 --- a/tests/components/ethernet/common-ksz8081rna.yaml +++ b/tests/components/ethernet/common-ksz8081rna.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-lan8670.yaml b/tests/components/ethernet/common-lan8670.yaml index fb751ebd23..ae4953974c 100644 --- a/tests/components/ethernet/common-lan8670.yaml +++ b/tests/components/ethernet/common-lan8670.yaml @@ -12,3 +12,7 @@ ethernet: gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml index 838d57df28..742800fdf4 100644 --- a/tests/components/ethernet/common-lan8720.yaml +++ b/tests/components/ethernet/common-lan8720.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-openeth.yaml b/tests/components/ethernet/common-openeth.yaml index fbb7579598..26595dbc52 100644 --- a/tests/components/ethernet/common-openeth.yaml +++ b/tests/components/ethernet/common-openeth.yaml @@ -1,2 +1,6 @@ ethernet: type: OPENETH + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml index 0e7cbe73c6..d5a60f6e98 100644 --- a/tests/components/ethernet/common-rtl8201.yaml +++ b/tests/components/ethernet/common-rtl8201.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-w5500.yaml b/tests/components/ethernet/common-w5500.yaml index b3e96f000d..1f8b8650dd 100644 --- a/tests/components/ethernet/common-w5500.yaml +++ b/tests/components/ethernet/common-w5500.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" From 6892805094426d903b0dd2650ef8531e861f0271 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 22:00:46 +0100 Subject: [PATCH 040/251] [api] Align water_heater_command with standard entity command pattern (#13655) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 2 +- esphome/components/api/api_pb2_service.cpp | 5 +++++ esphome/components/api/api_pb2_service.h | 6 ++++++ 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 597da25883..d25934c60b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -45,6 +45,7 @@ service APIConnection { rpc time_command (TimeCommandRequest) returns (void) {} rpc update_command (UpdateCommandRequest) returns (void) {} rpc valve_command (ValveCommandRequest) returns (void) {} + rpc water_heater_command (WaterHeaterCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 1626f395e6..839de29de7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1385,7 +1385,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec is_single); } -void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { +void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater) if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE) call.set_mode(static_cast(msg.mode)); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 21bf4c4073..b839a2a97b 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -170,7 +170,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_WATER_HEATER bool send_water_heater_state(water_heater::WaterHeater *water_heater); - void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; + void water_heater_command(const WaterHeaterCommandRequest &msg) override; #endif #ifdef USE_IR_RF diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4b7148e6c0..af0a2d0ca2 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -746,6 +746,11 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest & #ifdef USE_VALVE void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); } #endif +#ifdef USE_WATER_HEATER +void APIServerConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { + this->water_heater_command(msg); +} +#endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 200991c282..80a61c1041 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -303,6 +303,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_VALVE virtual void valve_command(const ValveCommandRequest &msg) = 0; #endif +#ifdef USE_WATER_HEATER + virtual void water_heater_command(const WaterHeaterCommandRequest &msg) = 0; +#endif #ifdef USE_BLUETOOTH_PROXY virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; #endif @@ -432,6 +435,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_VALVE void on_valve_command_request(const ValveCommandRequest &msg) override; #endif +#ifdef USE_WATER_HEATER + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; +#endif #ifdef USE_BLUETOOTH_PROXY void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; #endif From 848c2371590f1839704f1ddeeeda65b539c7506e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 22:05:27 +0100 Subject: [PATCH 041/251] [time] Use lazy callback for time sync to save 8 bytes (#13652) --- esphome/components/time/real_time_clock.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 70469e11b0..19aa1a4f4a 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -62,7 +62,7 @@ class RealTimeClock : public PollingComponent { void apply_timezone_(); #endif - CallbackManager time_sync_callback_; + LazyCallbackManager time_sync_callback_; }; template class TimeHasTimeCondition : public Condition { From 4f0894e970468d1ecf375866d25d46977903e13c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 22:05:39 +0100 Subject: [PATCH 042/251] [analyze-memory] Add top 30 largest symbols to report (#13673) --- esphome/analyze_memory/cli.py | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index a77e17afce..72a73dbdd4 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Callable +import heapq +from operator import itemgetter import sys from typing import TYPE_CHECKING @@ -29,6 +31,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): ) # Lower threshold for RAM symbols (RAM is more constrained) RAM_SYMBOL_SIZE_THRESHOLD: int = 24 + # Number of top symbols to show in the largest symbols report + TOP_SYMBOLS_LIMIT: int = 30 + # Width for symbol name display in top symbols report + COL_TOP_SYMBOL_NAME: int = 55 # Column width constants COL_COMPONENT: int = 29 @@ -147,6 +153,37 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] return f"{demangled} ({size:,} B){section_label}" + def _add_top_symbols(self, lines: list[str]) -> None: + """Add a section showing the top largest symbols in the binary.""" + # Collect all symbols from all components: (symbol, demangled, size, section, component) + all_symbols = [ + (symbol, demangled, size, section, component) + for component, symbols in self._component_symbols.items() + for symbol, demangled, size, section in symbols + ] + + # Get top N symbols by size using heapq for efficiency + top_symbols = heapq.nlargest( + self.TOP_SYMBOLS_LIMIT, all_symbols, key=itemgetter(2) + ) + + lines.append("") + lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:") + # Calculate truncation limit from column width (leaving room for "...") + truncate_limit = self.COL_TOP_SYMBOL_NAME - 3 + for i, (_, demangled, size, section, component) in enumerate(top_symbols): + # Format section label + section_label = f"[{section[1:]}]" if section else "" + # Truncate demangled name if too long + demangled_display = ( + f"{demangled[:truncate_limit]}..." + if len(demangled) > self.COL_TOP_SYMBOL_NAME + else demangled + ) + lines.append( + f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" + ) + def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" components = sorted( @@ -248,6 +285,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): "RAM", ) + # Top largest symbols in the binary + self._add_top_symbols(lines) + # Add ESPHome core detailed analysis if there are core symbols if self._esphome_core_symbols: self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis") From c089d9aeac70588cf56b20fa9c353cc081060839 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 22:21:52 +0100 Subject: [PATCH 043/251] [esp32_hosted] Replace sscanf with strtol for version parsing (#13658) --- .../update/esp32_hosted_update.cpp | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index ebcdd5f36e..dac2b01425 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -34,14 +34,29 @@ static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_M ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1); #ifdef USE_ESP32_HOSTED_HTTP_UPDATE +// Parse an integer from str, advancing ptr past the number +// Returns false if no digits were parsed +static bool parse_int(const char *&ptr, int &value) { + char *end; + value = static_cast(strtol(ptr, &end, 10)); + if (end == ptr) + return false; + ptr = end; + return true; +} + // Parse version string "major.minor.patch" into components -// Returns true if parsing succeeded +// Returns true if at least major.minor was parsed static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) { major = minor = patch = 0; - if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) { - return true; - } - return false; + const char *ptr = version_str.c_str(); + + if (!parse_int(ptr, major) || *ptr++ != '.' || !parse_int(ptr, minor)) + return false; + if (*ptr == '.') + parse_int(++ptr, patch); + + return true; } // Compare two versions, returns: From 1119003eb5208206eae7526246968dd703ca1dd2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 22:22:11 +0100 Subject: [PATCH 044/251] [core] Add missing uint32_t ID overloads for defer() and cancel_defer() (#13720) --- esphome/core/component.cpp | 4 ++ esphome/core/component.h | 4 ++ .../fixtures/scheduler_numeric_id_test.yaml | 45 ++++++++++++++++++- .../test_scheduler_numeric_id_test.py | 38 +++++++++++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 98e8c02d07..f09a39d2bb 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -359,6 +359,10 @@ void Component::defer(const std::string &name, std::function &&f) { // void Component::defer(const char *name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } +void Component::defer(uint32_t id, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, id, 0, std::move(f)); +} +bool Component::cancel_defer(uint32_t id) { return App.scheduler.cancel_timeout(this, id); } void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast(nullptr), timeout, std::move(f)); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 49349d4199..97f2afe1a4 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -494,11 +494,15 @@ class Component { /// Defer a callback to the next loop() call. void defer(std::function &&f); // NOLINT + /// Defer a callback with a numeric ID (zero heap allocation) + void defer(uint32_t id, std::function &&f); // NOLINT + /// Cancel a defer callback using the specified name, name must not be empty. // Remove before 2026.7.0 ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_defer(const std::string &name); // NOLINT bool cancel_defer(const char *name); // NOLINT + bool cancel_defer(uint32_t id); // NOLINT // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index bf60f2fda9..1669f026f5 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -20,6 +20,9 @@ globals: - id: retry_counter type: int initial_value: '0' + - id: defer_counter + type: int + initial_value: '0' - id: tests_done type: bool initial_value: 'false' @@ -136,11 +139,49 @@ script: App.scheduler.cancel_retry(component1, 6002U); ESP_LOGI("test", "Cancelled numeric retry 6002"); + // Test 12: defer with numeric ID (Component method) + class TestDeferComponent : public Component { + public: + void test_defer_methods() { + // Test defer with uint32_t ID - should execute on next loop + this->defer(7001U, []() { + ESP_LOGI("test", "Component numeric defer 7001 fired"); + id(defer_counter) += 1; + }); + + // Test another defer with numeric ID + this->defer(7002U, []() { + ESP_LOGI("test", "Component numeric defer 7002 fired"); + id(defer_counter) += 1; + }); + } + }; + + static TestDeferComponent test_defer_component; + test_defer_component.test_defer_methods(); + + // Test 13: cancel_defer with numeric ID (Component method) + class TestCancelDeferComponent : public Component { + public: + void test_cancel_defer() { + // Set a defer that should be cancelled + this->defer(8001U, []() { + ESP_LOGE("test", "ERROR: Numeric defer 8001 should have been cancelled"); + }); + // Cancel it immediately + bool cancelled = this->cancel_defer(8001U); + ESP_LOGI("test", "Cancelled numeric defer 8001: %s", cancelled ? "true" : "false"); + } + }; + + static TestCancelDeferComponent test_cancel_defer_component; + test_cancel_defer_component.test_cancel_defer(); + - id: report_results then: - lambda: |- - ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d", - id(timeout_counter), id(interval_counter), id(retry_counter)); + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d", + id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter)); sensor: - platform: template diff --git a/tests/integration/test_scheduler_numeric_id_test.py b/tests/integration/test_scheduler_numeric_id_test.py index 510256b9a4..c1958db685 100644 --- a/tests/integration/test_scheduler_numeric_id_test.py +++ b/tests/integration/test_scheduler_numeric_id_test.py @@ -19,6 +19,7 @@ async def test_scheduler_numeric_id_test( timeout_count = 0 interval_count = 0 retry_count = 0 + defer_count = 0 # Events for each test completion numeric_timeout_1001_fired = asyncio.Event() @@ -33,6 +34,9 @@ async def test_scheduler_numeric_id_test( max_id_timeout_fired = asyncio.Event() numeric_retry_done = asyncio.Event() numeric_retry_cancelled = asyncio.Event() + numeric_defer_7001_fired = asyncio.Event() + numeric_defer_7002_fired = asyncio.Event() + numeric_defer_cancelled = asyncio.Event() final_results_logged = asyncio.Event() # Track interval counts @@ -40,7 +44,7 @@ async def test_scheduler_numeric_id_test( numeric_retry_count = 0 def on_log_line(line: str) -> None: - nonlocal timeout_count, interval_count, retry_count + nonlocal timeout_count, interval_count, retry_count, defer_count nonlocal numeric_interval_count, numeric_retry_count # Strip ANSI color codes @@ -105,15 +109,27 @@ async def test_scheduler_numeric_id_test( elif "Cancelled numeric retry 6002" in clean_line: numeric_retry_cancelled.set() + # Check for numeric defer tests + elif "Component numeric defer 7001 fired" in clean_line: + numeric_defer_7001_fired.set() + + elif "Component numeric defer 7002 fired" in clean_line: + numeric_defer_7002_fired.set() + + elif "Cancelled numeric defer 8001: true" in clean_line: + numeric_defer_cancelled.set() + # Check for final results elif "Final results" in clean_line: match = re.search( - r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+)", clean_line + r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)", + clean_line, ) if match: timeout_count = int(match.group(1)) interval_count = int(match.group(2)) retry_count = int(match.group(3)) + defer_count = int(match.group(4)) final_results_logged.set() async with ( @@ -201,6 +217,23 @@ async def test_scheduler_numeric_id_test( "Numeric retry 6002 should have been cancelled" ) + # Wait for numeric defer tests + try: + await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7001 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(numeric_defer_7002_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7002 did not fire within 0.5 seconds") + + # Verify numeric defer was cancelled + try: + await asyncio.wait_for(numeric_defer_cancelled.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 8001 cancel confirmation not received") + # Wait for final results try: await asyncio.wait_for(final_results_logged.wait(), timeout=3.0) @@ -215,3 +248,4 @@ async def test_scheduler_numeric_id_test( assert retry_count >= 2, ( f"Expected at least 2 retry attempts, got {retry_count}" ) + assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}" From da947d060f9e14b9f8b241290e80b5cf2d405aae Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:20:24 -0500 Subject: [PATCH 045/251] [wizard] Use API encryption key instead of deprecated password (#13634) Co-authored-by: Claude Opus 4.5 --- esphome/wizard.py | 66 +++++++++++++++++--------- tests/unit_tests/test_wizard.py | 84 +++++++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 25 deletions(-) diff --git a/esphome/wizard.py b/esphome/wizard.py index d77450b04d..f5e8a1e462 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,5 +1,7 @@ +import base64 from pathlib import Path import random +import secrets import string from typing import Literal, NotRequired, TypedDict, Unpack import unicodedata @@ -116,7 +118,6 @@ class WizardFileKwargs(TypedDict): board: str ssid: NotRequired[str] psk: NotRequired[str] - password: NotRequired[str] ota_password: NotRequired[str] api_encryption_key: NotRequired[str] friendly_name: NotRequired[str] @@ -144,9 +145,7 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: config += API_CONFIG - # Configure API - if "password" in kwargs: - config += f' password: "{kwargs["password"]}"\n' + # Configure API encryption if "api_encryption_key" in kwargs: config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n' @@ -155,8 +154,6 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: config += " - platform: esphome\n" if "ota_password" in kwargs: config += f' password: "{kwargs["ota_password"]}"' - elif "password" in kwargs: - config += f' password: "{kwargs["password"]}"' # Configuring wifi config += "\n\nwifi:\n" @@ -205,7 +202,6 @@ class WizardWriteKwargs(TypedDict): platform: NotRequired[str] ssid: NotRequired[str] psk: NotRequired[str] - password: NotRequired[str] ota_password: NotRequired[str] api_encryption_key: NotRequired[str] friendly_name: NotRequired[str] @@ -232,7 +228,7 @@ def wizard_write(path: Path, **kwargs: Unpack[WizardWriteKwargs]) -> bool: else: # "basic" board = kwargs["board"] - for key in ("ssid", "psk", "password", "ota_password"): + for key in ("ssid", "psk", "ota_password"): if key in kwargs: kwargs[key] = sanitize_double_quotes(kwargs[key]) if "platform" not in kwargs: @@ -522,26 +518,54 @@ def wizard(path: Path) -> int: "Almost there! ESPHome can automatically upload custom firmwares over WiFi " "(over the air) and integrates into Home Assistant with a native API." ) + safe_print() + sleep(0.5) + + # Generate encryption key (32 bytes, base64 encoded) for secure API communication + noise_psk = secrets.token_bytes(32) + api_encryption_key = base64.b64encode(noise_psk).decode() + safe_print( - f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(AnsiFore.GREEN, 'password')} for connecting to this ESP?" + "For secure API communication, I've generated a random encryption key." + ) + safe_print() + safe_print( + f"Your {color(AnsiFore.GREEN, 'API encryption key')} is: " + f"{color(AnsiFore.BOLD_WHITE, api_encryption_key)}" + ) + safe_print() + safe_print("You'll need this key when adding the device to Home Assistant.") + sleep(1) + + safe_print() + safe_print( + f"Do you want to set a {color(AnsiFore.GREEN, 'password')} for OTA updates? " + "This can be insecure if you do not trust the WiFi network." ) safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) + ota_password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) else: - ssid, password, psk = "", "", "" + ssid, psk = "", "" + api_encryption_key = None + ota_password = "" - if not wizard_write( - path=path, - name=name, - platform=platform, - board=board, - ssid=ssid, - psk=psk, - password=password, - type="basic", - ): + kwargs = { + "path": path, + "name": name, + "platform": platform, + "board": board, + "ssid": ssid, + "psk": psk, + "type": "basic", + } + if api_encryption_key: + kwargs["api_encryption_key"] = api_encryption_key + if ota_password: + kwargs["ota_password"] = ota_password + + if not wizard_write(**kwargs): return 1 safe_print() diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index fd53a0b0b7..eb44c1c20f 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -25,7 +25,6 @@ def default_config() -> dict[str, Any]: "board": "esp01_1m", "ssid": "test_ssid", "psk": "test_psk", - "password": "", } @@ -37,7 +36,7 @@ def wizard_answers() -> list[str]: "nodemcuv2", # board "SSID", # ssid "psk", # wifi password - "ota_pass", # ota password + "", # ota password (empty for no password) ] @@ -105,16 +104,35 @@ def test_config_file_should_include_ota_when_password_set( default_config: dict[str, Any], ): """ - The Over-The-Air update should be enabled when a password is set + The Over-The-Air update should be enabled when an OTA password is set """ # Given - default_config["password"] = "foo" + default_config["ota_password"] = "foo" # When config = wz.wizard_file(**default_config) # Then assert "ota:" in config + assert 'password: "foo"' in config + + +def test_config_file_should_include_api_encryption_key( + default_config: dict[str, Any], +): + """ + The API encryption key should be included when set + """ + # Given + default_config["api_encryption_key"] = "test_encryption_key_base64==" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "api:" in config + assert "encryption:" in config + assert 'key: "test_encryption_key_base64=="' in config def test_wizard_write_sets_platform( @@ -556,3 +574,61 @@ def test_wizard_write_protects_existing_config( # Then assert result is False # Should return False when file exists assert config_file.read_text() == original_content + + +def test_wizard_accepts_ota_password( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): + """ + The wizard should pass ota_password to wizard_write when the user provides one + """ + + # Given + wizard_answers[5] = "my_ota_password" # Set OTA password + config_file = tmp_path / "test.yaml" + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + wizard_write_mock = MagicMock(return_value=True) + monkeypatch.setattr(wz, "wizard_write", wizard_write_mock) + + # When + retval = wz.wizard(config_file) + + # Then + assert retval == 0 + call_kwargs = wizard_write_mock.call_args.kwargs + assert "ota_password" in call_kwargs + assert call_kwargs["ota_password"] == "my_ota_password" + + +def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch): + """ + The wizard should handle rpipico board which doesn't support WiFi. + This tests the branch where api_encryption_key is None. + """ + + # Given + wizard_answers_rp2040 = [ + "test-node", # Name of the node + "RP2040", # platform + "rpipico", # board (no WiFi support) + ] + config_file = tmp_path / "test.yaml" + input_mock = MagicMock(side_effect=wizard_answers_rp2040) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + wizard_write_mock = MagicMock(return_value=True) + monkeypatch.setattr(wz, "wizard_write", wizard_write_mock) + + # When + retval = wz.wizard(config_file) + + # Then + assert retval == 0 + call_kwargs = wizard_write_mock.call_args.kwargs + # rpipico doesn't support WiFi, so no api_encryption_key or ota_password + assert "api_encryption_key" not in call_kwargs + assert "ota_password" not in call_kwargs From a6543d32bd23ae18f2ae49cd6023281e2c912aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 3 Feb 2026 02:15:18 +0100 Subject: [PATCH 046/251] [sx126x] fix maximal payload_length (#13723) --- esphome/components/sx126x/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index ed878ed0d4..413eb139d6 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -213,7 +213,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=-3, max=22), cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP), - cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), + cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=255), cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535), cv.Required(CONF_RST_PIN): pins.gpio_output_pin_schema, From 26e4cda610d32c21c980c1fe96eda4b70551d8b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 02:25:54 +0100 Subject: [PATCH 047/251] [logger] Use vsnprintf_P directly for ESP8266 flash format strings (#13716) --- esphome/components/logger/logger.cpp | 51 +++++++--------------------- esphome/components/logger/logger.h | 45 ++++++++++++++---------- 2 files changed, 40 insertions(+), 56 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 3a726d4046..25243ff3f6 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -128,22 +128,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. // // This function handles format strings stored in flash memory (PROGMEM) to save RAM. -// The buffer is used in a special way to avoid allocating extra memory: -// -// Memory layout during execution: -// Step 1: Copy format string from flash to buffer -// tx_buffer_: [format_string][null][.....................] -// tx_buffer_at_: ------------------^ -// msg_start: saved here -----------^ -// -// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning -// and writes formatted output starting at msg_start position -// tx_buffer_: [format_string][null][formatted_message][null] -// tx_buffer_at_: -------------------------------------^ -// -// Step 3: Output the formatted message (starting at msg_start) -// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start -// which points to: [formatted_message][null] +// Uses vsnprintf_P to read the format string directly from flash without copying to RAM. // void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT @@ -153,35 +138,25 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas RecursionGuard guard(global_recursion_guard_); this->tx_buffer_at_ = 0; - // Copy format string from progmem - auto *format_pgm_p = reinterpret_cast(format); - char ch = '.'; - while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') { - this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++); - } + // Write header, format body directly from flash, and write footer + this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_, + this->tx_buffer_size_); + this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_, + reinterpret_cast(format), args); + this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Buffer full from copying format - RAII guard handles cleanup on return - if (this->tx_buffer_at_ >= this->tx_buffer_size_) { - return; - } - - // Save the offset before calling format_log_to_buffer_with_terminator_ - // since it will increment tx_buffer_at_ to the end of the formatted string - uint16_t msg_start = this->tx_buffer_at_; - this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, - &this->tx_buffer_at_, this->tx_buffer_size_); - - uint16_t msg_length = - this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + // Ensure null termination + uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_; + this->tx_buffer_[null_pos] = '\0'; // Listeners get message first (before console write) #ifdef USE_LOG_LISTENERS for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); + listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); #endif - // Write to console starting at the msg_start - this->write_tx_buffer_to_console_(msg_start, &msg_length); + // Write to console + this->write_tx_buffer_to_console_(); } #endif // USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index fe9cab4993..40ac9a38aa 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -597,31 +597,40 @@ class Logger : public Component { *buffer_at = pos; } + // Helper to process vsnprintf return value and strip trailing newlines. + // Updates buffer_at with the formatted length, handling truncation: + // - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + __attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at, + uint16_t remaining, int ret) { + if (ret < 0) + return; // Encoding error, do not increment buffer_at + *buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast(ret); + // Remove all trailing newlines right after formatting + while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') + (*buffer_at)--; + } + inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, va_list args) { - // Get remaining capacity in the buffer + // Check remaining capacity in the buffer if (*buffer_at >= buffer_size) return; const uint16_t remaining = buffer_size - *buffer_at; - - const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); - - if (ret < 0) { - return; // Encoding error, do not increment buffer_at - } - - // Update buffer_at with the formatted length (handle truncation) - // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator - // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator - uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; - *buffer_at += formatted_len; - - // Remove all trailing newlines right after formatting - while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') { - (*buffer_at)--; - } + process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args)); } +#ifdef USE_STORE_LOG_STR_IN_FLASH + // ESP8266 variant that reads format string directly from flash using vsnprintf_P + inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format, + va_list args) { + if (*buffer_at >= buffer_size) + return; + const uint16_t remaining = buffer_size - *buffer_at; + process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args)); + } +#endif + inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); From efecea9450462e3cb65d1c361e57aa5b9d0463ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:27:34 +0100 Subject: [PATCH 048/251] Bump github/codeql-action from 4.32.0 to 4.32.1 (#13726) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index be761cee3d..817ea1d2be 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 with: category: "/language:${{matrix.language}}" From ccf5c1f7e9103a470f9167105ae266af760dd7e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 03:12:12 +0100 Subject: [PATCH 049/251] [esp32] Exclude additional unused IDF components (driver, dac, mcpwm, twai, openthread, ulp) (#13664) --- esphome/components/esp32/__init__.py | 6 ++++++ esphome/components/esp32_can/canbus.py | 5 +++++ esphome/components/esp32_dac/output.py | 8 +++++++- esphome/components/esp32_touch/__init__.py | 2 ++ esphome/components/openthread/__init__.py | 4 ++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index aed4ecad90..4c53b42e6f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -124,10 +124,14 @@ COMPILER_OPTIMIZATIONS = { # - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing + "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers "esp_adc", # ADC driver - only needed by adc component + "esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch + "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component "esp_eth", # Ethernet driver - only needed by ethernet component "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality "esp_http_client", # HTTP client - only needed by http_request component @@ -138,9 +142,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "espcoredump", # Core dump support - ESPHome has its own debug component "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation + "openthread", # Thread protocol - only needed by openthread component "perfmon", # Xtensa performance monitor - ESPHome has its own debug component "protocomm", # Protocol communication for provisioning - unused by ESPHome "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) + "ulp", # ULP coprocessor - not currently used by any ESPHome component "unity", # Unit testing framework - ESPHome doesn't use IDF's testing "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index 0768b35507..7245ba7513 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -15,6 +15,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S2, VARIANT_ESP32S3, get_esp32_variant, + include_builtin_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate): async def to_code(config): + # Legacy driver component provides driver/twai.h header + include_builtin_idf_component("driver") + # Also enable esp_driver_twai for future migration to new API + include_builtin_idf_component("esp_driver_twai") var = cg.new_Pvariable(config[CONF_ID]) await canbus.register_canbus(var, config) diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py index daace596d3..7c63d7bd11 100644 --- a/esphome/components/esp32_dac/output.py +++ b/esphome/components/esp32_dac/output.py @@ -1,7 +1,12 @@ from esphome import pins import esphome.codegen as cg from esphome.components import output -from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + get_esp32_variant, + include_builtin_idf_component, +) import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN @@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( async def to_code(config): + include_builtin_idf_component("esp_driver_dac") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await output.register_output(var, config) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 6accb89c35..a02370a343 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -269,6 +269,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) include_builtin_idf_component("esp_driver_touch_sens") + # Legacy driver component provides driver/touch_sensor.h header + include_builtin_idf_component("driver") touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 26c05a0a86..89d335c574 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -4,6 +4,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, + include_builtin_idf_component, only_on_variant, require_vfs_select, ) @@ -172,6 +173,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): + # Re-enable openthread IDF component (excluded by default) + include_builtin_idf_component("openthread") + cg.add_define("USE_OPENTHREAD") # OpenThread SRP needs access to mDNS services after setup From ae71f07abb34b9b053eb5233f1ee39fd1936de7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 03:19:38 +0100 Subject: [PATCH 050/251] [http_request] Fix requests taking full timeout when response is already complete (#13649) --- .../update/esp32_hosted_update.cpp | 14 ++++- .../components/http_request/http_request.h | 55 ++++++++++++++----- .../http_request/http_request_arduino.cpp | 20 ++++++- .../http_request/http_request_idf.cpp | 9 +-- .../http_request/ota/ota_http_request.cpp | 6 +- 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index dac2b01425..a7d5f7e3d5 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -211,11 +211,14 @@ bool Esp32HostedUpdate::fetch_manifest_() { int read_or_error = container->read(buf, sizeof(buf)); App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. if (result != http_request::HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT json_str.append(reinterpret_cast(buf), read_or_error); } container->end(); @@ -336,9 +339,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. + if (result == http_request::HttpReadLoopResult::COMPLETE) + break; if (result != http_request::HttpReadLoopResult::DATA) { if (result == http_request::HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading firmware data"); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 79098a6b72..c88360ca78 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -26,6 +26,7 @@ struct Header { enum HttpStatus { HTTP_STATUS_OK = 200, HTTP_STATUS_NO_CONTENT = 204, + HTTP_STATUS_RESET_CONTENT = 205, HTTP_STATUS_PARTIAL_CONTENT = 206, /* 3xx - Redirection */ @@ -126,19 +127,21 @@ struct HttpReadResult { /// Result of processing a non-blocking read with timeout (for manual loops) enum class HttpReadLoopResult : uint8_t { - DATA, ///< Data was read, process it - RETRY, ///< No data yet, already delayed, caller should continue loop - ERROR, ///< Read error, caller should exit loop - TIMEOUT, ///< Timeout waiting for data, caller should exit loop + DATA, ///< Data was read, process it + COMPLETE, ///< All content has been read, caller should exit loop + RETRY, ///< No data yet, already delayed, caller should continue loop + ERROR, ///< Read error, caller should exit loop + TIMEOUT, ///< Timeout waiting for data, caller should exit loop }; /// Process a read result with timeout tracking and delay handling /// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error /// @param last_data_time Time of last successful read, updated when data received /// @param timeout_ms Maximum time to wait for data -/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit -inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, - uint32_t timeout_ms) { +/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete()) +/// @return How the caller should proceed - see HttpReadLoopResult enum +inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, + bool is_read_complete) { if (bytes_read_or_error > 0) { last_data_time = millis(); return HttpReadLoopResult::DATA; @@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_ if (bytes_read_or_error < 0) { return HttpReadLoopResult::ERROR; } - // bytes_read_or_error == 0: no data available yet + // bytes_read_or_error == 0: either "no data yet" or "all content read" + if (is_read_complete) { + return HttpReadLoopResult::COMPLETE; + } if (millis() - last_data_time >= timeout_ms) { return HttpReadLoopResult::TIMEOUT; } @@ -159,9 +165,9 @@ class HttpRequestComponent; class HttpContainer : public Parented { public: virtual ~HttpContainer() = default; - size_t content_length; - int status_code; - uint32_t duration_ms; + size_t content_length{0}; + int status_code{-1}; ///< -1 indicates no response received yet + uint32_t duration_ms{0}; /** * @brief Read data from the HTTP response body. @@ -194,9 +200,24 @@ class HttpContainer : public Parented { virtual void end() = 0; void set_secure(bool secure) { this->secure_ = secure; } + void set_chunked(bool chunked) { this->is_chunked_ = chunked; } size_t get_bytes_read() const { return this->bytes_read_; } + /// Check if all expected content has been read + /// For chunked responses, returns false (completion detected via read() returning error/EOF) + bool is_read_complete() const { + // Per RFC 9112, these responses have no body: + // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified + if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || + this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) { + return true; + } + // For non-chunked responses, complete when bytes_read >= content_length + // This handles both Content-Length: 0 and Content-Length: N cases + return !this->is_chunked_ && this->bytes_read_ >= this->content_length; + } + /** * @brief Get response headers. * @@ -209,6 +230,7 @@ class HttpContainer : public Parented { protected: size_t bytes_read_{0}; bool secure_{false}; + bool is_chunked_{false}; ///< True if response uses chunked transfer encoding std::map> response_headers_{}; }; @@ -219,7 +241,7 @@ class HttpContainer : public Parented { /// @param total_size Total bytes to read /// @param chunk_size Maximum bytes per read call /// @param timeout_ms Read timeout in milliseconds -/// @return HttpReadResult with status and error_code on failure +/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms) { size_t read_index = 0; @@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms); + auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + if (result == HttpReadLoopResult::COMPLETE) + break; // Server sent less data than requested, but transfer is complete if (result == HttpReadLoopResult::ERROR) return {HttpReadStatus::ERROR, read_bytes_or_error}; if (result == HttpReadLoopResult::TIMEOUT) @@ -393,11 +417,12 @@ template class HttpRequestSendAction : public Action { int read_or_error = container->read(buf + read_index, std::min(max_length - read_index, 512)); App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; if (result != HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT read_index += read_or_error; } response_body.reserve(read_index); diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 82538b2cb3..2f12b58766 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -135,9 +135,23 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the // early return check (bytes_read_ >= content_length) will never trigger. + // + // TODO: Chunked transfer encoding is NOT properly supported on Arduino. + // The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where + // esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr() + // returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead + // of decoded content. This wasn't noticed because requests would complete and payloads + // were only examined on IDF. The long transfer times were also masked by the misleading + // "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues: + // 1. Response body is corrupted - contains chunk size headers mixed with data + // 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout + // The proper fix would be to use getString() for chunked responses, which decodes chunks + // internally, but this buffers the entire response in memory. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; + // -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding + container->set_chunked(content_length == -1); container->duration_ms = millis() - start; return container; @@ -178,9 +192,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) - // For chunked encoding (content_length == SIZE_MAX), we can't use this check - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've read all expected content (non-chunked only) + // For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false + if (this->is_read_complete()) { return 0; // All content read successfully } // No data available - check if connection is still open diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 2b4dee953a..bd12b7d123 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -160,6 +160,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -195,6 +196,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -239,10 +241,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content - // Skip this check when content_length is 0 (chunked transfer encoding or unknown length) - // For chunked responses, esp_http_client_read() will return 0 when all data is received - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've already read all expected content (non-chunked only) + // For chunked responses (content_length == 0), esp_http_client_read() handles EOF + if (this->is_read_complete()) { return 0; // All content read successfully } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8a4b3684cf..8f4ecfab2d 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout); + auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added for OTA in the future. + if (result == HttpReadLoopResult::COMPLETE) + break; if (result != HttpReadLoopResult::DATA) { if (result == HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading data"); From 9f1a427ce235aa6ab3f1206cfd7940fa7f338b04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:03:52 +0100 Subject: [PATCH 051/251] [preferences] Use static storage for singletons and flash buffer (#13727) --- esphome/components/esp32/preferences.cpp | 7 ++++--- esphome/components/esp8266/preferences.cpp | 17 +++++++++-------- esphome/components/host/preferences.cpp | 7 ++++--- esphome/components/libretiny/preferences.cpp | 7 ++++--- esphome/components/rp2040/preferences.cpp | 17 +++++++++-------- esphome/components/zephyr/preferences.cpp | 7 ++++--- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 4e0bb68133..7d5af023b4 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -203,10 +203,11 @@ class ESP32Preferences : public ESPPreferences { } }; +static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->open(); - global_preferences = prefs; + s_preferences.open(); + global_preferences = &s_preferences; } } // namespace esp32 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 35d1cd07f7..f037b881a8 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -17,10 +17,6 @@ namespace esphome::esp8266 { static const char *const TAG = "esp8266.preferences"; -static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; @@ -43,6 +39,11 @@ static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif +static uint32_t + s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { return false; @@ -180,7 +181,6 @@ class ESP8266Preferences : public ESPPreferences { uint32_t current_flash_offset = 0; // in words void setup() { - s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT ESP_LOGVV(TAG, "Loading preferences from flash"); { @@ -283,10 +283,11 @@ class ESP8266Preferences : public ESPPreferences { } }; +static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - pref->setup(); - global_preferences = pref; + s_preferences.setup(); + global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 7b939cdebb..5ad87c1f2a 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -66,10 +66,11 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ return ESPPreferenceObject(backend); }; +static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - host_preferences = pref; - global_preferences = pref; + host_preferences = &s_preferences; + global_preferences = &s_preferences; } bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 978dcce3fa..5a60b535da 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -189,10 +189,11 @@ class LibreTinyPreferences : public ESPPreferences { } }; +static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->open(); - global_preferences = prefs; + s_preferences.open(); + global_preferences = &s_preferences; } } // namespace libretiny diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index e84033bc52..172da32adc 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -18,11 +18,12 @@ namespace rp2040 { static const char *const TAG = "rp2040.preferences"; -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static uint8_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512; -static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512; +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static uint8_t + s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation static constexpr size_t PREF_BUFFER_SIZE = 64; @@ -91,7 +92,6 @@ class RP2040Preferences : public ESPPreferences { RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} void setup() { - s_flash_storage = new uint8_t[RP2040_FLASH_STORAGE_SIZE]; // NOLINT ESP_LOGVV(TAG, "Loading preferences from flash"); memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); } @@ -149,10 +149,11 @@ class RP2040Preferences : public ESPPreferences { uint8_t *eeprom_sector_; }; +static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->setup(); - global_preferences = prefs; + s_preferences.setup(); + global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index 311133a813..f02fa16326 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -152,10 +152,11 @@ class ZephyrPreferences : public ESPPreferences { } }; +static ZephyrPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - global_preferences = prefs; - prefs->open(); + global_preferences = &s_preferences; + s_preferences.open(); } } // namespace zephyr From d41c84d624165984d1e1eaf6cde6312b4bb6f9a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:29:18 +0100 Subject: [PATCH 052/251] [wifi] Conditionally compile on_connect/on_disconnect triggers (#13684) --- esphome/components/wifi/__init__.py | 2 ++ esphome/components/wifi/automation.h | 16 ++++++++-------- esphome/components/wifi/wifi_component.cpp | 13 ++++++++++--- esphome/components/wifi/wifi_component.h | 19 ++++++++++++++----- esphome/core/defines.h | 2 ++ tests/components/wifi/common.yaml | 4 ++++ 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 98266eb589..6863e6fb62 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -585,11 +585,13 @@ async def to_code(config): await cg.past_safe_mode() if on_connect_config := config.get(CONF_ON_CONNECT): + cg.add_define("USE_WIFI_CONNECT_TRIGGER") await automation.build_automation( var.get_connect_trigger(), [], on_connect_config ) if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + cg.add_define("USE_WIFI_DISCONNECT_TRIGGER") await automation.build_automation( var.get_disconnect_trigger(), [], on_disconnect_config ) diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index fb0e71bcf6..1ad69b3992 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -48,7 +48,7 @@ template class WiFiConfigureAction : public Action, publi char ssid_buf[SSID_BUFFER_SIZE]; if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) { // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); + this->connect_trigger_.trigger(); return; } // Create a new WiFiAP object with the new SSID and password @@ -79,13 +79,13 @@ template class WiFiConfigureAction : public Action, publi // Start a timeout for the fallback if the connection to the old AP fails this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { this->connecting_ = false; - this->error_trigger_->trigger(); + this->error_trigger_.trigger(); }); }); } - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } - Trigger<> *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } + Trigger<> *get_error_trigger() { return &this->error_trigger_; } void loop() override { if (!this->connecting_) @@ -98,10 +98,10 @@ template class WiFiConfigureAction : public Action, publi char ssid_buf[SSID_BUFFER_SIZE]; if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) { // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); + this->connect_trigger_.trigger(); } else { // Callback to notify the user that the connection failed - this->error_trigger_->trigger(); + this->error_trigger_.trigger(); } } } @@ -110,8 +110,8 @@ template class WiFiConfigureAction : public Action, publi bool connecting_{false}; WiFiAP new_sta_; WiFiAP old_sta_; - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *error_trigger_{new Trigger<>()}; + Trigger<> connect_trigger_; + Trigger<> error_trigger_; }; } // namespace esphome::wifi diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 65c653a62a..c4bfdf3c42 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -651,14 +651,21 @@ void WiFiComponent::loop() { const uint32_t now = App.get_loop_component_start_time(); if (this->has_sta()) { +#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) if (this->is_connected() != this->handled_connected_state_) { +#ifdef USE_WIFI_DISCONNECT_TRIGGER if (this->handled_connected_state_) { - this->disconnect_trigger_->trigger(); - } else { - this->connect_trigger_->trigger(); + this->disconnect_trigger_.trigger(); } +#endif +#ifdef USE_WIFI_CONNECT_TRIGGER + if (!this->handled_connected_state_) { + this->connect_trigger_.trigger(); + } +#endif this->handled_connected_state_ = this->is_connected(); } +#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f27c522a1b..4bdc253f66 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -454,8 +454,12 @@ class WiFiComponent : public Component { void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; - Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } +#endif int32_t get_wifi_channel(); @@ -706,7 +710,9 @@ class WiFiComponent : public Component { // Group all boolean values together bool has_ap_{false}; +#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; +#endif bool error_from_callback_{false}; bool scan_done_{false}; bool ap_setup_{false}; @@ -733,9 +739,12 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif - // Pointers at the end (naturally aligned) - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *disconnect_trigger_{new Trigger<>()}; +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif private: // Stores a pointer to a string literal (static storage duration). diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1edc648084..ee865a7e65 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -227,6 +227,8 @@ #define USE_WIFI_SCAN_RESULTS_LISTENERS #define USE_WIFI_CONNECT_STATE_LISTENERS #define USE_WIFI_POWER_SAVE_LISTENERS +#define USE_WIFI_CONNECT_TRIGGER +#define USE_WIFI_DISCONNECT_TRIGGER #define ESPHOME_WIFI_IP_STATE_LISTENERS 2 #define ESPHOME_WIFI_SCAN_RESULTS_LISTENERS 2 #define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2 diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 7ce74ab00d..10b68347eb 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -26,3 +26,7 @@ wifi: - ssid: MySSID3 password: password3 priority: 0 + on_connect: + - logger.log: "WiFi connected!" + on_disconnect: + - logger.log: "WiFi disconnected!" From 8cb701e4128abdf0508ad90e97bffda0263accda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:29:31 +0100 Subject: [PATCH 053/251] [water_heater] Store mode strings in flash and avoid heap allocation in set_mode (#13728) --- .../components/water_heater/water_heater.cpp | 19 ++++++++++--------- .../components/water_heater/water_heater.h | 3 ++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 7266294d84..286addf7db 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/controller_registry.h" +#include "esphome/core/progmem.h" #include @@ -22,23 +23,23 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) { return *this; } -WaterHeaterCall &WaterHeaterCall::set_mode(const std::string &mode) { - if (str_equals_case_insensitive(mode, "OFF")) { +WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { + if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) { this->set_mode(WATER_HEATER_MODE_OFF); - } else if (str_equals_case_insensitive(mode, "ECO")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) { this->set_mode(WATER_HEATER_MODE_ECO); - } else if (str_equals_case_insensitive(mode, "ELECTRIC")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) { this->set_mode(WATER_HEATER_MODE_ELECTRIC); - } else if (str_equals_case_insensitive(mode, "PERFORMANCE")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) { this->set_mode(WATER_HEATER_MODE_PERFORMANCE); - } else if (str_equals_case_insensitive(mode, "HIGH_DEMAND")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) { this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); - } else if (str_equals_case_insensitive(mode, "HEAT_PUMP")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) { this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); - } else if (str_equals_case_insensitive(mode, "GAS")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) { this->set_mode(WATER_HEATER_MODE_GAS); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode); } return *this; } diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 84fc46d208..7bd05ba7f5 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -75,7 +75,8 @@ class WaterHeaterCall { WaterHeaterCall(WaterHeater *parent); WaterHeaterCall &set_mode(WaterHeaterMode mode); - WaterHeaterCall &set_mode(const std::string &mode); + WaterHeaterCall &set_mode(const char *mode); + WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); } WaterHeaterCall &set_target_temperature(float temperature); WaterHeaterCall &set_target_temperature_low(float temperature); WaterHeaterCall &set_target_temperature_high(float temperature); From 9d63642bdb16598ab8c0ec3eafbb00178df96955 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:29:43 +0100 Subject: [PATCH 054/251] [media_player] Store command strings in flash and avoid heap allocation in set_command (#13731) --- .../components/media_player/media_player.cpp | 21 ++++++++++--------- .../components/media_player/media_player.h | 3 ++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index b46ec39d30..17d9b054da 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace media_player { @@ -107,25 +108,25 @@ MediaPlayerCall &MediaPlayerCall::set_command(optional comma this->command_ = command; return *this; } -MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) { - if (str_equals_case_insensitive(command, "PLAY")) { +MediaPlayerCall &MediaPlayerCall::set_command(const char *command) { + if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PLAY")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_PLAY); - } else if (str_equals_case_insensitive(command, "PAUSE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PAUSE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_PAUSE); - } else if (str_equals_case_insensitive(command, "STOP")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_STOP); - } else if (str_equals_case_insensitive(command, "MUTE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("MUTE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_MUTE); - } else if (str_equals_case_insensitive(command, "UNMUTE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNMUTE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE); - } else if (str_equals_case_insensitive(command, "TOGGLE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE); - } else if (str_equals_case_insensitive(command, "TURN_ON")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_ON")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); - } else if (str_equals_case_insensitive(command, "TURN_OFF")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); } return *this; } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index b753e2d088..f75a68dd85 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -114,7 +114,8 @@ class MediaPlayerCall { MediaPlayerCall &set_command(MediaPlayerCommand command); MediaPlayerCall &set_command(optional command); - MediaPlayerCall &set_command(const std::string &command); + MediaPlayerCall &set_command(const char *command); + MediaPlayerCall &set_command(const std::string &command) { return this->set_command(command.c_str()); } MediaPlayerCall &set_media_url(const std::string &url); From fbeb0e8e542af3b8b4f2f86a1fc8d46e2bb915ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 04:40:44 +0100 Subject: [PATCH 055/251] [opentherm] Fix ESP-IDF build by re-enabling legacy driver component (#13732) --- esphome/components/opentherm/__init__.py | 8 ++++++++ esphome/components/opentherm/opentherm.cpp | 2 ++ esphome/components/opentherm/opentherm.h | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index 8cbee1eed2..dddb9dc891 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -4,8 +4,10 @@ from typing import Any from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266 +from esphome.core import CORE from . import const, generate, schema, validate @@ -83,6 +85,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config: dict[str, Any]) -> None: + if CORE.is_esp32: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # Provides driver/timer.h header for hardware timer API + # TODO: Remove this once opentherm migrates to GPTimer API (driver/gptimer.h) + include_builtin_idf_component("driver") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 2bf438a52f..cdf89207bc 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -8,6 +8,8 @@ #include "opentherm.h" #include "esphome/core/helpers.h" #include +// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) +// The legacy timer API is deprecated in ESP-IDF 5.x. See opentherm.h for details. #ifdef USE_ESP32 #include "driver/timer.h" #include "esp_err.h" diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index 3996481760..a2c347d0d8 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -12,6 +12,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) +// The legacy timer API is deprecated in ESP-IDF 5.x. Migration would allow removing the +// "driver" IDF component dependency. See: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id4 #ifdef USE_ESP32 #include "driver/timer.h" #endif From a430b3a42606696bda318af4314c1be26103a0b5 Mon Sep 17 00:00:00 2001 From: Roger Fachini Date: Mon, 2 Feb 2026 19:46:46 -0800 Subject: [PATCH 056/251] [speaker.media_player]: Add verbose error message for puremagic parsing (#13725) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/speaker/media_player/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 370b4576a7..034312236c 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -157,8 +157,14 @@ def _read_audio_file_and_type(file_config): import puremagic - file_type: str = puremagic.from_string(data) - file_type = file_type.removeprefix(".") + try: + file_type: str = puremagic.from_string(data) + file_type = file_type.removeprefix(".") + except puremagic.PureError as e: + raise cv.Invalid( + f"Unable to determine audio file type of '{path}'. " + f"Try re-encoding the file into a supported format. Details: {e}" + ) media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] if file_type in ("wav"): From ff6f7d3248926c7594a2de969b3fc2f6d93b3550 Mon Sep 17 00:00:00 2001 From: Andrew Gillis Date: Mon, 2 Feb 2026 22:59:51 -0500 Subject: [PATCH 057/251] [mipi_dsi] Add WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B (#13608) --- .../components/mipi_dsi/models/waveshare.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index c3d080f8b2..f83cacc5a3 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -94,3 +94,29 @@ DriverChip( (0x29, 0x00), ], ) + +DriverChip( + "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B", + height=600, + width=1024, + hsync_back_porch=160, + hsync_pulse_width=10, + hsync_front_porch=160, + vsync_back_porch=23, + vsync_pulse_width=1, + vsync_front_porch=12, + pclk_frequency="52MHz", + lane_bit_rate="900Mbps", + no_transform=True, + color_order="RGB", + initsequence=[ + (0x80, 0x8B), + (0x81, 0x78), + (0x82, 0x84), + (0x83, 0x88), + (0x84, 0xA8), + (0x85, 0xE3), + (0x86, 0x88), + (0xB2, 0x10), + ], +) From d4110bf6507be83235a5c397dd442574519cf99d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 05:29:24 +0100 Subject: [PATCH 058/251] [lock] Store state strings in flash and avoid heap allocation in set_state (#13729) --- esphome/components/lock/lock.cpp | 17 +++++++++-------- esphome/components/lock/lock.h | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 9fa1ba3600..4c61418256 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::lock { @@ -84,21 +85,21 @@ LockCall &LockCall::set_state(optional state) { this->state_ = state; return *this; } -LockCall &LockCall::set_state(const std::string &state) { - if (str_equals_case_insensitive(state, "LOCKED")) { +LockCall &LockCall::set_state(const char *state) { + if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) { this->set_state(LOCK_STATE_LOCKED); - } else if (str_equals_case_insensitive(state, "UNLOCKED")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) { this->set_state(LOCK_STATE_UNLOCKED); - } else if (str_equals_case_insensitive(state, "JAMMED")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) { this->set_state(LOCK_STATE_JAMMED); - } else if (str_equals_case_insensitive(state, "LOCKING")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) { this->set_state(LOCK_STATE_LOCKING); - } else if (str_equals_case_insensitive(state, "UNLOCKING")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) { this->set_state(LOCK_STATE_UNLOCKING); - } else if (str_equals_case_insensitive(state, "NONE")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("NONE")) == 0) { this->set_state(LOCK_STATE_NONE); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state); } return *this; } diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 69fc405713..bebd296eac 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -83,7 +83,8 @@ class LockCall { /// Set the state of the lock device. LockCall &set_state(optional state); /// Set the state of the lock device based on a string. - LockCall &set_state(const std::string &state); + LockCall &set_state(const char *state); + LockCall &set_state(const std::string &state) { return this->set_state(state.c_str()); } void perform(); From b3e09e5c68b4734bb8cd896ce8aedd00523e1a02 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:14:09 +1100 Subject: [PATCH 059/251] [key_collector] Add text sensor and allow multiple callbacks (#13617) --- esphome/components/key_collector/__init__.py | 91 +++++++++++++------ .../key_collector/key_collector.cpp | 45 +++++---- .../components/key_collector/key_collector.h | 36 ++++---- .../key_collector/text_sensor/__init__.py | 28 ++++++ tests/components/key_collector/common.yaml | 14 +++ 5 files changed, 146 insertions(+), 68 deletions(-) create mode 100644 esphome/components/key_collector/text_sensor/__init__.py diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index 17af40da1a..badb28c32c 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -1,4 +1,7 @@ +from dataclasses import dataclass + from esphome import automation +from esphome.automation import Trigger import esphome.codegen as cg from esphome.components import key_provider import esphome.config_validation as cv @@ -10,7 +13,10 @@ from esphome.const import ( CONF_ON_TIMEOUT, CONF_SOURCE_ID, CONF_TIMEOUT, + CONF_TRIGGER_ID, ) +from esphome.cpp_generator import MockObj, literal +from esphome.types import TemplateArgsType CODEOWNERS = ["@ssieb"] @@ -32,22 +38,50 @@ KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component) EnableAction = key_collector_ns.class_("EnableAction", automation.Action) DisableAction = key_collector_ns.class_("DisableAction", automation.Action) +X_TYPE = cg.std_string_ref.operator("const") + + +@dataclass +class Argument: + type: MockObj + name: str + + +TRIGGER_TYPES = { + CONF_ON_PROGRESS: [Argument(X_TYPE, "x"), Argument(cg.uint8, "start")], + CONF_ON_RESULT: [ + Argument(X_TYPE, "x"), + Argument(cg.uint8, "start"), + Argument(cg.uint8, "end"), + ], + CONF_ON_TIMEOUT: [Argument(X_TYPE, "x"), Argument(cg.uint8, "start")], +} + CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(KeyCollector), - cv.Optional(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider), - cv.Optional(CONF_MIN_LENGTH): cv.int_, - cv.Optional(CONF_MAX_LENGTH): cv.int_, + cv.Optional(CONF_SOURCE_ID): cv.ensure_list( + cv.use_id(key_provider.KeyProvider) + ), + cv.Optional(CONF_MIN_LENGTH): cv.uint16_t, + cv.Optional(CONF_MAX_LENGTH): cv.uint16_t, cv.Optional(CONF_START_KEYS): cv.string, cv.Optional(CONF_END_KEYS): cv.string, cv.Optional(CONF_END_KEY_REQUIRED): cv.boolean, cv.Optional(CONF_BACK_KEYS): cv.string, cv.Optional(CONF_CLEAR_KEYS): cv.string, cv.Optional(CONF_ALLOWED_KEYS): cv.string, - cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(single=True), - cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True), + **{ + cv.Optional(trigger_type): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(*[arg.type for arg in args]) + ), + } + ) + for trigger_type, args in TRIGGER_TYPES.items() + }, cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, } @@ -59,9 +93,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CONF_SOURCE_ID in config: - source = await cg.get_variable(config[CONF_SOURCE_ID]) - cg.add(var.set_provider(source)) + for source_conf in config.get(CONF_SOURCE_ID, ()): + source = await cg.get_variable(source_conf) + cg.add(var.add_provider(source)) if CONF_MIN_LENGTH in config: cg.add(var.set_min_length(config[CONF_MIN_LENGTH])) if CONF_MAX_LENGTH in config: @@ -78,26 +112,25 @@ async def to_code(config): cg.add(var.set_clear_keys(config[CONF_CLEAR_KEYS])) if CONF_ALLOWED_KEYS in config: cg.add(var.set_allowed_keys(config[CONF_ALLOWED_KEYS])) - if CONF_ON_PROGRESS in config: - await automation.build_automation( - var.get_progress_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start")], - config[CONF_ON_PROGRESS], - ) - if CONF_ON_RESULT in config: - await automation.build_automation( - var.get_result_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start"), (cg.uint8, "end")], - config[CONF_ON_RESULT], - ) - if CONF_ON_TIMEOUT in config: - await automation.build_automation( - var.get_timeout_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start")], - config[CONF_ON_TIMEOUT], - ) - if CONF_TIMEOUT in config: - cg.add(var.set_timeout(config[CONF_TIMEOUT])) + + for trigger_name, args in TRIGGER_TYPES.items(): + arglist: TemplateArgsType = [(arg.type, arg.name) for arg in args] + for conf in config.get(trigger_name, ()): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + add_trig = getattr( + var, + f"add_on_{trigger_name.rsplit('_', maxsplit=1)[-1].lower()}_callback", + ) + await automation.build_automation( + trigger, + arglist, + conf, + ) + lamb = trigger.trigger(*[literal(arg.name) for arg in args]) + cg.add(add_trig(await cg.process_lambda(lamb, arglist))) + + if timeout := config.get(CONF_TIMEOUT): + cg.add(var.set_timeout(timeout)) cg.add(var.set_enabled(config[CONF_ENABLE_ON_BOOT])) diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp index 9cfc74f50e..68d1c60bf9 100644 --- a/esphome/components/key_collector/key_collector.cpp +++ b/esphome/components/key_collector/key_collector.cpp @@ -7,15 +7,10 @@ namespace key_collector { static const char *const TAG = "key_collector"; -KeyCollector::KeyCollector() - : progress_trigger_(new Trigger()), - result_trigger_(new Trigger()), - timeout_trigger_(new Trigger()) {} - void KeyCollector::loop() { if ((this->timeout_ == 0) || this->result_.empty() || (millis() - this->last_key_time_ < this->timeout_)) return; - this->timeout_trigger_->trigger(this->result_, this->start_key_); + this->timeout_callbacks_.call(this->result_, this->start_key_); this->clear(); } @@ -43,64 +38,68 @@ void KeyCollector::dump_config() { ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0); } -void KeyCollector::set_provider(key_provider::KeyProvider *provider) { - provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); }); +void KeyCollector::add_provider(key_provider::KeyProvider *provider) { + provider->add_on_key_callback([this](uint8_t key) { this->send_key(key); }); } void KeyCollector::set_enabled(bool enabled) { this->enabled_ = enabled; - if (!enabled) + if (!enabled) { this->clear(false); + } } void KeyCollector::clear(bool progress_update) { + auto had_state = !this->result_.empty() || this->start_key_ != 0; this->result_.clear(); this->start_key_ = 0; - if (progress_update) - this->progress_trigger_->trigger(this->result_, 0); + if (progress_update && had_state) { + this->progress_callbacks_.call(this->result_, 0); + } + this->disable_loop(); } -void KeyCollector::send_key(uint8_t key) { this->key_pressed_(key); } - -void KeyCollector::key_pressed_(uint8_t key) { +void KeyCollector::send_key(uint8_t key) { if (!this->enabled_) return; this->last_key_time_ = millis(); if (!this->start_keys_.empty() && !this->start_key_) { if (this->start_keys_.find(key) != std::string::npos) { this->start_key_ = key; - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } return; } if (this->back_keys_.find(key) != std::string::npos) { if (!this->result_.empty()) { this->result_.pop_back(); - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } return; } if (this->clear_keys_.find(key) != std::string::npos) { - if (!this->result_.empty()) - this->clear(); + this->clear(); return; } if (this->end_keys_.find(key) != std::string::npos) { if ((this->min_length_ == 0) || (this->result_.size() >= this->min_length_)) { - this->result_trigger_->trigger(this->result_, this->start_key_, key); + this->result_callbacks_.call(this->result_, this->start_key_, key); this->clear(); } return; } - if (!this->allowed_keys_.empty() && (this->allowed_keys_.find(key) == std::string::npos)) + if (!this->allowed_keys_.empty() && this->allowed_keys_.find(key) == std::string::npos) return; - if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) + if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) { + if (this->result_.empty()) + this->enable_loop(); this->result_.push_back(key); + } if ((this->max_length_ > 0) && (this->result_.size() == this->max_length_) && (!this->end_key_required_)) { - this->result_trigger_->trigger(this->result_, this->start_key_, 0); + this->result_callbacks_.call(this->result_, this->start_key_, 0); this->clear(false); } - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } } // namespace key_collector diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 735f396809..8e30c333df 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -3,27 +3,33 @@ #include #include "esphome/components/key_provider/key_provider.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" namespace esphome { namespace key_collector { class KeyCollector : public Component { public: - KeyCollector(); void loop() override; void dump_config() override; - void set_provider(key_provider::KeyProvider *provider); - void set_min_length(uint32_t min_length) { this->min_length_ = min_length; }; - void set_max_length(uint32_t max_length) { this->max_length_ = max_length; }; + void add_provider(key_provider::KeyProvider *provider); + void set_min_length(uint16_t min_length) { this->min_length_ = min_length; }; + void set_max_length(uint16_t max_length) { this->max_length_ = max_length; }; void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; void set_back_keys(std::string back_keys) { this->back_keys_ = std::move(back_keys); }; void set_clear_keys(std::string clear_keys) { this->clear_keys_ = std::move(clear_keys); }; void set_allowed_keys(std::string allowed_keys) { this->allowed_keys_ = std::move(allowed_keys); }; - Trigger *get_progress_trigger() const { return this->progress_trigger_; }; - Trigger *get_result_trigger() const { return this->result_trigger_; }; - Trigger *get_timeout_trigger() const { return this->timeout_trigger_; }; + void add_on_progress_callback(std::function &&callback) { + this->progress_callbacks_.add(std::move(callback)); + } + void add_on_result_callback(std::function &&callback) { + this->result_callbacks_.add(std::move(callback)); + } + void add_on_timeout_callback(std::function &&callback) { + this->timeout_callbacks_.add(std::move(callback)); + } void set_timeout(int timeout) { this->timeout_ = timeout; }; void set_enabled(bool enabled); @@ -31,10 +37,8 @@ class KeyCollector : public Component { void send_key(uint8_t key); protected: - void key_pressed_(uint8_t key); - - uint32_t min_length_{0}; - uint32_t max_length_{0}; + uint16_t min_length_{0}; + uint16_t max_length_{0}; std::string start_keys_; std::string end_keys_; bool end_key_required_{false}; @@ -43,12 +47,12 @@ class KeyCollector : public Component { std::string allowed_keys_; std::string result_; uint8_t start_key_{0}; - Trigger *progress_trigger_; - Trigger *result_trigger_; - Trigger *timeout_trigger_; - uint32_t last_key_time_; + LazyCallbackManager progress_callbacks_; + LazyCallbackManager result_callbacks_; + LazyCallbackManager timeout_callbacks_; + uint32_t last_key_time_{}; uint32_t timeout_{0}; - bool enabled_; + bool enabled_{}; }; template class EnableAction : public Action, public Parented { diff --git a/esphome/components/key_collector/text_sensor/__init__.py b/esphome/components/key_collector/text_sensor/__init__.py new file mode 100644 index 0000000000..1676cf7bdf --- /dev/null +++ b/esphome/components/key_collector/text_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +from esphome.components.text_sensor import TextSensor +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.cpp_generator import literal +from esphome.types import TemplateArgsType + +from .. import CONF_ON_RESULT, CONF_SOURCE_ID, TRIGGER_TYPES, KeyCollector + +CONFIG_SCHEMA = text_sensor.text_sensor_schema(TextSensor).extend( + { + cv.GenerateID(CONF_SOURCE_ID): cv.use_id(KeyCollector), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_SOURCE_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + await text_sensor.register_text_sensor(var, config) + args = TRIGGER_TYPES[CONF_ON_RESULT] + arglist: TemplateArgsType = [(arg.type, arg.name) for arg in args] + cg.add( + parent.add_on_result_callback( + await cg.process_lambda(var.publish_state(literal(args[0].name)), arglist) + ) + ) diff --git a/tests/components/key_collector/common.yaml b/tests/components/key_collector/common.yaml index 12e541c865..43a0478a18 100644 --- a/tests/components/key_collector/common.yaml +++ b/tests/components/key_collector/common.yaml @@ -18,14 +18,23 @@ key_collector: - logger.log: format: "input progress: '%s', started by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)"] + - logger.log: + format: "second listener - progress: '%s'" + args: ['x.c_str()'] on_result: - logger.log: format: "input result: '%s', started by '%c', ended by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + - logger.log: + format: "second listener - result: '%s'" + args: ['x.c_str()'] on_timeout: - logger.log: format: "input timeout: '%s', started by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)"] + - logger.log: + format: "second listener - timeout: '%s'" + args: ['x.c_str()'] enable_on_boot: false button: @@ -34,3 +43,8 @@ button: on_press: - key_collector.enable: - key_collector.disable: + +text_sensor: + - platform: key_collector + id: collected_keys + source_id: reader From c027d9116febad4cd55d647b2090f22b4f3467e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 12:13:03 +0100 Subject: [PATCH 060/251] [template] Add additional tests for template select (#13741) --- tests/components/template/common-base.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 9dc65fbab8..9dece7c3a5 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -296,6 +296,16 @@ select: // Migration guide: Store in std::string std::string stored_option(id(template_select).current_option()); ESP_LOGI("test", "Stored: %s", stored_option.c_str()); + - platform: template + id: template_select_with_action + name: "Template select with action" + options: + - option_a + - option_b + set_action: + - logger.log: + format: "Selected: %s" + args: ["x.c_str()"] lock: - platform: template From f4d7d06c41a5347ae94bd857e8cfe62f1da5b8ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 12:23:05 +0100 Subject: [PATCH 061/251] [dlms_meter] Rename test UART package key to match directory name (#13743) --- tests/components/dlms_meter/test.esp32-ard.yaml | 2 +- tests/components/dlms_meter/test.esp32-idf.yaml | 2 +- tests/components/dlms_meter/test.esp8266-ard.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/dlms_meter/test.esp32-ard.yaml b/tests/components/dlms_meter/test.esp32-ard.yaml index 4f8a06c31b..c9910aa600 100644 --- a/tests/components/dlms_meter/test.esp32-ard.yaml +++ b/tests/components/dlms_meter/test.esp32-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml + uart_2400: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml <<: !include common-generic.yaml diff --git a/tests/components/dlms_meter/test.esp32-idf.yaml b/tests/components/dlms_meter/test.esp32-idf.yaml index f993515fce..1547532f1e 100644 --- a/tests/components/dlms_meter/test.esp32-idf.yaml +++ b/tests/components/dlms_meter/test.esp32-idf.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml + uart_2400: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml <<: !include common-netznoe.yaml diff --git a/tests/components/dlms_meter/test.esp8266-ard.yaml b/tests/components/dlms_meter/test.esp8266-ard.yaml index 2ce7955c9f..119a1978de 100644 --- a/tests/components/dlms_meter/test.esp8266-ard.yaml +++ b/tests/components/dlms_meter/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml + uart_2400: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml <<: !include common-generic.yaml From d0017ded5bc0cb62204892802986198694624ab2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 12:48:31 +0100 Subject: [PATCH 062/251] [template] Split TemplateSelect into TemplateSelectWithSetAction to save RAM (#13685) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- .../components/template/select/__init__.py | 87 +++++++++++++------ .../template/select/template_select.cpp | 73 ++++++---------- .../template/select/template_select.h | 78 +++++++++++++---- .../fixtures/select_stringref_trigger.yaml | 16 +++- .../test_select_stringref_trigger.py | 24 +++-- 5 files changed, 180 insertions(+), 98 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 574f1f5fb7..8de0b65c2d 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -10,35 +10,65 @@ from esphome.const import ( CONF_OPTIONS, CONF_RESTORE_VALUE, CONF_SET_ACTION, + CONF_UPDATE_INTERVAL, + SCHEDULER_DONT_RUN, ) +from esphome.core import TimePeriodMilliseconds +from esphome.cpp_generator import TemplateArguments from .. import template_ns TemplateSelect = template_ns.class_( "TemplateSelect", select.Select, cg.PollingComponent ) +TemplateSelectWithSetAction = template_ns.class_( + "TemplateSelectWithSetAction", TemplateSelect +) def validate(config): + errors = [] if CONF_LAMBDA in config: if config[CONF_OPTIMISTIC]: - raise cv.Invalid("optimistic cannot be used with lambda") + errors.append( + cv.Invalid( + "optimistic cannot be used with lambda", path=[CONF_OPTIMISTIC] + ) + ) if CONF_INITIAL_OPTION in config: - raise cv.Invalid("initial_value cannot be used with lambda") + errors.append( + cv.Invalid( + "initial_value cannot be used with lambda", + path=[CONF_INITIAL_OPTION], + ) + ) if CONF_RESTORE_VALUE in config: - raise cv.Invalid("restore_value cannot be used with lambda") + errors.append( + cv.Invalid( + "restore_value cannot be used with lambda", + path=[CONF_RESTORE_VALUE], + ) + ) elif CONF_INITIAL_OPTION in config: if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: - raise cv.Invalid( - f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + errors.append( + cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]", + path=[CONF_INITIAL_OPTION], + ) ) else: config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: - raise cv.Invalid( - "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + errors.append( + cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + ) ) + if errors: + raise cv.MultipleInvalid(errors) + return config @@ -62,29 +92,34 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await select.register_select(var, config, options=config[CONF_OPTIONS]) + var_id = config[CONF_ID] + if CONF_SET_ACTION in config: + var_id.type = TemplateSelectWithSetAction + has_lambda = CONF_LAMBDA in config + optimistic = config.get(CONF_OPTIMISTIC, False) + restore_value = config.get(CONF_RESTORE_VALUE, False) + options = config[CONF_OPTIONS] + initial_option = config.get(CONF_INITIAL_OPTION, 0) + initial_option_index = options.index(initial_option) if not has_lambda else 0 + + var = cg.new_Pvariable( + var_id, + TemplateArguments(has_lambda, optimistic, restore_value, initial_option_index), + ) + component_config = config.copy() + if not has_lambda: + # No point in polling if not using a lambda + component_config[CONF_UPDATE_INTERVAL] = TimePeriodMilliseconds( + milliseconds=SCHEDULER_DONT_RUN + ) + await cg.register_component(var, component_config) + await select.register_select(var, config, options=options) if CONF_LAMBDA in config: - template_ = await cg.process_lambda( + lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) ) - cg.add(var.set_template(template_)) - - else: - # Only set if non-default to avoid bloating setup() function - if config[CONF_OPTIMISTIC]: - cg.add(var.set_optimistic(True)) - initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) - # Only set if non-zero to avoid bloating setup() function - # (initial_option_index_ is zero-initialized in the header) - if initial_option_index != 0: - cg.add(var.set_initial_option_index(initial_option_index)) - - # Only set if True (default is False) - if config.get(CONF_RESTORE_VALUE): - cg.add(var.set_restore_value(True)) + cg.add(var.set_lambda(lambda_)) if CONF_SET_ACTION in config: await automation.build_automation( diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index fa34aa9fa7..e68729c2d4 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -5,61 +5,44 @@ namespace esphome::template_ { static const char *const TAG = "template.select"; -void TemplateSelect::setup() { - if (this->f_.has_value()) - return; - - size_t index = this->initial_option_index_; - if (this->restore_value_) { - this->pref_ = this->make_entity_preference(); - size_t restored_index; - if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { - index = restored_index; - ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); - } else { - ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); - } +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, + const size_t initial_option_index, bool restore_value) { + LOG_SELECT("", "Template Select", sel_comp); + if (has_lambda) { + LOG_UPDATE_INTERVAL(sel_comp); } else { - ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); + ESP_LOGCONFIG(TAG, + " Optimistic: %s\n" + " Initial Option: %s\n" + " Restore Value: %s", + YESNO(optimistic), sel_comp->option_at(initial_option_index), YESNO(restore_value)); } - - this->publish_state(index); } -void TemplateSelect::update() { - if (!this->f_.has_value()) - return; +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index) { + ESP_LOGD(TAG, "State from initial: %s", sel_comp->option_at(initial_index)); + sel_comp->publish_state(initial_index); +} - auto val = this->f_(); +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index) { + size_t index = initial_index; + if (pref.load(&index) && sel_comp->has_index(index)) { + ESP_LOGD(TAG, "State from restore: %s", sel_comp->option_at(index)); + } else { + index = initial_index; + ESP_LOGD(TAG, "State from initial (no valid stored index): %s", sel_comp->option_at(initial_index)); + } + sel_comp->publish_state(index); +} + +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val) { if (val.has_value()) { - if (!this->has_option(*val)) { + if (!sel_comp->has_option(*val)) { ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); return; } - this->publish_state(*val); + sel_comp->publish_state(*val); } } -void TemplateSelect::control(size_t index) { - this->set_trigger_->trigger(StringRef(this->option_at(index))); - - if (this->optimistic_) - this->publish_state(index); - - if (this->restore_value_) - this->pref_.save(&index); -} - -void TemplateSelect::dump_config() { - LOG_SELECT("", "Template Select", this); - LOG_UPDATE_INTERVAL(this); - if (this->f_.has_value()) - return; - ESP_LOGCONFIG(TAG, - " Optimistic: %s\n" - " Initial Option: %s\n" - " Restore Value: %s", - YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); -} - } // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 114d25b9ce..5da6d732bd 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,29 +9,71 @@ namespace esphome::template_ { -class TemplateSelect final : public select::Select, public PollingComponent { - public: - template void set_template(F &&f) { this->f_.set(std::forward(f)); } +struct Empty {}; +class BaseTemplateSelect : public select::Select, public PollingComponent {}; - void setup() override; - void update() override; - void dump_config() override; +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index, + bool restore_value); +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index); +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index); +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val); + +/// Base template select class - used when no set_action is configured + +template +class TemplateSelect : public BaseTemplateSelect { + public: + template void set_lambda(F &&f) { + if constexpr (HAS_LAMBDA) { + this->f_.set(std::forward(f)); + } + } + + void setup() override { + if constexpr (!HAS_LAMBDA) { + if constexpr (RESTORE_VALUE) { + this->pref_ = this->template make_entity_preference(); + setup_with_restore(this, this->pref_, INITIAL_OPTION_INDEX); + } else { + setup_initial(this, INITIAL_OPTION_INDEX); + } + } + } + + void update() override { + if constexpr (HAS_LAMBDA) { + update_lambda(this, this->f_()); + } + } + void dump_config() override { + dump_config_helper(this, OPTIMISTIC, HAS_LAMBDA, INITIAL_OPTION_INDEX, RESTORE_VALUE); + }; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } - void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } - void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + protected: + void control(size_t index) override { + if constexpr (OPTIMISTIC) + this->publish_state(index); + if constexpr (RESTORE_VALUE) + this->pref_.save(&index); + } + [[no_unique_address]] std::conditional_t, Empty> f_{}; + [[no_unique_address]] std::conditional_t pref_{}; +}; + +/// Template select with set_action trigger - only instantiated when set_action is configured +template +class TemplateSelectWithSetAction final + : public TemplateSelect { + public: + Trigger *get_set_trigger() { return &this->set_trigger_; } protected: - void control(size_t index) override; - bool optimistic_ = false; - size_t initial_option_index_{0}; - bool restore_value_ = false; - Trigger *set_trigger_ = new Trigger(); - TemplateLambda f_; - - ESPPreferenceObject pref_; + void control(size_t index) override { + this->set_trigger_.trigger(StringRef(this->option_at(index))); + TemplateSelect::control(index); + } + Trigger set_trigger_; }; } // namespace esphome::template_ diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml index bb1e1fd843..5858e2f529 100644 --- a/tests/integration/fixtures/select_stringref_trigger.yaml +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -56,7 +56,21 @@ select: std::string prefix = x.substr(0, 6); ESP_LOGI("test", "Substr prefix: %s", prefix.c_str()); - # Second select with numeric options to test ADL functions + # Second select with set_action trigger (uses TemplateSelectWithSetAction subclass) + - platform: template + name: "Action Select" + id: action_select + options: + - "Action A" + - "Action B" + set_action: + then: + # Test: set_action trigger receives StringRef + - logger.log: + format: "set_action triggered: %s" + args: ['x.c_str()'] + + # Third select with numeric options to test ADL functions - platform: template name: "Baud Rate" id: baud_select diff --git a/tests/integration/test_select_stringref_trigger.py b/tests/integration/test_select_stringref_trigger.py index 7fc72a2290..5baba9c7f5 100644 --- a/tests/integration/test_select_stringref_trigger.py +++ b/tests/integration/test_select_stringref_trigger.py @@ -28,6 +28,8 @@ async def test_select_stringref_trigger( find_substr_future = loop.create_future() find_char_future = loop.create_future() substr_future = loop.create_future() + # set_action trigger (TemplateSelectWithSetAction subclass) + set_action_future = loop.create_future() # ADL functions stoi_future = loop.create_future() stol_future = loop.create_future() @@ -43,6 +45,8 @@ async def test_select_stringref_trigger( find_substr_pattern = re.compile(r"Found 'Option' in value") find_char_pattern = re.compile(r"Space at position: 6") # space at index 6 substr_pattern = re.compile(r"Substr prefix: Option") + # set_action trigger pattern (TemplateSelectWithSetAction subclass) + set_action_pattern = re.compile(r"set_action triggered: Action B") # ADL function patterns (115200 from baud rate select) stoi_pattern = re.compile(r"stoi result: 115200") stol_pattern = re.compile(r"stol result: 115200") @@ -67,6 +71,9 @@ async def test_select_stringref_trigger( find_char_future.set_result(True) if not substr_future.done() and substr_pattern.search(line): substr_future.set_result(True) + # set_action trigger + if not set_action_future.done() and set_action_pattern.search(line): + set_action_future.set_result(True) # ADL functions if not stoi_future.done() and stoi_pattern.search(line): stoi_future.set_result(True) @@ -89,22 +96,21 @@ async def test_select_stringref_trigger( # List entities to find our select entities, _ = await client.list_entities_services() - select_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Test Select"), - None, - ) + select_entity = next((e for e in entities if e.name == "Test Select"), None) assert select_entity is not None, "Test Select entity not found" - baud_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"), - None, - ) + baud_entity = next((e for e in entities if e.name == "Baud Rate"), None) assert baud_entity is not None, "Baud Rate entity not found" + action_entity = next((e for e in entities if e.name == "Action Select"), None) + assert action_entity is not None, "Action Select entity not found" + # Change select to Option B - this should trigger on_value with StringRef client.select_command(select_entity.key, "Option B") # Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod) client.select_command(baud_entity.key, "115200") + # Change action select - tests set_action trigger (TemplateSelectWithSetAction) + client.select_command(action_entity.key, "Action B") # Wait for all log messages confirming StringRef operations work try: @@ -118,6 +124,7 @@ async def test_select_stringref_trigger( find_substr_future, find_char_future, substr_future, + set_action_future, stoi_future, stol_future, stof_future, @@ -135,6 +142,7 @@ async def test_select_stringref_trigger( "find_substr": find_substr_future.done(), "find_char": find_char_future.done(), "substr": substr_future.done(), + "set_action": set_action_future.done(), "stoi": stoi_future.done(), "stol": stol_future.done(), "stof": stof_future.done(), From 21bd0ff6aaa23bc32e4ff52f850da33bd4cfc5f2 Mon Sep 17 00:00:00 2001 From: Tomer Shalev Date: Tue, 3 Feb 2026 15:37:27 +0200 Subject: [PATCH 063/251] [mqtt] Stop sending deprecated color_mode and brightness in light discovery (fixes #13666) (#13667) --- esphome/components/mqtt/mqtt_light.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 3e3537fa5c..aa47bdf996 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -47,7 +47,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery root[ESPHOME_F("schema")] = ESPHOME_F("json"); auto traits = this->state_->get_traits(); - root[MQTT_COLOR_MODE] = true; // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson JsonArray color_modes = root[ESPHOME_F("supported_color_modes")].to(); if (traits.supports_color_mode(ColorMode::ON_OFF)) @@ -68,10 +67,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) color_modes.add(ESPHOME_F("rgbww")); - // legacy API - if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) - root[ESPHOME_F("brightness")] = true; - if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { root[MQTT_MIN_MIREDS] = traits.get_min_mireds(); From 8d0ce49eb4b6d25761d59442f890e95705641527 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:34:15 +0100 Subject: [PATCH 064/251] [api] Eliminate intermediate buffers in protobuf dump helpers (#13742) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_pb2_dump.cpp | 27 ++++++------------------- esphome/components/api/proto.h | 14 +++++++++++++ script/api_protobuf/api_protobuf.py | 27 ++++++------------------- 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 29121f05e0..e9db36ae21 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -23,15 +23,8 @@ static inline void append_field_prefix(DumpBuffer &out, const char *field_name, out.append(indent, ' ').append(field_name).append(": "); } -static inline void append_with_newline(DumpBuffer &out, const char *str) { - out.append(str); - out.append("\n"); -} - static inline void append_uint(DumpBuffer &out, uint32_t value) { - char buf[16]; - snprintf(buf, sizeof(buf), "%" PRIu32, value); - out.append(buf); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value)); } // RAII helper for message dump formatting @@ -49,31 +42,23 @@ class MessageDumpHelper { // Helper functions to reduce code duplication in dump methods static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRId32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%g", value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu64, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) { @@ -112,7 +97,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); - append_with_newline(out, hex_buf); + out.append(hex_buf).append("\n"); } template<> const char *proto_enum_to_string(enums::EntityCategory value) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 2e0df297c3..552b4a4625 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -402,6 +402,20 @@ class DumpBuffer { const char *c_str() const { return buf_; } size_t size() const { return pos_; } + /// Get writable buffer pointer for use with buf_append_printf + char *data() { return buf_; } + /// Get current position for use with buf_append_printf + size_t pos() const { return pos_; } + /// Update position after buf_append_printf call + void set_pos(size_t pos) { + if (pos >= CAPACITY) { + pos_ = CAPACITY - 1; + } else { + pos_ = pos; + } + buf_[pos_] = '\0'; + } + private: void append_impl_(const char *str, size_t len) { size_t space = CAPACITY - 1 - pos_; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7625458f9f..72103285e8 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2599,15 +2599,8 @@ static inline void append_field_prefix(DumpBuffer &out, const char *field_name, out.append(indent, ' ').append(field_name).append(": "); } -static inline void append_with_newline(DumpBuffer &out, const char *str) { - out.append(str); - out.append("\\n"); -} - static inline void append_uint(DumpBuffer &out, uint32_t value) { - char buf[16]; - snprintf(buf, sizeof(buf), "%" PRIu32, value); - out.append(buf); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value)); } // RAII helper for message dump formatting @@ -2625,31 +2618,23 @@ class MessageDumpHelper { // Helper functions to reduce code duplication in dump methods static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRId32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%g", value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu64, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) { @@ -2689,7 +2674,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); - append_with_newline(out, hex_buf); + out.append(hex_buf).append("\\n"); } """ From 18f7e0e6b33526821b99cefd0b9575dbe68eee6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:42:45 +0100 Subject: [PATCH 065/251] [pulse_counter][hlw8012] Fix ESP-IDF build by re-enabling legacy driver component (#13747) --- esphome/components/hlw8012/sensor.py | 8 ++++++++ .../components/pulse_counter/pulse_counter_sensor.h | 4 ++++ esphome/components/pulse_counter/sensor.py | 10 +++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 201ea4451f..4727877633 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHANGE_MODE_EVERY, @@ -25,6 +26,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.core import CORE AUTO_LOAD = ["pulse_counter"] @@ -91,6 +93,12 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): + if CORE.is_esp32: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h + # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) + include_builtin_idf_component("driver") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 5ba59cca2a..f906e9e5cb 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -6,6 +6,10 @@ #include +// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h) +// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the +// "driver" IDF component dependency. See: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6 #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #include #define HAS_PCNT diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index dbf67fd2ad..65be5ee793 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_COUNT_MODE, @@ -126,7 +127,14 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = await sensor.new_sensor(config, config.get(CONF_USE_PCNT)) + use_pcnt = config.get(CONF_USE_PCNT) + if CORE.is_esp32 and use_pcnt: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # Provides driver/pcnt.h header for hardware pulse counter API + # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) + include_builtin_idf_component("driver") + + var = await sensor.new_sensor(config, use_pcnt) await cg.register_component(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) From b8b072cf8643dd8f0741d6657a92c99f7fcf2e92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:43:27 +0100 Subject: [PATCH 066/251] [web_server_idf] Add const char* overloads for getParam/hasParam to avoid temporary string allocations (#13746) --- esphome/components/web_server_idf/utils.cpp | 13 +++++-------- esphome/components/web_server_idf/utils.h | 5 ++++- .../components/web_server_idf/web_server_idf.cpp | 6 +++--- esphome/components/web_server_idf/web_server_idf.h | 11 ++++++++--- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index f27814062c..d3c3c3dc55 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -73,18 +73,15 @@ optional request_get_url_query(httpd_req_t *req) { return {str}; } -optional query_key_value(const std::string &query_url, const std::string &key) { - if (query_url.empty()) { +optional query_key_value(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { return {}; } - auto val = std::unique_ptr(new char[query_url.size()]); - if (!val) { - ESP_LOGE(TAG, "Not enough memory to the query key value"); - return {}; - } + // Use stack buffer for typical query strings, heap fallback for large ones + SmallBufferWithHeapFallback<256, char> val(query_len); - if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) { + if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 3a86aec7ac..ae58f82398 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -15,7 +15,10 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); optional request_get_url_query(httpd_req_t *req); -optional query_key_value(const std::string &query_url, const std::string &key); +optional query_key_value(const char *query_url, size_t query_len, const char *key); +inline optional query_key_value(const std::string &query_url, const std::string &key) { + return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); +} // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 2e5a74cbef..9860810452 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -366,7 +366,7 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { } #endif -AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { +AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { // Check cache first - only successful lookups are cached for (auto *param : this->params_) { if (param->name() == name) { @@ -375,11 +375,11 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { } // Look up value from query strings - optional val = query_key_value(this->post_query_, name); + optional val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); if (!val.has_value()) { auto url_query = request_get_url_query(*this); if (url_query.has_value()) { - val = query_key_value(url_query.value(), name); + val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); } } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index e38913ef4a..817f47da79 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -162,19 +162,24 @@ class AsyncWebServerRequest { } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; } + bool hasParam(const char *name) { return this->getParam(name) != nullptr; } // NOLINTNEXTLINE(readability-identifier-naming) - AsyncWebParameter *getParam(const std::string &name); + bool hasParam(const std::string &name) { return this->getParam(name.c_str()) != nullptr; } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const char *name); + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const std::string &name) { + std::string arg(const char *name) { auto *param = this->getParam(name); if (param) { return param->value(); } return {}; } + std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } optional get_header(const char *name) const; From 5d4bde98dcd251fc2762ca14356977bba0310fa1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 16:56:48 +0100 Subject: [PATCH 067/251] [mqtt] Refactor state publishing with dedicated enum-to-string helpers (#13544) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../mqtt/mqtt_alarm_control_panel.cpp | 65 ++--- esphome/components/mqtt/mqtt_climate.cpp | 269 ++++++++---------- esphome/components/mqtt/mqtt_component.cpp | 17 ++ esphome/components/mqtt/mqtt_component.h | 33 +++ esphome/components/mqtt/mqtt_cover.cpp | 25 +- esphome/components/mqtt/mqtt_fan.cpp | 16 +- esphome/components/mqtt/mqtt_valve.cpp | 25 +- 7 files changed, 253 insertions(+), 197 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 263e554778..dc8f75d8f5 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -13,6 +13,33 @@ static const char *const TAG = "mqtt.alarm_control_panel"; using namespace esphome::alarm_control_panel; +static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_DISARMED: + return ESPHOME_F("disarmed"); + case ACP_STATE_ARMED_HOME: + return ESPHOME_F("armed_home"); + case ACP_STATE_ARMED_AWAY: + return ESPHOME_F("armed_away"); + case ACP_STATE_ARMED_NIGHT: + return ESPHOME_F("armed_night"); + case ACP_STATE_ARMED_VACATION: + return ESPHOME_F("armed_vacation"); + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return ESPHOME_F("armed_custom_bypass"); + case ACP_STATE_PENDING: + return ESPHOME_F("pending"); + case ACP_STATE_ARMING: + return ESPHOME_F("arming"); + case ACP_STATE_DISARMING: + return ESPHOME_F("disarming"); + case ACP_STATE_TRIGGERED: + return ESPHOME_F("triggered"); + default: + return ESPHOME_F("unknown"); + } +} + MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} void MQTTAlarmControlPanelComponent::setup() { @@ -85,43 +112,9 @@ const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return th bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } bool MQTTAlarmControlPanelComponent::publish_state() { - const char *state_s; - switch (this->alarm_control_panel_->get_state()) { - case ACP_STATE_DISARMED: - state_s = "disarmed"; - break; - case ACP_STATE_ARMED_HOME: - state_s = "armed_home"; - break; - case ACP_STATE_ARMED_AWAY: - state_s = "armed_away"; - break; - case ACP_STATE_ARMED_NIGHT: - state_s = "armed_night"; - break; - case ACP_STATE_ARMED_VACATION: - state_s = "armed_vacation"; - break; - case ACP_STATE_ARMED_CUSTOM_BYPASS: - state_s = "armed_custom_bypass"; - break; - case ACP_STATE_PENDING: - state_s = "pending"; - break; - case ACP_STATE_ARMING: - state_s = "arming"; - break; - case ACP_STATE_DISARMING: - state_s = "disarming"; - break; - case ACP_STATE_TRIGGERED: - state_s = "triggered"; - break; - default: - state_s = "unknown"; - } char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - return this->publish(this->get_state_topic_to_(topic_buf), state_s); + return this->publish(this->get_state_topic_to_(topic_buf), + alarm_state_to_mqtt_str(this->alarm_control_panel_->get_state())); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 37d643f9e7..673593ef84 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -1,5 +1,6 @@ #include "mqtt_climate.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,111 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; +static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { + switch (mode) { + case CLIMATE_MODE_OFF: + return ESPHOME_F("off"); + case CLIMATE_MODE_HEAT_COOL: + return ESPHOME_F("heat_cool"); + case CLIMATE_MODE_AUTO: + return ESPHOME_F("auto"); + case CLIMATE_MODE_COOL: + return ESPHOME_F("cool"); + case CLIMATE_MODE_HEAT: + return ESPHOME_F("heat"); + case CLIMATE_MODE_FAN_ONLY: + return ESPHOME_F("fan_only"); + case CLIMATE_MODE_DRY: + return ESPHOME_F("dry"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { + switch (action) { + case CLIMATE_ACTION_OFF: + return ESPHOME_F("off"); + case CLIMATE_ACTION_COOLING: + return ESPHOME_F("cooling"); + case CLIMATE_ACTION_HEATING: + return ESPHOME_F("heating"); + case CLIMATE_ACTION_IDLE: + return ESPHOME_F("idle"); + case CLIMATE_ACTION_DRYING: + return ESPHOME_F("drying"); + case CLIMATE_ACTION_FAN: + return ESPHOME_F("fan"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) { + switch (fan_mode) { + case CLIMATE_FAN_ON: + return ESPHOME_F("on"); + case CLIMATE_FAN_OFF: + return ESPHOME_F("off"); + case CLIMATE_FAN_AUTO: + return ESPHOME_F("auto"); + case CLIMATE_FAN_LOW: + return ESPHOME_F("low"); + case CLIMATE_FAN_MEDIUM: + return ESPHOME_F("medium"); + case CLIMATE_FAN_HIGH: + return ESPHOME_F("high"); + case CLIMATE_FAN_MIDDLE: + return ESPHOME_F("middle"); + case CLIMATE_FAN_FOCUS: + return ESPHOME_F("focus"); + case CLIMATE_FAN_DIFFUSE: + return ESPHOME_F("diffuse"); + case CLIMATE_FAN_QUIET: + return ESPHOME_F("quiet"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) { + switch (swing_mode) { + case CLIMATE_SWING_OFF: + return ESPHOME_F("off"); + case CLIMATE_SWING_BOTH: + return ESPHOME_F("both"); + case CLIMATE_SWING_VERTICAL: + return ESPHOME_F("vertical"); + case CLIMATE_SWING_HORIZONTAL: + return ESPHOME_F("horizontal"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) { + switch (preset) { + case CLIMATE_PRESET_NONE: + return ESPHOME_F("none"); + case CLIMATE_PRESET_HOME: + return ESPHOME_F("home"); + case CLIMATE_PRESET_ECO: + return ESPHOME_F("eco"); + case CLIMATE_PRESET_AWAY: + return ESPHOME_F("away"); + case CLIMATE_PRESET_BOOST: + return ESPHOME_F("boost"); + case CLIMATE_PRESET_COMFORT: + return ESPHOME_F("comfort"); + case CLIMATE_PRESET_SLEEP: + return ESPHOME_F("sleep"); + case CLIMATE_PRESET_ACTIVITY: + return ESPHOME_F("activity"); + default: + return ESPHOME_F("unknown"); + } +} + void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson auto traits = this->device_->get_traits(); @@ -260,34 +366,8 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); // mode - const char *mode_s; - switch (this->device_->mode) { - case CLIMATE_MODE_OFF: - mode_s = "off"; - break; - case CLIMATE_MODE_AUTO: - mode_s = "auto"; - break; - case CLIMATE_MODE_COOL: - mode_s = "cool"; - break; - case CLIMATE_MODE_HEAT: - mode_s = "heat"; - break; - case CLIMATE_MODE_FAN_ONLY: - mode_s = "fan_only"; - break; - case CLIMATE_MODE_DRY: - mode_s = "dry"; - break; - case CLIMATE_MODE_HEAT_COOL: - mode_s = "heat_cool"; - break; - default: - mode_s = "unknown"; - } bool success = true; - if (!this->publish(this->get_mode_state_topic(), mode_s)) + if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode))) success = false; int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -327,134 +407,37 @@ bool MQTTClimateComponent::publish_state_() { } if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { - std::string payload; - if (this->device_->preset.has_value()) { - switch (this->device_->preset.value()) { - case CLIMATE_PRESET_NONE: - payload = "none"; - break; - case CLIMATE_PRESET_HOME: - payload = "home"; - break; - case CLIMATE_PRESET_AWAY: - payload = "away"; - break; - case CLIMATE_PRESET_BOOST: - payload = "boost"; - break; - case CLIMATE_PRESET_COMFORT: - payload = "comfort"; - break; - case CLIMATE_PRESET_ECO: - payload = "eco"; - break; - case CLIMATE_PRESET_SLEEP: - payload = "sleep"; - break; - case CLIMATE_PRESET_ACTIVITY: - payload = "activity"; - break; - default: - payload = "unknown"; - } - } - if (this->device_->has_custom_preset()) - payload = this->device_->get_custom_preset().c_str(); - if (!this->publish(this->get_preset_state_topic(), payload)) + if (this->device_->has_custom_preset()) { + if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset())) + success = false; + } else if (this->device_->preset.has_value()) { + if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value()))) + success = false; + } else if (!this->publish(this->get_preset_state_topic(), "")) { success = false; + } } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - const char *payload; - switch (this->device_->action) { - case CLIMATE_ACTION_OFF: - payload = "off"; - break; - case CLIMATE_ACTION_COOLING: - payload = "cooling"; - break; - case CLIMATE_ACTION_HEATING: - payload = "heating"; - break; - case CLIMATE_ACTION_IDLE: - payload = "idle"; - break; - case CLIMATE_ACTION_DRYING: - payload = "drying"; - break; - case CLIMATE_ACTION_FAN: - payload = "fan"; - break; - default: - payload = "unknown"; - } - if (!this->publish(this->get_action_state_topic(), payload)) + if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action))) success = false; } if (traits.get_supports_fan_modes()) { - std::string payload; - if (this->device_->fan_mode.has_value()) { - switch (this->device_->fan_mode.value()) { - case CLIMATE_FAN_ON: - payload = "on"; - break; - case CLIMATE_FAN_OFF: - payload = "off"; - break; - case CLIMATE_FAN_AUTO: - payload = "auto"; - break; - case CLIMATE_FAN_LOW: - payload = "low"; - break; - case CLIMATE_FAN_MEDIUM: - payload = "medium"; - break; - case CLIMATE_FAN_HIGH: - payload = "high"; - break; - case CLIMATE_FAN_MIDDLE: - payload = "middle"; - break; - case CLIMATE_FAN_FOCUS: - payload = "focus"; - break; - case CLIMATE_FAN_DIFFUSE: - payload = "diffuse"; - break; - case CLIMATE_FAN_QUIET: - payload = "quiet"; - break; - default: - payload = "unknown"; - } - } - if (this->device_->has_custom_fan_mode()) - payload = this->device_->get_custom_fan_mode().c_str(); - if (!this->publish(this->get_fan_mode_state_topic(), payload)) + if (this->device_->has_custom_fan_mode()) { + if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode())) + success = false; + } else if (this->device_->fan_mode.has_value()) { + if (!this->publish(this->get_fan_mode_state_topic(), + climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value()))) + success = false; + } else if (!this->publish(this->get_fan_mode_state_topic(), "")) { success = false; + } } if (traits.get_supports_swing_modes()) { - const char *payload; - switch (this->device_->swing_mode) { - case CLIMATE_SWING_OFF: - payload = "off"; - break; - case CLIMATE_SWING_BOTH: - payload = "both"; - break; - case CLIMATE_SWING_VERTICAL: - payload = "vertical"; - break; - case CLIMATE_SWING_HORIZONTAL: - payload = "horizontal"; - break; - default: - payload = "unknown"; - } - if (!this->publish(this->get_swing_mode_state_topic(), payload)) + if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) success = false; } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index aec6140e3f..a77afd3f4e 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -5,6 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/version.h" #include "mqtt_const.h" @@ -149,6 +150,22 @@ bool MQTTComponent::publish(const char *topic, const char *payload) { return this->publish(topic, payload, strlen(payload)); } +#ifdef USE_ESP8266 +bool MQTTComponent::publish(const std::string &topic, ProgmemStr payload) { + return this->publish(topic.c_str(), payload); +} + +bool MQTTComponent::publish(const char *topic, ProgmemStr payload) { + if (topic[0] == '\0') + return false; + // On ESP8266, ProgmemStr is __FlashStringHelper* - need to copy from flash + char buf[64]; + strncpy_P(buf, reinterpret_cast(payload), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return global_mqtt_client->publish(topic, buf, strlen(buf), this->qos_, this->retain_); +} +#endif + bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 304a2c0d0e..2cec6fda7e 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -9,6 +9,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include "mqtt_client.h" @@ -157,6 +158,15 @@ class MQTTComponent : public Component { */ bool publish(const std::string &topic, const char *payload, size_t payload_length); + /** Send a MQTT message. + * + * @param topic The topic. + * @param payload The null-terminated payload. + */ + bool publish(const std::string &topic, const char *payload) { + return this->publish(topic.c_str(), payload, strlen(payload)); + } + /** Send a MQTT message (no heap allocation for topic). * * @param topic The topic as C string. @@ -189,6 +199,29 @@ class MQTTComponent : public Component { */ bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); } +#ifdef USE_ESP8266 + /** Send a MQTT message with a PROGMEM string payload. + * + * @param topic The topic. + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(const std::string &topic, ProgmemStr payload); + + /** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic). + * + * @param topic The topic as C string. + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(const char *topic, ProgmemStr payload); + + /** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic). + * + * @param topic The topic as StringRef (for use with get_state_topic_to_()). + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(StringRef topic, ProgmemStr payload) { return this->publish(topic.c_str(), payload); } +#endif + /** Construct and send a JSON MQTT message. * * @param topic The topic. diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index d5bd13869a..50e68ecbcc 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -1,5 +1,6 @@ #include "mqtt_cover.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.cover"; using namespace esphome::cover; +static ProgmemStr cover_state_to_mqtt_str(CoverOperation operation, float position, bool supports_position) { + if (operation == COVER_OPERATION_OPENING) + return ESPHOME_F("opening"); + if (operation == COVER_OPERATION_CLOSING) + return ESPHOME_F("closing"); + if (position == COVER_CLOSED) + return ESPHOME_F("closed"); + if (position == COVER_OPEN) + return ESPHOME_F("open"); + if (supports_position) + return ESPHOME_F("open"); + return ESPHOME_F("unknown"); +} + MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {} void MQTTCoverComponent::setup() { auto traits = this->cover_->get_traits(); @@ -109,14 +124,10 @@ bool MQTTCoverComponent::publish_state() { if (!this->publish(this->get_tilt_state_topic(), pos, len)) success = false; } - const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening" - : this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing" - : this->cover_->position == COVER_CLOSED ? "closed" - : this->cover_->position == COVER_OPEN ? "open" - : traits.get_supports_position() ? "open" - : "unknown"; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) + if (!this->publish(this->get_state_topic_to_(topic_buf), + cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, + traits.get_supports_position()))) success = false; return success; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index c9791fb0f1..84d51895c5 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -1,5 +1,6 @@ #include "mqtt_fan.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,14 @@ static const char *const TAG = "mqtt.fan"; using namespace esphome::fan; +static ProgmemStr fan_direction_to_mqtt_str(FanDirection direction) { + return direction == FanDirection::FORWARD ? ESPHOME_F("forward") : ESPHOME_F("reverse"); +} + +static ProgmemStr fan_oscillation_to_mqtt_str(bool oscillating) { + return oscillating ? ESPHOME_F("oscillate_on") : ESPHOME_F("oscillate_off"); +} + MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {} Fan *MQTTFanComponent::get_state() const { return this->state_; } @@ -164,13 +173,12 @@ bool MQTTFanComponent::publish_state() { this->publish(this->get_state_topic_to_(topic_buf), state_s); bool failed = false; if (this->state_->get_traits().supports_direction()) { - bool success = this->publish(this->get_direction_state_topic(), - this->state_->direction == fan::FanDirection::FORWARD ? "forward" : "reverse"); + bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction)); failed = failed || !success; } if (this->state_->get_traits().supports_oscillation()) { - bool success = this->publish(this->get_oscillation_state_topic(), - this->state_->oscillating ? "oscillate_on" : "oscillate_off"); + bool success = + this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating)); failed = failed || !success; } auto traits = this->state_->get_traits(); diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 2e100823bf..16e25f6a8a 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -1,5 +1,6 @@ #include "mqtt_valve.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.valve"; using namespace esphome::valve; +static ProgmemStr valve_state_to_mqtt_str(ValveOperation operation, float position, bool supports_position) { + if (operation == VALVE_OPERATION_OPENING) + return ESPHOME_F("opening"); + if (operation == VALVE_OPERATION_CLOSING) + return ESPHOME_F("closing"); + if (position == VALVE_CLOSED) + return ESPHOME_F("closed"); + if (position == VALVE_OPEN) + return ESPHOME_F("open"); + if (supports_position) + return ESPHOME_F("open"); + return ESPHOME_F("unknown"); +} + MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {} void MQTTValveComponent::setup() { auto traits = this->valve_->get_traits(); @@ -78,14 +93,10 @@ bool MQTTValveComponent::publish_state() { if (!this->publish(this->get_position_state_topic(), pos, len)) success = false; } - const char *state_s = this->valve_->current_operation == VALVE_OPERATION_OPENING ? "opening" - : this->valve_->current_operation == VALVE_OPERATION_CLOSING ? "closing" - : this->valve_->position == VALVE_CLOSED ? "closed" - : this->valve_->position == VALVE_OPEN ? "open" - : traits.get_supports_position() ? "open" - : "unknown"; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) + if (!this->publish(this->get_state_topic_to_(topic_buf), + valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position, + traits.get_supports_position()))) success = false; return success; } From f11b8615dab566c1e6e84264b84aaf618ff8c787 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 17:03:02 +0100 Subject: [PATCH 068/251] [cse7766] Fix power reading stuck when load switches off (#13734) --- esphome/components/cse7766/cse7766.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 4432195365..c36e57c929 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -152,6 +152,10 @@ void CSE7766Component::parse_data_() { if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); } + } else if (this->power_sensor_ != nullptr) { + // No valid power measurement from chip - publish 0W to avoid stale readings + // This typically happens when current is below the measurable threshold (~50mA) + this->power_sensor_->publish_state(0.0f); } float current = 0.0f; From e6bae1a97e863abd4fe82e4901b52daa431b154c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:16:13 -0500 Subject: [PATCH 069/251] [adc] Add ESP32-C2 support for curve fitting calibration (#13749) Co-authored-by: Claude Opus 4.5 --- esphome/components/adc/adc_sensor_esp32.cpp | 42 ++++++++++++--------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ea1263db5f..ece45f3746 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -42,11 +42,11 @@ void ADCSensor::setup() { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize init_config.unit_id = this->adc_unit_; init_config.ulp_mode = ADC_ULP_MODE_DISABLE; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || - // USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#endif // USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || + // USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); if (err != ESP_OK) { ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); @@ -74,8 +74,9 @@ void ADCSensor::setup() { if (this->calibration_handle_ == nullptr) { adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 // RISC-V variants and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) @@ -112,7 +113,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 } this->setup_flags_.init_complete = true; @@ -184,12 +185,13 @@ float ADCSensor::sample_fixed_attenuation_() { } else { ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); if (this->calibration_handle_ != nullptr) { -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 this->calibration_handle_ = nullptr; } } @@ -217,8 +219,9 @@ float ADCSensor::sample_autorange_() { // Need to recalibrate for the new attenuation if (this->calibration_handle_ != nullptr) { // Delete old calibration handle -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else adc_cali_delete_scheme_line_fitting(this->calibration_handle_); @@ -229,8 +232,9 @@ float ADCSensor::sample_autorange_() { // Create new calibration handle for this attenuation adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_curve_fitting_config_t cali_config = {}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -264,8 +268,9 @@ float ADCSensor::sample_autorange_() { if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); if (handle != nullptr) { -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); @@ -286,8 +291,9 @@ float ADCSensor::sample_autorange_() { ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ + USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); From 95f39149d76023fee4317fdc416efa6994ef214f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:28:59 -0500 Subject: [PATCH 070/251] [rtttl] Fix dotted note parsing order to match RTTTL spec (#13722) Co-authored-by: Claude Opus 4.5 --- esphome/components/rtttl/rtttl.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 65fcc207d4..c179282c50 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -290,25 +290,26 @@ void Rtttl::loop() { this->position_++; } - // now, get optional '.' dotted note - if (this->rtttl_[this->position_] == '.') { - this->note_duration_ += this->note_duration_ / 2; - this->position_++; - } - // now, get scale uint8_t scale = get_integer_(); - if (scale == 0) + if (scale == 0) { scale = this->default_octave_; + } if (scale < 4 || scale > 7) { ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale); this->finish_(); return; } - bool need_note_gap = false; + + // now, get optional '.' dotted note + if (this->rtttl_[this->position_] == '.') { + this->note_duration_ += this->note_duration_ / 2; + this->position_++; + } // Now play the note + bool need_note_gap = false; if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { From 2541ec15656e79d44501605be72c18edac66e78f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Feb 2026 09:42:13 +0100 Subject: [PATCH 071/251] [wifi] Fix wifi.connected condition returning false in connect state listener automations (#13733) --- esphome/components/wifi/wifi_component.cpp | 21 +++++++++++++++++++ esphome/components/wifi/wifi_component.h | 15 +++++++++++++ .../wifi/wifi_component_esp8266.cpp | 10 ++++++--- .../wifi/wifi_component_esp_idf.cpp | 9 +++++--- .../wifi/wifi_component_libretiny.cpp | 11 ++++++---- .../components/wifi/wifi_component_pico_w.cpp | 12 ++++++----- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c4bfdf3c42..e9b78c9225 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1464,6 +1464,12 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Notify listeners now that state machine has reached STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->notify_connect_state_listeners_(); +#endif + return; } @@ -2183,6 +2189,21 @@ void WiFiComponent::release_scan_results_() { } } +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +void WiFiComponent::notify_connect_state_listeners_() { + if (!this->pending_.connect_state) + return; + this->pending_.connect_state = false; + // Get current SSID and BSSID from the WiFi driver + char ssid_buf[SSID_BUFFER_SIZE]; + const char *ssid = this->wifi_ssid_to(ssid_buf); + bssid_t bssid = this->wifi_bssid(); + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); + } +} +#endif // USE_WIFI_CONNECT_STATE_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4bdc253f66..98f339809a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -632,6 +632,11 @@ class WiFiComponent : public Component { /// Free scan results memory unless a component needs them void release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) + void notify_connect_state_listeners_(); +#endif + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -739,6 +744,16 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Pending listener notifications deferred until state machine reaches appropriate state. + // Listeners are notified after state transitions complete so conditions like + // wifi.connected return correct values in automations. + // Uses bitfields to minimize memory; more flags may be added as needed. + struct { + bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED + } pending_{}; +#endif + #ifdef USE_WIFI_CONNECT_TRIGGER Trigger<> connect_trigger_; #endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c714afaad3..c6bd40037d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -500,6 +500,10 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { } } +// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). +// All listener notifications should be deferred to wifi_loop_() via pending_ flags +// to avoid stack overflow. Currently only connect_state is deferred; disconnect, +// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { @@ -512,9 +516,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + global_wifi_component->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a32232a758..22bf4be483 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -710,6 +710,9 @@ void WiFiComponent::wifi_loop_() { delete data; // NOLINT(cppcoreguidelines-owning-memory) } } +// Events are processed from queue in main loop context, but listener notifications +// must be deferred until after the state machine transitions (in check_connecting_finished) +// so that conditions like wifi.connected return correct values in automations. void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { @@ -743,9 +746,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index af2b82c3c6..285a520ef5 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -423,7 +423,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } } -// Process a single event from the queue - runs in main loop context +// Process a single event from the queue - runs in main loop context. +// Listener notifications must be deferred until after the state machine transitions +// (in check_connecting_finished) so that conditions like wifi.connected return +// correct values in automations. void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { switch (event->event_id) { case ESPHOME_EVENT_ID_WIFI_READY: { @@ -456,9 +459,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // This matches ESP32 IDF behavior where s_sta_connected is set but // wifi_sta_connect_status_() also checks got_ipv4_address_ #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 84c10d5d43..1ce36c2d93 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -252,6 +252,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } +// Pico W uses polling for connection state detection. +// Connect state listener notifications are deferred until after the state machine +// transitions (in check_connecting_finished) so that conditions like wifi.connected +// return correct values in automations. void WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { @@ -278,11 +282,9 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - String ssid = WiFi.SSID(); - bssid_t bssid = this->wifi_bssid(); - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, notify IP listeners immediately as the IP is already configured #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) From 4d05cd30592ab23208953ad8a1c6c277dab34cc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:24:05 +0000 Subject: [PATCH 072/251] Bump ruff from 0.14.14 to 0.15.0 (#13752) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- esphome/components/i2c/__init__.py | 2 +- esphome/components/lvgl/automation.py | 8 ++---- esphome/components/opentherm/generate.py | 4 ++- esphome/components/zephyr/__init__.py | 7 +++-- esphome/config_validation.py | 2 +- esphome/enum.py | 18 ++---------- esphome/wizard.py | 2 +- requirements_test.txt | 2 +- script/api_protobuf/api_protobuf.py | 2 +- script/merge_component_configs.py | 2 +- tests/integration/test_script_queued.py | 8 ++++-- tests/script/test_helpers.py | 4 +-- tests/unit_tests/core/test_config.py | 26 ++++++++++------- tests/unit_tests/test_writer.py | 36 ++++++++++++------------ 15 files changed, 60 insertions(+), 65 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b068673ecf..991e053d5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.14 + rev: v0.15.0 hooks: # Run the linter. - id: ruff diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 19efda0b49..de3f2be674 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -183,7 +183,7 @@ async def to_code(config): if CORE.using_zephyr: zephyr_add_prj_conf("I2C", True) i2c = "i2c0" - if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + if zephyr_data()[KEY_BOARD] == "xiao_ble": i2c = "i2c1" zephyr_add_overlay( f""" diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 9b58727f2a..b589e42f3b 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -272,9 +272,7 @@ async def obj_hide_to_code(config, action_id, template_arg, args): async def do_hide(widget: Widget): widget.add_flag("LV_OBJ_FLAG_HIDDEN") - widgets = [ - widget.outer if widget.outer else widget for widget in await get_widgets(config) - ] + widgets = [widget.outer or widget for widget in await get_widgets(config)] return await action_to_code(widgets, do_hide, action_id, template_arg, args) @@ -285,9 +283,7 @@ async def obj_show_to_code(config, action_id, template_arg, args): if widget.move_to_foreground: lv_obj.move_foreground(widget.obj) - widgets = [ - widget.outer if widget.outer else widget for widget in await get_widgets(config) - ] + widgets = [widget.outer or widget for widget in await get_widgets(config)] return await action_to_code(widgets, do_show, action_id, template_arg, args) diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index 4e6f3b0a12..0b39895798 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -31,7 +31,9 @@ def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> N cg.RawExpression( " sep ".join( map( - lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})", + lambda key: ( + f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})" + ), keys, ) ) diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 8e3ae86bbe..43d5cebebb 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -213,9 +213,10 @@ def copy_files(): zephyr_data()[KEY_OVERLAY], ) - if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[ - KEY_BOARD - ] in ["xiao_ble"]: + if ( + zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT + or zephyr_data()[KEY_BOARD] == "xiao_ble" + ): fake_board_manifest = """ { "frameworks": [ diff --git a/esphome/config_validation.py b/esphome/config_validation.py index b7ab02013d..55e13a7050 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -682,7 +682,7 @@ def only_with_framework( def validator_(obj): if CORE.target_framework not in frameworks: err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" - if suggestion := suggestions.get(CORE.target_framework, None): + if suggestion := suggestions.get(CORE.target_framework): (component, docs_path) = suggestion err_str += f"\nPlease use '{component}'" if docs_path: diff --git a/esphome/enum.py b/esphome/enum.py index 0fe30cf92a..cf0d8b645b 100644 --- a/esphome/enum.py +++ b/esphome/enum.py @@ -2,19 +2,7 @@ from __future__ import annotations -from enum import Enum -from typing import Any +from enum import StrEnum as _StrEnum - -class StrEnum(str, Enum): - """Partial backport of Python 3.11's StrEnum for our basic use cases.""" - - def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: - """Create a new StrEnum instance.""" - if not isinstance(value, str): - raise TypeError(f"{value!r} is not a string") - return super().__new__(cls, value, *args, **kwargs) - - def __str__(self) -> str: - """Return self.value.""" - return str(self.value) +# Re-export StrEnum from standard library for backwards compatibility +StrEnum = _StrEnum diff --git a/esphome/wizard.py b/esphome/wizard.py index f5e8a1e462..4b74847996 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -470,7 +470,7 @@ def wizard(path: Path) -> int: sleep(1) # Do not create wifi if the board does not support it - if board not in ["rpipico"]: + if board != "rpipico": safe_print_step(3, WIFI_BIG) safe_print("In this step, I'm going to create the configuration for WiFi.") safe_print() diff --git a/requirements_test.txt b/requirements_test.txt index 5d90764021..2cf6f6456e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.14 # also change in .pre-commit-config.yaml when updating +ruff==0.15.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 72103285e8..8baf6acf11 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -280,7 +280,7 @@ class TypeInfo(ABC): """ field_id_size = self.calculate_field_id_size() method = f"{base_method}_force" if force else base_method - value = value_expr if value_expr else name + value = value_expr or name return f"size.{method}({field_id_size}, {value});" @abstractmethod diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 59774edba9..5e98f1fef5 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -249,7 +249,7 @@ def merge_component_configs( if all_packages is None: # First component - initialize package dict - all_packages = comp_packages if comp_packages else {} + all_packages = comp_packages or {} elif comp_packages: # Merge packages - combine all unique package types # If both have the same package type, verify they're identical diff --git a/tests/integration/test_script_queued.py b/tests/integration/test_script_queued.py index c86c289719..84c7f950b6 100644 --- a/tests/integration/test_script_queued.py +++ b/tests/integration/test_script_queued.py @@ -98,9 +98,11 @@ async def test_script_queued( if not test3_complete.done(): loop.call_later( 0.3, - lambda: test3_complete.set_result(True) - if not test3_complete.done() - else None, + lambda: ( + test3_complete.set_result(True) + if not test3_complete.done() + else None + ), ) # Test 4: Rejection diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index c51273f298..7e60ba41fc 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1011,8 +1011,8 @@ def test_get_all_dependencies_handles_missing_components() -> None: comp.dependencies = ["missing_comp"] comp.auto_load = [] - mock_get_component.side_effect = ( - lambda name: comp if name == "existing" else None + mock_get_component.side_effect = lambda name: ( + comp if name == "existing" else None ) result = helpers.get_all_dependencies({"existing", "nonexistent"}) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index ab7bdbb98c..88801a9ca0 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -453,11 +453,14 @@ def test_preload_core_config_no_platform(setup_core: Path) -> None: # Mock _is_target_platform to avoid expensive component loading with patch("esphome.core.config._is_target_platform") as mock_is_platform: # Return True for known platforms - mock_is_platform.side_effect = lambda name: name in [ - "esp32", - "esp8266", - "rp2040", - ] + mock_is_platform.side_effect = lambda name: ( + name + in [ + "esp32", + "esp8266", + "rp2040", + ] + ) with pytest.raises(cv.Invalid, match="Platform missing"): preload_core_config(config, result) @@ -477,11 +480,14 @@ def test_preload_core_config_multiple_platforms(setup_core: Path) -> None: # Mock _is_target_platform to avoid expensive component loading with patch("esphome.core.config._is_target_platform") as mock_is_platform: # Return True for known platforms - mock_is_platform.side_effect = lambda name: name in [ - "esp32", - "esp8266", - "rp2040", - ] + mock_is_platform.side_effect = lambda name: ( + name + in [ + "esp32", + "esp8266", + "rp2040", + ] + ) with pytest.raises(cv.Invalid, match="Found multiple target platform blocks"): preload_core_config(config, result) diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index ac05e0d31b..134b63df4a 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -466,8 +466,8 @@ def test_clean_build( ) as mock_get_instance: mock_config = MagicMock() mock_get_instance.return_value = mock_config - mock_config.get.side_effect = ( - lambda section, option: str(platformio_cache_dir) + mock_config.get.side_effect = lambda section, option: ( + str(platformio_cache_dir) if (section, option) == ("platformio", "cache_dir") else "" ) @@ -630,8 +630,8 @@ def test_clean_build_empty_cache_dir( ) as mock_get_instance: mock_config = MagicMock() mock_get_instance.return_value = mock_config - mock_config.get.side_effect = ( - lambda section, option: " " # Whitespace only + mock_config.get.side_effect = lambda section, option: ( + " " # Whitespace only if (section, option) == ("platformio", "cache_dir") else "" ) @@ -1574,8 +1574,8 @@ def test_copy_src_tree_writes_build_info_files( mock_component.resources = mock_resources # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "Test comment" @@ -1649,8 +1649,8 @@ def test_copy_src_tree_detects_config_hash_change( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF # Different from existing mock_core.comment = "" @@ -1711,8 +1711,8 @@ def test_copy_src_tree_detects_version_change( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1761,8 +1761,8 @@ def test_copy_src_tree_handles_invalid_build_info_json( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1835,8 +1835,8 @@ def test_copy_src_tree_build_info_timestamp_behavior( mock_component.resources = mock_resources # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1930,8 +1930,8 @@ def test_copy_src_tree_detects_removed_source_file( existing_file.write_text("// test file") # Setup mocks - no components, so the file should be removed - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1992,8 +1992,8 @@ def test_copy_src_tree_ignores_removed_generated_file( build_info_h.write_text("// old generated file") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" From 5dc8bfe95efe0a6efd21cbed855b724d05b469ef Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 4 Feb 2026 01:29:27 -0800 Subject: [PATCH 073/251] [ultrasonic] adjust timeouts and bring the parameter back (#13738) Co-authored-by: Samuel Sieb --- CODEOWNERS | 2 +- esphome/components/ultrasonic/__init__.py | 2 +- esphome/components/ultrasonic/sensor.py | 11 +--- .../ultrasonic/ultrasonic_sensor.cpp | 56 ++++++++++++++----- .../components/ultrasonic/ultrasonic_sensor.h | 5 ++ 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1d165a6f57..25e6dc1b29 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -532,7 +532,7 @@ esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli -esphome/components/ultrasonic/* @OttoWinter +esphome/components/ultrasonic/* @ssieb @swoboda1337 esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon esphome/components/usb_cdc_acm/* @kbx81 diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index 71a87b6ae5..3bca12bffc 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@swoboda1337", "@ssieb"] diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 4b04ee7578..fad4e6b11d 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -34,7 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_TIMEOUT): cv.distance, + cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, @@ -52,12 +52,5 @@ async def to_code(config): cg.add(var.set_trigger_pin(trigger)) echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN]) cg.add(var.set_echo_pin(echo)) - - # Remove before 2026.8.0 - if CONF_TIMEOUT in config: - _LOGGER.warning( - "'timeout' option is deprecated and will be removed in 2026.8.0. " - "The option has no effect and can be safely removed." - ) - + cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2))) cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME])) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 369a10edbd..d3f7e69444 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,12 +6,11 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; -static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering) -static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion +static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { + if (arg->echo_pin_isr.digital_read()) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -38,6 +37,7 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); + this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -53,29 +53,55 @@ void UltrasonicSensorComponent::loop() { return; } + if (!this->store_.echo_start) { + uint32_t elapsed = micros() - this->measurement_start_us_; + if (elapsed >= START_TIMEOUT_US) { + ESP_LOGW(TAG, "'%s' - Measurement start timed out", this->name_.c_str()); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } else { + uint32_t elapsed; + if (this->store_.echo_end) { + elapsed = this->store_.echo_end_us - this->store_.echo_start_us; + } else { + elapsed = micros() - this->store_.echo_start_us; + } + if (elapsed >= this->timeout_us_) { + ESP_LOGD(TAG, "'%s' - Measurement pulse timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } + if (this->store_.echo_end) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); - float result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + float result; + if (this->store_.echo_start) { + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", + this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); + result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + } else { + ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); + result = NAN; + } this->publish_state(result); this->measurement_pending_ = false; return; } - - uint32_t elapsed = micros() - this->measurement_start_us_; - if (elapsed >= MEASUREMENT_TIMEOUT_US) { - ESP_LOGD(TAG, "'%s' - Measurement timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); - this->publish_state(NAN); - this->measurement_pending_ = false; - } } void UltrasonicSensorComponent::dump_config() { LOG_SENSOR("", "Ultrasonic Sensor", this); LOG_PIN(" Echo Pin: ", this->echo_pin_); LOG_PIN(" Trigger Pin: ", this->trigger_pin_); - ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " us", this->pulse_time_us_); + ESP_LOGCONFIG(TAG, + " Pulse time: %" PRIu32 " µs\n" + " Timeout: %" PRIu32 " µs", + this->pulse_time_us_, this->timeout_us_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index b0c00e51f0..a38737aff5 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,6 +11,8 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); + ISRInternalGPIOPin echo_pin_isr; + volatile uint32_t wait_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; @@ -29,6 +31,8 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent float get_setup_priority() const override { return setup_priority::DATA; } + /// Set the maximum time in µs to wait for the echo to return + void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } @@ -41,6 +45,7 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent ISRInternalGPIOPin trigger_pin_isr_; InternalGPIOPin *echo_pin_; UltrasonicSensorStore store_; + uint32_t timeout_us_{}; uint32_t pulse_time_us_{}; uint32_t measurement_start_us_{0}; From 5544f0d346a4d689dfe78b9d7ed5e83b1f45ad7e Mon Sep 17 00:00:00 2001 From: J0k3r2k1 <60352302+J0k3r2k1@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:33:45 +0100 Subject: [PATCH 074/251] [mipi_spi] Fix log_pin() FlashStringHelper compatibility (#13624) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/mipi_spi/mipi_spi.cpp | 39 +++++++++++++++-- esphome/components/mipi_spi/mipi_spi.h | 42 ++++--------------- .../components/mipi_spi/test.esp8266-ard.yaml | 10 +++++ 3 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 tests/components/mipi_spi/test.esp8266-ard.yaml diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 272915b4e1..90f6324511 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -1,6 +1,39 @@ #include "mipi_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace mipi_spi {} // namespace mipi_spi -} // namespace esphome +namespace esphome::mipi_spi { + +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) { + ESP_LOGCONFIG(TAG, + "MIPI_SPI Display\n" + " Model: %s\n" + " Width: %d\n" + " Height: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s\n" + " Invert colors: %s\n" + " Color order: %s\n" + " Display pixels: %d bits\n" + " Endianness: %s\n" + " SPI Mode: %d\n" + " SPI Data rate: %uMHz\n" + " SPI Bus width: %d", + model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)), + YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB", + display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast(data_rate / 1000000), + bus_width); + LOG_PIN(" CS Pin: ", cs); + LOG_PIN(" Reset Pin: ", reset); + LOG_PIN(" DC Pin: ", dc); + if (offset_width != 0) + ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width); + if (offset_height != 0) + ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height); + if (brightness.has_value()) + ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value()); +} + +} // namespace esphome::mipi_spi diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index a59cb8104b..083ff9507f 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -63,6 +63,11 @@ enum BusType { BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer }; +// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width); + /** * Base class for MIPI SPI displays. * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. @@ -201,40 +206,9 @@ class MipiSpi : public display::Display, } void dump_config() override { - esph_log_config(TAG, - "MIPI_SPI Display\n" - " Model: %s\n" - " Width: %u\n" - " Height: %u", - this->model_, WIDTH, HEIGHT); - if constexpr (OFFSET_WIDTH != 0) - esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH); - if constexpr (OFFSET_HEIGHT != 0) - esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT); - esph_log_config(TAG, - " Swap X/Y: %s\n" - " Mirror X: %s\n" - " Mirror Y: %s\n" - " Invert colors: %s\n" - " Color order: %s\n" - " Display pixels: %d bits\n" - " Endianness: %s\n", - YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), - YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_), - this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); - if (this->brightness_.has_value()) - esph_log_config(TAG, " Brightness: %u", this->brightness_.value()); - if (this->cs_ != nullptr) - esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str()); - if (this->reset_pin_ != nullptr) - esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str()); - if (this->dc_pin_ != nullptr) - esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str()); - esph_log_config(TAG, - " SPI Mode: %d\n" - " SPI Data rate: %dMHz\n" - " SPI Bus width: %d", - this->mode_, static_cast(this->data_rate_ / 1000000), BUS_TYPE); + internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_, + DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, + this->mode_, this->data_rate_, BUS_TYPE); } protected: diff --git a/tests/components/mipi_spi/test.esp8266-ard.yaml b/tests/components/mipi_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ef6197d852 --- /dev/null +++ b/tests/components/mipi_spi/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + dc_pin: GPIO15 + cs_pin: GPIO5 + enable_pin: GPIO4 + reset_pin: GPIO16 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml From 8314ad9ca01b8bdf5d91f0da768836d236f87595 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:59:13 -0500 Subject: [PATCH 075/251] [max7219] Allocate buffer in constructor (#13660) Co-authored-by: Claude Opus 4.5 --- esphome/components/max7219/display.py | 3 +-- esphome/components/max7219/max7219.cpp | 17 ++++++++--------- esphome/components/max7219/max7219.h | 11 +++++------ 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index a434125148..abb20702bd 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -28,11 +28,10 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS]) await spi.register_spi_device(var, config, write_only=True) await display.register_display(var, config) - cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) cg.add(var.set_intensity(config[CONF_INTENSITY])) cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 157b317c02..d701e6fc86 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { static const char *const TAG = "max7219"; @@ -115,12 +114,14 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { }; float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) { + this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT + memset(this->buffer_, 0, this->num_chips_ * 8); +} + void MAX7219Component::setup() { this->spi_setup(); - this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT - for (uint8_t i = 0; i < this->num_chips_ * 8; i++) - this->buffer_[i] = 0; - // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); // let's use our own ASCII -> led pattern encoding @@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t intensity) { this->intensity_ = intensity; } } -void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; @@ -240,5 +240,4 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time } uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 58d871d54c..ef38628f28 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -6,8 +6,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { class MAX7219Component; @@ -17,6 +16,8 @@ class MAX7219Component : public PollingComponent, public spi::SPIDevice { public: + explicit MAX7219Component(uint8_t num_chips); + void set_writer(max7219_writer_t &&writer); void setup() override; @@ -30,7 +31,6 @@ class MAX7219Component : public PollingComponent, void display(); void set_intensity(uint8_t intensity); - void set_num_chips(uint8_t num_chips); void set_reverse(bool reverse) { this->reverse_ = reverse; }; /// Evaluate the printf-format and print the result at the given position. @@ -56,10 +56,9 @@ class MAX7219Component : public PollingComponent, uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most) bool intensity_changed_{}; // True if we need to re-send the intensity uint8_t num_chips_{1}; - uint8_t *buffer_; + uint8_t *buffer_{nullptr}; bool reverse_{false}; max7219_writer_t writer_{}; }; -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 From 49ef4e00df263618ece4ee5ffa52ca02d432c3e9 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Mon, 2 Feb 2026 10:48:08 -0500 Subject: [PATCH 076/251] [mqtt] resolve warnings related to use of ip.str() (#13719) --- esphome/components/mqtt/mqtt_backend_esp32.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index bd2d2a67b2..adba0cf004 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -139,7 +139,8 @@ class MQTTBackendESP32 final : public MQTTBackend { this->lwt_retain_ = retain; } void set_server(network::IPAddress ip, uint16_t port) final { - this->host_ = ip.str(); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + this->host_ = ip.str_to(ip_buf); this->port_ = port; } void set_server(const char *host, uint16_t port) final { From b085585461120df1b375bf14ce283c929fcd1c4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 22:22:11 +0100 Subject: [PATCH 077/251] [core] Add missing uint32_t ID overloads for defer() and cancel_defer() (#13720) --- esphome/core/component.cpp | 4 ++ esphome/core/component.h | 4 ++ .../fixtures/scheduler_numeric_id_test.yaml | 45 ++++++++++++++++++- .../test_scheduler_numeric_id_test.py | 38 +++++++++++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2f61f7d195..1c398d9ac0 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -356,6 +356,10 @@ void Component::defer(const std::string &name, std::function &&f) { // void Component::defer(const char *name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } +void Component::defer(uint32_t id, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, id, 0, std::move(f)); +} +bool Component::cancel_defer(uint32_t id) { return App.scheduler.cancel_timeout(this, id); } void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast(nullptr), timeout, std::move(f)); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 49349d4199..97f2afe1a4 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -494,11 +494,15 @@ class Component { /// Defer a callback to the next loop() call. void defer(std::function &&f); // NOLINT + /// Defer a callback with a numeric ID (zero heap allocation) + void defer(uint32_t id, std::function &&f); // NOLINT + /// Cancel a defer callback using the specified name, name must not be empty. // Remove before 2026.7.0 ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_defer(const std::string &name); // NOLINT bool cancel_defer(const char *name); // NOLINT + bool cancel_defer(uint32_t id); // NOLINT // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index bf60f2fda9..1669f026f5 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -20,6 +20,9 @@ globals: - id: retry_counter type: int initial_value: '0' + - id: defer_counter + type: int + initial_value: '0' - id: tests_done type: bool initial_value: 'false' @@ -136,11 +139,49 @@ script: App.scheduler.cancel_retry(component1, 6002U); ESP_LOGI("test", "Cancelled numeric retry 6002"); + // Test 12: defer with numeric ID (Component method) + class TestDeferComponent : public Component { + public: + void test_defer_methods() { + // Test defer with uint32_t ID - should execute on next loop + this->defer(7001U, []() { + ESP_LOGI("test", "Component numeric defer 7001 fired"); + id(defer_counter) += 1; + }); + + // Test another defer with numeric ID + this->defer(7002U, []() { + ESP_LOGI("test", "Component numeric defer 7002 fired"); + id(defer_counter) += 1; + }); + } + }; + + static TestDeferComponent test_defer_component; + test_defer_component.test_defer_methods(); + + // Test 13: cancel_defer with numeric ID (Component method) + class TestCancelDeferComponent : public Component { + public: + void test_cancel_defer() { + // Set a defer that should be cancelled + this->defer(8001U, []() { + ESP_LOGE("test", "ERROR: Numeric defer 8001 should have been cancelled"); + }); + // Cancel it immediately + bool cancelled = this->cancel_defer(8001U); + ESP_LOGI("test", "Cancelled numeric defer 8001: %s", cancelled ? "true" : "false"); + } + }; + + static TestCancelDeferComponent test_cancel_defer_component; + test_cancel_defer_component.test_cancel_defer(); + - id: report_results then: - lambda: |- - ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d", - id(timeout_counter), id(interval_counter), id(retry_counter)); + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d", + id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter)); sensor: - platform: template diff --git a/tests/integration/test_scheduler_numeric_id_test.py b/tests/integration/test_scheduler_numeric_id_test.py index 510256b9a4..c1958db685 100644 --- a/tests/integration/test_scheduler_numeric_id_test.py +++ b/tests/integration/test_scheduler_numeric_id_test.py @@ -19,6 +19,7 @@ async def test_scheduler_numeric_id_test( timeout_count = 0 interval_count = 0 retry_count = 0 + defer_count = 0 # Events for each test completion numeric_timeout_1001_fired = asyncio.Event() @@ -33,6 +34,9 @@ async def test_scheduler_numeric_id_test( max_id_timeout_fired = asyncio.Event() numeric_retry_done = asyncio.Event() numeric_retry_cancelled = asyncio.Event() + numeric_defer_7001_fired = asyncio.Event() + numeric_defer_7002_fired = asyncio.Event() + numeric_defer_cancelled = asyncio.Event() final_results_logged = asyncio.Event() # Track interval counts @@ -40,7 +44,7 @@ async def test_scheduler_numeric_id_test( numeric_retry_count = 0 def on_log_line(line: str) -> None: - nonlocal timeout_count, interval_count, retry_count + nonlocal timeout_count, interval_count, retry_count, defer_count nonlocal numeric_interval_count, numeric_retry_count # Strip ANSI color codes @@ -105,15 +109,27 @@ async def test_scheduler_numeric_id_test( elif "Cancelled numeric retry 6002" in clean_line: numeric_retry_cancelled.set() + # Check for numeric defer tests + elif "Component numeric defer 7001 fired" in clean_line: + numeric_defer_7001_fired.set() + + elif "Component numeric defer 7002 fired" in clean_line: + numeric_defer_7002_fired.set() + + elif "Cancelled numeric defer 8001: true" in clean_line: + numeric_defer_cancelled.set() + # Check for final results elif "Final results" in clean_line: match = re.search( - r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+)", clean_line + r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)", + clean_line, ) if match: timeout_count = int(match.group(1)) interval_count = int(match.group(2)) retry_count = int(match.group(3)) + defer_count = int(match.group(4)) final_results_logged.set() async with ( @@ -201,6 +217,23 @@ async def test_scheduler_numeric_id_test( "Numeric retry 6002 should have been cancelled" ) + # Wait for numeric defer tests + try: + await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7001 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(numeric_defer_7002_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7002 did not fire within 0.5 seconds") + + # Verify numeric defer was cancelled + try: + await asyncio.wait_for(numeric_defer_cancelled.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 8001 cancel confirmation not received") + # Wait for final results try: await asyncio.wait_for(final_results_logged.wait(), timeout=3.0) @@ -215,3 +248,4 @@ async def test_scheduler_numeric_id_test( assert retry_count >= 2, ( f"Expected at least 2 retry attempts, got {retry_count}" ) + assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}" From 094d64f872f9bf3856ba6fec8eb8dabf2f5114c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 03:19:38 +0100 Subject: [PATCH 078/251] [http_request] Fix requests taking full timeout when response is already complete (#13649) --- .../update/esp32_hosted_update.cpp | 14 ++++- .../components/http_request/http_request.h | 55 ++++++++++++++----- .../http_request/http_request_arduino.cpp | 20 ++++++- .../http_request/http_request_idf.cpp | 9 +-- .../http_request/ota/ota_http_request.cpp | 6 +- 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 0e0778dd23..7ffa61fc97 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -193,11 +193,14 @@ bool Esp32HostedUpdate::fetch_manifest_() { int read_or_error = container->read(buf, sizeof(buf)); App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. if (result != http_request::HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT json_str.append(reinterpret_cast(buf), read_or_error); } container->end(); @@ -318,9 +321,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. + if (result == http_request::HttpReadLoopResult::COMPLETE) + break; if (result != http_request::HttpReadLoopResult::DATA) { if (result == http_request::HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading firmware data"); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index fb39ca504c..f3c99c6de4 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -26,6 +26,7 @@ struct Header { enum HttpStatus { HTTP_STATUS_OK = 200, HTTP_STATUS_NO_CONTENT = 204, + HTTP_STATUS_RESET_CONTENT = 205, HTTP_STATUS_PARTIAL_CONTENT = 206, /* 3xx - Redirection */ @@ -126,19 +127,21 @@ struct HttpReadResult { /// Result of processing a non-blocking read with timeout (for manual loops) enum class HttpReadLoopResult : uint8_t { - DATA, ///< Data was read, process it - RETRY, ///< No data yet, already delayed, caller should continue loop - ERROR, ///< Read error, caller should exit loop - TIMEOUT, ///< Timeout waiting for data, caller should exit loop + DATA, ///< Data was read, process it + COMPLETE, ///< All content has been read, caller should exit loop + RETRY, ///< No data yet, already delayed, caller should continue loop + ERROR, ///< Read error, caller should exit loop + TIMEOUT, ///< Timeout waiting for data, caller should exit loop }; /// Process a read result with timeout tracking and delay handling /// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error /// @param last_data_time Time of last successful read, updated when data received /// @param timeout_ms Maximum time to wait for data -/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit -inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, - uint32_t timeout_ms) { +/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete()) +/// @return How the caller should proceed - see HttpReadLoopResult enum +inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, + bool is_read_complete) { if (bytes_read_or_error > 0) { last_data_time = millis(); return HttpReadLoopResult::DATA; @@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_ if (bytes_read_or_error < 0) { return HttpReadLoopResult::ERROR; } - // bytes_read_or_error == 0: no data available yet + // bytes_read_or_error == 0: either "no data yet" or "all content read" + if (is_read_complete) { + return HttpReadLoopResult::COMPLETE; + } if (millis() - last_data_time >= timeout_ms) { return HttpReadLoopResult::TIMEOUT; } @@ -159,9 +165,9 @@ class HttpRequestComponent; class HttpContainer : public Parented { public: virtual ~HttpContainer() = default; - size_t content_length; - int status_code; - uint32_t duration_ms; + size_t content_length{0}; + int status_code{-1}; ///< -1 indicates no response received yet + uint32_t duration_ms{0}; /** * @brief Read data from the HTTP response body. @@ -194,9 +200,24 @@ class HttpContainer : public Parented { virtual void end() = 0; void set_secure(bool secure) { this->secure_ = secure; } + void set_chunked(bool chunked) { this->is_chunked_ = chunked; } size_t get_bytes_read() const { return this->bytes_read_; } + /// Check if all expected content has been read + /// For chunked responses, returns false (completion detected via read() returning error/EOF) + bool is_read_complete() const { + // Per RFC 9112, these responses have no body: + // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified + if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || + this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) { + return true; + } + // For non-chunked responses, complete when bytes_read >= content_length + // This handles both Content-Length: 0 and Content-Length: N cases + return !this->is_chunked_ && this->bytes_read_ >= this->content_length; + } + /** * @brief Get response headers. * @@ -209,6 +230,7 @@ class HttpContainer : public Parented { protected: size_t bytes_read_{0}; bool secure_{false}; + bool is_chunked_{false}; ///< True if response uses chunked transfer encoding std::map> response_headers_{}; }; @@ -219,7 +241,7 @@ class HttpContainer : public Parented { /// @param total_size Total bytes to read /// @param chunk_size Maximum bytes per read call /// @param timeout_ms Read timeout in milliseconds -/// @return HttpReadResult with status and error_code on failure +/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms) { size_t read_index = 0; @@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms); + auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + if (result == HttpReadLoopResult::COMPLETE) + break; // Server sent less data than requested, but transfer is complete if (result == HttpReadLoopResult::ERROR) return {HttpReadStatus::ERROR, read_bytes_or_error}; if (result == HttpReadLoopResult::TIMEOUT) @@ -393,11 +417,12 @@ template class HttpRequestSendAction : public Action { int read_or_error = container->read(buf + read_index, std::min(max_length - read_index, 512)); App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; if (result != HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT read_index += read_or_error; } response_body.reserve(read_index); diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 82538b2cb3..2f12b58766 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -135,9 +135,23 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the // early return check (bytes_read_ >= content_length) will never trigger. + // + // TODO: Chunked transfer encoding is NOT properly supported on Arduino. + // The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where + // esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr() + // returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead + // of decoded content. This wasn't noticed because requests would complete and payloads + // were only examined on IDF. The long transfer times were also masked by the misleading + // "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues: + // 1. Response body is corrupted - contains chunk size headers mixed with data + // 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout + // The proper fix would be to use getString() for chunked responses, which decodes chunks + // internally, but this buffers the entire response in memory. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; + // -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding + container->set_chunked(content_length == -1); container->duration_ms = millis() - start; return container; @@ -178,9 +192,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) - // For chunked encoding (content_length == SIZE_MAX), we can't use this check - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've read all expected content (non-chunked only) + // For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false + if (this->is_read_complete()) { return 0; // All content read successfully } // No data available - check if connection is still open diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 95c59aa04c..9cfa825e17 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -155,6 +155,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -190,6 +191,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -234,10 +236,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content - // Skip this check when content_length is 0 (chunked transfer encoding or unknown length) - // For chunked responses, esp_http_client_read() will return 0 when all data is received - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've already read all expected content (non-chunked only) + // For chunked responses (content_length == 0), esp_http_client_read() handles EOF + if (this->is_read_complete()) { return 0; // All content read successfully } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 6c77e75d8c..d073644a37 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout); + auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added for OTA in the future. + if (result == HttpReadLoopResult::COMPLETE) + break; if (result != HttpReadLoopResult::DATA) { if (result == HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading data"); From bc41d25657236e32b04465ae05040b8928f0448c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 17:03:02 +0100 Subject: [PATCH 079/251] [cse7766] Fix power reading stuck when load switches off (#13734) --- esphome/components/cse7766/cse7766.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 71fe15f0ae..e7bcb64f8c 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -152,6 +152,10 @@ void CSE7766Component::parse_data_() { if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); } + } else if (this->power_sensor_ != nullptr) { + // No valid power measurement from chip - publish 0W to avoid stale readings + // This typically happens when current is below the measurable threshold (~50mA) + this->power_sensor_->publish_state(0.0f); } float current = 0.0f; From 900aab45f12b7b6d590d97c7ad086743c36de078 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Feb 2026 09:42:13 +0100 Subject: [PATCH 080/251] [wifi] Fix wifi.connected condition returning false in connect state listener automations (#13733) --- esphome/components/wifi/wifi_component.cpp | 21 +++++++++++++++++++ esphome/components/wifi/wifi_component.h | 15 +++++++++++++ .../wifi/wifi_component_esp8266.cpp | 10 ++++++--- .../wifi/wifi_component_esp_idf.cpp | 9 +++++--- .../wifi/wifi_component_libretiny.cpp | 11 ++++++---- .../components/wifi/wifi_component_pico_w.cpp | 12 ++++++----- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 52d9b2b442..fd7e2c6ee6 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1383,6 +1383,12 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Notify listeners now that state machine has reached STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->notify_connect_state_listeners_(); +#endif + return; } @@ -2090,6 +2096,21 @@ void WiFiComponent::release_scan_results_() { } } +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +void WiFiComponent::notify_connect_state_listeners_() { + if (!this->pending_.connect_state) + return; + this->pending_.connect_state = false; + // Get current SSID and BSSID from the WiFi driver + char ssid_buf[SSID_BUFFER_SIZE]; + const char *ssid = this->wifi_ssid_to(ssid_buf); + bssid_t bssid = this->wifi_bssid(); + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); + } +} +#endif // USE_WIFI_CONNECT_STATE_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index dfc91fb5da..28db486b88 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -618,6 +618,11 @@ class WiFiComponent : public Component { /// Free scan results memory unless a component needs them void release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) + void notify_connect_state_listeners_(); +#endif + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -721,6 +726,16 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Pending listener notifications deferred until state machine reaches appropriate state. + // Listeners are notified after state transitions complete so conditions like + // wifi.connected return correct values in automations. + // Uses bitfields to minimize memory; more flags may be added as needed. + struct { + bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED + } pending_{}; +#endif + // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 59101439c2..41033cca19 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -500,6 +500,10 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { } } +// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). +// All listener notifications should be deferred to wifi_loop_() via pending_ flags +// to avoid stack overflow. Currently only connect_state is deferred; disconnect, +// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { @@ -512,9 +516,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + global_wifi_component->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 15fd407e3c..711d88bd68 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -710,6 +710,9 @@ void WiFiComponent::wifi_loop_() { delete data; // NOLINT(cppcoreguidelines-owning-memory) } } +// Events are processed from queue in main loop context, but listener notifications +// must be deferred until after the state machine transitions (in check_connecting_finished) +// so that conditions like wifi.connected return correct values in automations. void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { @@ -743,9 +746,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 20cd32fa8f..cddca2aa91 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -423,7 +423,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } } -// Process a single event from the queue - runs in main loop context +// Process a single event from the queue - runs in main loop context. +// Listener notifications must be deferred until after the state machine transitions +// (in check_connecting_finished) so that conditions like wifi.connected return +// correct values in automations. void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { switch (event->event_id) { case ESPHOME_EVENT_ID_WIFI_READY: { @@ -456,9 +459,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // This matches ESP32 IDF behavior where s_sta_connected is set but // wifi_sta_connect_status_() also checks got_ipv4_address_ #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 29ac096d94..c55aeef5a4 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -240,6 +240,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } +// Pico W uses polling for connection state detection. +// Connect state listener notifications are deferred until after the state machine +// transitions (in check_connecting_finished) so that conditions like wifi.connected +// return correct values in automations. void WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { @@ -264,11 +268,9 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - String ssid = WiFi.SSID(); - bssid_t bssid = this->wifi_bssid(); - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, notify IP listeners immediately as the IP is already configured #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) From bafbd4235a5ac9d631b79d64ce1a1a796fbc3d18 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 4 Feb 2026 01:29:27 -0800 Subject: [PATCH 081/251] [ultrasonic] adjust timeouts and bring the parameter back (#13738) Co-authored-by: Samuel Sieb --- CODEOWNERS | 2 +- esphome/components/ultrasonic/__init__.py | 2 +- esphome/components/ultrasonic/sensor.py | 11 +--- .../ultrasonic/ultrasonic_sensor.cpp | 56 ++++++++++++++----- .../components/ultrasonic/ultrasonic_sensor.h | 5 ++ 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8a37aeb29f..136152e6ff 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -528,7 +528,7 @@ esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli -esphome/components/ultrasonic/* @OttoWinter +esphome/components/ultrasonic/* @ssieb @swoboda1337 esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon esphome/components/usb_cdc_acm/* @kbx81 diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index 71a87b6ae5..3bca12bffc 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@swoboda1337", "@ssieb"] diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 4b04ee7578..fad4e6b11d 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -34,7 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_TIMEOUT): cv.distance, + cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, @@ -52,12 +52,5 @@ async def to_code(config): cg.add(var.set_trigger_pin(trigger)) echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN]) cg.add(var.set_echo_pin(echo)) - - # Remove before 2026.8.0 - if CONF_TIMEOUT in config: - _LOGGER.warning( - "'timeout' option is deprecated and will be removed in 2026.8.0. " - "The option has no effect and can be safely removed." - ) - + cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2))) cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME])) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 369a10edbd..d3f7e69444 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,12 +6,11 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; -static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering) -static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion +static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { + if (arg->echo_pin_isr.digital_read()) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -38,6 +37,7 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); + this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -53,29 +53,55 @@ void UltrasonicSensorComponent::loop() { return; } + if (!this->store_.echo_start) { + uint32_t elapsed = micros() - this->measurement_start_us_; + if (elapsed >= START_TIMEOUT_US) { + ESP_LOGW(TAG, "'%s' - Measurement start timed out", this->name_.c_str()); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } else { + uint32_t elapsed; + if (this->store_.echo_end) { + elapsed = this->store_.echo_end_us - this->store_.echo_start_us; + } else { + elapsed = micros() - this->store_.echo_start_us; + } + if (elapsed >= this->timeout_us_) { + ESP_LOGD(TAG, "'%s' - Measurement pulse timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } + if (this->store_.echo_end) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); - float result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + float result; + if (this->store_.echo_start) { + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", + this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); + result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + } else { + ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); + result = NAN; + } this->publish_state(result); this->measurement_pending_ = false; return; } - - uint32_t elapsed = micros() - this->measurement_start_us_; - if (elapsed >= MEASUREMENT_TIMEOUT_US) { - ESP_LOGD(TAG, "'%s' - Measurement timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); - this->publish_state(NAN); - this->measurement_pending_ = false; - } } void UltrasonicSensorComponent::dump_config() { LOG_SENSOR("", "Ultrasonic Sensor", this); LOG_PIN(" Echo Pin: ", this->echo_pin_); LOG_PIN(" Trigger Pin: ", this->trigger_pin_); - ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " us", this->pulse_time_us_); + ESP_LOGCONFIG(TAG, + " Pulse time: %" PRIu32 " µs\n" + " Timeout: %" PRIu32 " µs", + this->pulse_time_us_, this->timeout_us_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index b0c00e51f0..a38737aff5 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,6 +11,8 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); + ISRInternalGPIOPin echo_pin_isr; + volatile uint32_t wait_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; @@ -29,6 +31,8 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent float get_setup_priority() const override { return setup_priority::DATA; } + /// Set the maximum time in µs to wait for the echo to return + void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } @@ -41,6 +45,7 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent ISRInternalGPIOPin trigger_pin_isr_; InternalGPIOPin *echo_pin_; UltrasonicSensorStore store_; + uint32_t timeout_us_{}; uint32_t pulse_time_us_{}; uint32_t measurement_start_us_{0}; From 1b3c9aa98efba2235f5a406ca0752faba7c160bf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:01:32 +0100 Subject: [PATCH 082/251] Bump version to 2026.1.4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f9ffa9e25a..140ac06565 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.1.3 +PROJECT_NUMBER = 2026.1.4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 1ce6eb4ba3..862c7e37e6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.1.3" +__version__ = "2026.1.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ba18a8b3e33cabbc40772a49348423f3d73ee987 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Feb 2026 06:44:17 -0500 Subject: [PATCH 083/251] [adc] Fix ESP32-C2 ADC calibration to use line fitting (#13756) Co-authored-by: Claude Opus 4.5 --- esphome/components/adc/adc_sensor_esp32.cpp | 44 ++++++++----------- tests/components/adc/test.esp32-c2-idf.yaml | 12 +++++ .../build_components_base.esp32-c2-idf.yaml | 20 +++++++++ 3 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 tests/components/adc/test.esp32-c2-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-c2-idf.yaml diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ece45f3746..1d3138623e 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -74,10 +74,9 @@ void ADCSensor::setup() { if (this->calibration_handle_ == nullptr) { adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 - // RISC-V variants and S3 use curve fitting calibration +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 + // RISC-V variants (except C2) and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -95,14 +94,14 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#else // Other ESP32 variants use line fitting calibration +#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, .atten = this->attenuation_, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, // Default reference voltage in mV -#endif // !defined(USE_ESP32_VARIANT_ESP32S2) +#endif // !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) }; err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); if (err == ESP_OK) { @@ -113,7 +112,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 } this->setup_flags_.init_complete = true; @@ -185,13 +184,12 @@ float ADCSensor::sample_fixed_attenuation_() { } else { ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); if (this->calibration_handle_ != nullptr) { -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // USE_ESP32_VARIANT_ESP32C2 || ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 this->calibration_handle_ = nullptr; } } @@ -219,9 +217,8 @@ float ADCSensor::sample_autorange_() { // Need to recalibrate for the new attenuation if (this->calibration_handle_ != nullptr) { // Delete old calibration handle -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else adc_cali_delete_scheme_line_fitting(this->calibration_handle_); @@ -232,9 +229,8 @@ float ADCSensor::sample_autorange_() { // Create new calibration handle for this attenuation adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_curve_fitting_config_t cali_config = {}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -251,7 +247,7 @@ float ADCSensor::sample_autorange_() { .unit_id = this->adc_unit_, .atten = atten, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, #endif }; @@ -268,9 +264,8 @@ float ADCSensor::sample_autorange_() { if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); if (handle != nullptr) { -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); @@ -291,9 +286,8 @@ float ADCSensor::sample_autorange_() { ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle -#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ - USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || \ - USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); diff --git a/tests/components/adc/test.esp32-c2-idf.yaml b/tests/components/adc/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..e764f0fe21 --- /dev/null +++ b/tests/components/adc/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +sensor: + - id: my_sensor + platform: adc + pin: GPIO1 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/test_build_components/build_components_base.esp32-c2-idf.yaml b/tests/test_build_components/build_components_base.esp32-c2-idf.yaml new file mode 100644 index 0000000000..59691be7aa --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c2-idf.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32c2idf + friendly_name: $component_name + +esp32: + board: esp32-c2-devkitm-1 + framework: + type: esp-idf + # Use custom partition table with larger app partition (3MB) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file From 36f2654fa67cdfc76741b6329e76f789671ed775 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Wed, 4 Feb 2026 17:06:59 +0100 Subject: [PATCH 084/251] [pylontech] Refactor parser to support new firmware version and SysError (#12300) --- esphome/components/pylontech/pylontech.cpp | 138 ++++++++++++++++++--- esphome/components/pylontech/pylontech.h | 7 +- 2 files changed, 122 insertions(+), 23 deletions(-) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 74b7caefb2..61b5356c90 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -2,6 +2,28 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +// Helper macros +#define PARSE_INT(field, field_name) \ + { \ + get_token(token_buf); \ + auto val = parse_number(token_buf); \ + if (val.has_value()) { \ + (field) = val.value(); \ + } else { \ + ESP_LOGD(TAG, "invalid " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \ + return; \ + } \ + } + +#define PARSE_STR(field, field_name) \ + { \ + get_token(field); \ + if (strlen(field) < 2) { \ + ESP_LOGD(TAG, "too short " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \ + return; \ + } \ + } + namespace esphome { namespace pylontech { @@ -64,33 +86,106 @@ void PylontechComponent::loop() { void PylontechComponent::process_line_(std::string &buffer) { ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str()); // clang-format off - // example line to parse: - // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St - // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // example lines to parse: + // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St + // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // 1 46012 1255 9100 5300 5500 3047 3091 SysError Low Normal Normal 4% 2025-11-28 17:56:33 Low Normal 7800 Normal + // newer firmware example: + // Power Volt Curr Tempr Tlow Tlow.Id Thigh Thigh.Id Vlow Vlow.Id Vhigh Vhigh.Id Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St SysAlarm.St + // 1 49405 0 17600 13700 8 14500 0 3293 2 3294 0 Idle Normal Normal Normal 60% 2025-12-05 00:53:41 Normal Normal 16600 Normal Normal // clang-format on PylontechListener::LineContents l{}; - char mostempr_s[6]; - const int parsed = sscanf( // NOLINT - buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT - &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT - l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT - if (l.bat_num <= 0) { - ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); - return; + const char *cursor = buffer.c_str(); + char token_buf[TEXT_SENSOR_MAX_LEN] = {0}; + + // Helper Lambda to extract tokens + auto get_token = [&](char *token_buf) -> void { + // Skip leading whitespace + while (*cursor == ' ' || *cursor == '\t') { + cursor++; + } + + if (*cursor == '\0') { + token_buf[0] = 0; + return; + } + + const char *start = cursor; + + // Find end of field + while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && *cursor != '\r') { + cursor++; + } + + size_t token_len = std::min(static_cast(cursor - start), static_cast(TEXT_SENSOR_MAX_LEN - 1)); + memcpy(token_buf, start, token_len); + token_buf[token_len] = 0; + }; + + { + get_token(token_buf); + auto val = parse_number(token_buf); + if (val.has_value() && val.value() > 0) { + l.bat_num = val.value(); + } else if (strcmp(token_buf, "Power") == 0) { + // header line i.e. "Power Volt Curr" and so on + this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos; + ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without", + buffer.substr(0, buffer.size() - 2).c_str()); + return; + } else { + ESP_LOGD(TAG, "unknown line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } } - if (parsed != 14) { - ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); - return; + PARSE_INT(l.volt, "Volt"); + PARSE_INT(l.curr, "Curr"); + PARSE_INT(l.tempr, "Tempr"); + PARSE_INT(l.tlow, "Tlow"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Tlow.Id } - auto mostempr_parsed = parse_number(mostempr_s); - if (mostempr_parsed.has_value()) { - l.mostempr = mostempr_parsed.value(); - } else { - l.mostempr = -300; - ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num); + PARSE_INT(l.thigh, "Thigh"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Thigh.Id } + PARSE_INT(l.vlow, "Vlow"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Vlow.Id + } + PARSE_INT(l.vhigh, "Vhigh"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Vhigh.Id + } + PARSE_STR(l.base_st, "Base.St"); + PARSE_STR(l.volt_st, "Volt.St"); + PARSE_STR(l.curr_st, "Curr.St"); + PARSE_STR(l.temp_st, "Temp.St"); + { + get_token(token_buf); + for (char &i : token_buf) { + if (i == '%') { + i = 0; + break; + } + } + auto coul_val = parse_number(token_buf); + if (coul_val.has_value()) { + l.coulomb = coul_val.value(); + } else { + ESP_LOGD(TAG, "invalid Coulomb in line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + } + get_token(token_buf); // Skip Date + get_token(token_buf); // Skip Time + get_token(token_buf); // Skip B.V.St + get_token(token_buf); // Skip B.T.St + PARSE_INT(l.mostempr, "Mostempr"); + + ESP_LOGD(TAG, "successful line %s", buffer.substr(0, buffer.size() - 2).c_str()); for (PylontechListener *listener : this->listeners_) { listener->on_line_read(&l); @@ -101,3 +196,6 @@ float PylontechComponent::get_setup_priority() const { return setup_priority::DA } // namespace pylontech } // namespace esphome + +#undef PARSE_INT +#undef PARSE_STR diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h index 3282cb4d9f..10c669ad9a 100644 --- a/esphome/components/pylontech/pylontech.h +++ b/esphome/components/pylontech/pylontech.h @@ -8,14 +8,14 @@ namespace esphome { namespace pylontech { static const uint8_t NUM_BUFFERS = 20; -static const uint8_t TEXT_SENSOR_MAX_LEN = 8; +static const uint8_t TEXT_SENSOR_MAX_LEN = 14; class PylontechListener { public: struct LineContents { int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr; - char base_st[TEXT_SENSOR_MAX_LEN], volt_st[TEXT_SENSOR_MAX_LEN], curr_st[TEXT_SENSOR_MAX_LEN], - temp_st[TEXT_SENSOR_MAX_LEN]; + char base_st[TEXT_SENSOR_MAX_LEN] = {0}, volt_st[TEXT_SENSOR_MAX_LEN] = {0}, curr_st[TEXT_SENSOR_MAX_LEN] = {0}, + temp_st[TEXT_SENSOR_MAX_LEN] = {0}; }; virtual void on_line_read(LineContents *line); @@ -45,6 +45,7 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice { std::string buffer_[NUM_BUFFERS]; int buffer_index_write_ = 0; int buffer_index_read_ = 0; + bool has_tlow_id_ = false; std::vector listeners_{}; }; From becb6559f143fc5c361065785fc8260bfe51bd26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Feb 2026 17:48:41 +0100 Subject: [PATCH 085/251] [components] Remove redundant setup priority overrides that duplicate default (#13745) --- esphome/components/absolute_humidity/absolute_humidity.cpp | 2 -- esphome/components/absolute_humidity/absolute_humidity.h | 1 - esphome/components/adc/adc_sensor.h | 5 ----- esphome/components/adc/adc_sensor_common.cpp | 2 -- esphome/components/adc128s102/sensor/adc128s102_sensor.cpp | 2 -- esphome/components/adc128s102/sensor/adc128s102_sensor.h | 1 - esphome/components/aht10/aht10.cpp | 2 -- esphome/components/aht10/aht10.h | 1 - esphome/components/am2315c/am2315c.cpp | 2 -- esphome/components/am2315c/am2315c.h | 1 - esphome/components/am2320/am2320.cpp | 1 - esphome/components/am2320/am2320.h | 1 - esphome/components/apds9960/apds9960.cpp | 1 - esphome/components/apds9960/apds9960.h | 1 - esphome/components/aqi/aqi_sensor.h | 1 - esphome/components/as3935/as3935.cpp | 2 -- esphome/components/as3935/as3935.h | 1 - esphome/components/as5600/sensor/as5600_sensor.cpp | 2 -- esphome/components/as5600/sensor/as5600_sensor.h | 1 - esphome/components/as7341/as7341.cpp | 2 -- esphome/components/as7341/as7341.h | 1 - esphome/components/atm90e26/atm90e26.cpp | 1 - esphome/components/atm90e26/atm90e26.h | 1 - esphome/components/bh1750/bh1750.cpp | 2 -- esphome/components/bh1750/bh1750.h | 1 - esphome/components/bme280_base/bme280_base.cpp | 1 - esphome/components/bme280_base/bme280_base.h | 1 - esphome/components/bme680/bme680.cpp | 2 -- esphome/components/bme680/bme680.h | 1 - esphome/components/bme680_bsec/bme680_bsec.cpp | 2 -- esphome/components/bme680_bsec/bme680_bsec.h | 1 - esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 2 -- esphome/components/bme68x_bsec2/bme68x_bsec2.h | 1 - esphome/components/bmi160/bmi160.cpp | 1 - esphome/components/bmi160/bmi160.h | 2 -- esphome/components/bmp085/bmp085.cpp | 1 - esphome/components/bmp085/bmp085.h | 2 -- esphome/components/bmp280_base/bmp280_base.cpp | 1 - esphome/components/bmp280_base/bmp280_base.h | 1 - esphome/components/bmp3xx_base/bmp3xx_base.cpp | 1 - esphome/components/bmp3xx_base/bmp3xx_base.h | 1 - esphome/components/cd74hc4067/cd74hc4067.cpp | 2 -- esphome/components/cd74hc4067/cd74hc4067.h | 1 - esphome/components/cm1106/cm1106.h | 2 -- esphome/components/combination/combination.h | 2 -- esphome/components/cse7761/cse7761.cpp | 2 -- esphome/components/cse7761/cse7761.h | 1 - esphome/components/cse7766/cse7766.cpp | 1 - esphome/components/cse7766/cse7766.h | 1 - esphome/components/current_based/current_based_cover.cpp | 1 - esphome/components/current_based/current_based_cover.h | 1 - esphome/components/daly_bms/daly_bms.cpp | 2 -- esphome/components/daly_bms/daly_bms.h | 1 - esphome/components/dht/dht.cpp | 2 -- esphome/components/dht/dht.h | 2 -- esphome/components/dht12/dht12.cpp | 2 +- esphome/components/dht12/dht12.h | 1 - esphome/components/dps310/dps310.cpp | 2 -- esphome/components/dps310/dps310.h | 1 - esphome/components/ds1307/ds1307.cpp | 2 -- esphome/components/ds1307/ds1307.h | 1 - esphome/components/duty_cycle/duty_cycle_sensor.cpp | 2 -- esphome/components/duty_cycle/duty_cycle_sensor.h | 1 - esphome/components/ee895/ee895.cpp | 2 -- esphome/components/ee895/ee895.h | 1 - esphome/components/emc2101/sensor/emc2101_sensor.cpp | 2 -- esphome/components/emc2101/sensor/emc2101_sensor.h | 2 -- esphome/components/endstop/endstop_cover.cpp | 2 +- esphome/components/endstop/endstop_cover.h | 1 - esphome/components/ens210/ens210.cpp | 2 -- esphome/components/ens210/ens210.h | 1 - esphome/components/esp32_camera/esp32_camera.cpp | 2 -- esphome/components/esp32_camera/esp32_camera.h | 1 - esphome/components/esp32_touch/esp32_touch.h | 1 - esphome/components/gdk101/gdk101.cpp | 2 -- esphome/components/gdk101/gdk101.h | 1 - esphome/components/gp8403/gp8403.h | 1 - esphome/components/hc8/hc8.cpp | 2 -- esphome/components/hc8/hc8.h | 2 -- esphome/components/hdc1080/hdc1080.h | 2 -- esphome/components/hlw8012/hlw8012.cpp | 1 - esphome/components/hlw8012/hlw8012.h | 1 - esphome/components/hm3301/hm3301.cpp | 2 -- esphome/components/hm3301/hm3301.h | 1 - esphome/components/hmc5883l/hmc5883l.cpp | 1 - esphome/components/hmc5883l/hmc5883l.h | 1 - .../components/homeassistant/time/homeassistant_time.cpp | 2 -- esphome/components/homeassistant/time/homeassistant_time.h | 1 - esphome/components/honeywell_hih_i2c/honeywell_hih.cpp | 2 -- esphome/components/honeywell_hih_i2c/honeywell_hih.h | 1 - esphome/components/hte501/hte501.cpp | 1 - esphome/components/hte501/hte501.h | 1 - esphome/components/htu21d/htu21d.cpp | 2 -- esphome/components/htu21d/htu21d.h | 2 -- esphome/components/htu31d/htu31d.cpp | 6 ------ esphome/components/htu31d/htu31d.h | 2 -- esphome/components/hx711/hx711.cpp | 1 - esphome/components/hx711/hx711.h | 1 - esphome/components/hydreon_rgxx/hydreon_rgxx.cpp | 2 -- esphome/components/hydreon_rgxx/hydreon_rgxx.h | 2 -- esphome/components/hyt271/hyt271.cpp | 2 -- esphome/components/hyt271/hyt271.h | 2 -- esphome/components/ina219/ina219.cpp | 2 -- esphome/components/ina219/ina219.h | 1 - esphome/components/ina226/ina226.cpp | 2 -- esphome/components/ina226/ina226.h | 1 - esphome/components/ina2xx_base/ina2xx_base.cpp | 2 -- esphome/components/ina2xx_base/ina2xx_base.h | 1 - esphome/components/ina3221/ina3221.cpp | 1 - esphome/components/ina3221/ina3221.h | 1 - esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 2 -- esphome/components/kamstrup_kmp/kamstrup_kmp.h | 1 - esphome/components/kmeteriso/kmeteriso.cpp | 2 -- esphome/components/kmeteriso/kmeteriso.h | 1 - esphome/components/m5stack_8angle/m5stack_8angle.cpp | 2 -- esphome/components/m5stack_8angle/m5stack_8angle.h | 1 - esphome/components/max17043/max17043.cpp | 2 -- esphome/components/max17043/max17043.h | 1 - esphome/components/max31855/max31855.cpp | 1 - esphome/components/max31855/max31855.h | 1 - esphome/components/max31856/max31856.cpp | 2 -- esphome/components/max31856/max31856.h | 1 - esphome/components/max31865/max31865.cpp | 2 -- esphome/components/max31865/max31865.h | 1 - esphome/components/max44009/max44009.cpp | 2 -- esphome/components/max44009/max44009.h | 1 - esphome/components/max6675/max6675.cpp | 1 - esphome/components/max6675/max6675.h | 1 - esphome/components/mcp3008/sensor/mcp3008_sensor.cpp | 2 -- esphome/components/mcp3008/sensor/mcp3008_sensor.h | 1 - esphome/components/mcp3204/sensor/mcp3204_sensor.cpp | 2 -- esphome/components/mcp3204/sensor/mcp3204_sensor.h | 1 - esphome/components/mcp9808/mcp9808.cpp | 1 - esphome/components/mcp9808/mcp9808.h | 1 - esphome/components/mhz19/mhz19.cpp | 2 -- esphome/components/mhz19/mhz19.h | 2 -- esphome/components/mics_4514/mics_4514.cpp | 1 - esphome/components/mics_4514/mics_4514.h | 1 - esphome/components/mlx90393/sensor_mlx90393.cpp | 2 -- esphome/components/mlx90393/sensor_mlx90393.h | 1 - esphome/components/mlx90614/mlx90614.cpp | 2 -- esphome/components/mlx90614/mlx90614.h | 1 - esphome/components/mmc5603/mmc5603.cpp | 2 -- esphome/components/mmc5603/mmc5603.h | 1 - esphome/components/mmc5983/mmc5983.cpp | 2 -- esphome/components/mmc5983/mmc5983.h | 1 - esphome/components/mpu6050/mpu6050.cpp | 1 - esphome/components/mpu6050/mpu6050.h | 2 -- esphome/components/mpu6886/mpu6886.cpp | 2 -- esphome/components/mpu6886/mpu6886.h | 2 -- esphome/components/ms5611/ms5611.cpp | 1 - esphome/components/ms5611/ms5611.h | 1 - esphome/components/msa3xx/msa3xx.cpp | 1 - esphome/components/msa3xx/msa3xx.h | 2 -- esphome/components/nau7802/nau7802.cpp | 2 -- esphome/components/nau7802/nau7802.h | 1 - esphome/components/nextion/nextion.cpp | 1 - esphome/components/nextion/nextion.h | 1 - esphome/components/npi19/npi19.cpp | 2 -- esphome/components/npi19/npi19.h | 1 - esphome/components/ntc/ntc.cpp | 1 - esphome/components/ntc/ntc.h | 1 - esphome/components/opt3001/opt3001.cpp | 2 -- esphome/components/opt3001/opt3001.h | 1 - esphome/components/pcf85063/pcf85063.cpp | 2 -- esphome/components/pcf85063/pcf85063.h | 1 - esphome/components/pcf8563/pcf8563.cpp | 2 -- esphome/components/pcf8563/pcf8563.h | 1 - esphome/components/pm1006/pm1006.cpp | 2 -- esphome/components/pm1006/pm1006.h | 2 -- esphome/components/pm2005/pm2005.h | 2 -- esphome/components/pmwcs3/pmwcs3.cpp | 2 -- esphome/components/pmwcs3/pmwcs3.h | 1 - esphome/components/pn532/pn532.cpp | 2 -- esphome/components/pn532/pn532.h | 1 - esphome/components/pulse_meter/pulse_meter_sensor.cpp | 2 -- esphome/components/pulse_meter/pulse_meter_sensor.h | 1 - esphome/components/pylontech/pylontech.cpp | 2 -- esphome/components/pylontech/pylontech.h | 2 -- esphome/components/qmc5883l/qmc5883l.cpp | 2 -- esphome/components/qmc5883l/qmc5883l.h | 1 - esphome/components/rd03d/rd03d.h | 1 - esphome/components/resampler/speaker/resampler_speaker.h | 1 - esphome/components/rotary_encoder/rotary_encoder.cpp | 1 - esphome/components/rotary_encoder/rotary_encoder.h | 2 -- esphome/components/rx8130/rx8130.h | 2 -- esphome/components/sdp3x/sdp3x.cpp | 2 -- esphome/components/sdp3x/sdp3x.h | 1 - esphome/components/sds011/sds011.cpp | 2 -- esphome/components/sds011/sds011.h | 2 -- esphome/components/sht3xd/sht3xd.cpp | 2 -- esphome/components/sht3xd/sht3xd.h | 1 - esphome/components/shtcx/shtcx.cpp | 2 -- esphome/components/shtcx/shtcx.h | 1 - esphome/components/smt100/smt100.cpp | 2 -- esphome/components/smt100/smt100.h | 2 -- esphome/components/sonoff_d1/sonoff_d1.h | 1 - esphome/components/spi_device/spi_device.cpp | 2 -- esphome/components/spi_device/spi_device.h | 2 -- esphome/components/sts3x/sts3x.cpp | 2 +- esphome/components/sts3x/sts3x.h | 1 - esphome/components/sy6970/sy6970.h | 1 - esphome/components/t6615/t6615.cpp | 1 - esphome/components/t6615/t6615.h | 2 -- esphome/components/tc74/tc74.cpp | 2 -- esphome/components/tc74/tc74.h | 2 -- esphome/components/tcs34725/tcs34725.cpp | 1 - esphome/components/tcs34725/tcs34725.h | 1 - esphome/components/tee501/tee501.cpp | 1 - esphome/components/tee501/tee501.h | 1 - esphome/components/tem3200/tem3200.cpp | 2 -- esphome/components/tem3200/tem3200.h | 1 - esphome/components/time_based/time_based_cover.cpp | 2 +- esphome/components/time_based/time_based_cover.h | 1 - esphome/components/tmp102/tmp102.cpp | 2 -- esphome/components/tmp102/tmp102.h | 2 -- esphome/components/tmp117/tmp117.cpp | 2 +- esphome/components/tmp117/tmp117.h | 1 - esphome/components/tsl2561/tsl2561.cpp | 2 +- esphome/components/tsl2561/tsl2561.h | 1 - esphome/components/tsl2591/tsl2591.cpp | 2 -- esphome/components/tsl2591/tsl2591.h | 2 -- esphome/components/tx20/tx20.cpp | 2 -- esphome/components/tx20/tx20.h | 1 - esphome/components/ultrasonic/ultrasonic_sensor.h | 2 -- esphome/components/version/version_text_sensor.cpp | 1 - esphome/components/version/version_text_sensor.h | 1 - esphome/components/wts01/wts01.h | 1 - 228 files changed, 6 insertions(+), 337 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index b13fcd519a..9c66531d05 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -45,8 +45,6 @@ void AbsoluteHumidityComponent::dump_config() { this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); } -float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; } - void AbsoluteHumidityComponent::loop() { if (!this->next_update_) { return; diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h index 9f3b9eab8b..71feee2c42 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.h +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -24,7 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 526dd57fd5..91cf4eaafc 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -68,11 +68,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage /// This method is called during the ESPHome setup process to log the configuration. void dump_config() override; - /// Return the setup priority for this component. - /// Components with higher priority are initialized earlier during setup. - /// @return A float representing the setup priority. - float get_setup_priority() const override; - #ifdef USE_ZEPHYR /// Set the ADC channel to be used by the ADC sensor. /// @param channel Pointer to an adc_dt_spec structure representing the ADC channel. diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index 748c8634b7..c779fd5893 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -79,7 +79,5 @@ void ADCSensor::set_sample_count(uint8_t sample_count) { void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } -float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace adc } // namespace esphome diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp index 03ce31d3cb..800b2d5261 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp @@ -9,8 +9,6 @@ static const char *const TAG = "adc128s102.sensor"; ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} -float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; } - void ADC128S102Sensor::dump_config() { LOG_SENSOR("", "ADC128S102 Sensor", this); ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_); diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.h b/esphome/components/adc128s102/sensor/adc128s102_sensor.h index 234500c2f4..5e6fc74e9c 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.h +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.h @@ -19,7 +19,6 @@ class ADC128S102Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 03d9d9cd9e..1b1f8335cc 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -150,8 +150,6 @@ void AHT10Component::update() { this->restart_read_(); } -float AHT10Component::get_setup_priority() const { return setup_priority::DATA; } - void AHT10Component::dump_config() { ESP_LOGCONFIG(TAG, "AHT10:"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index a3320c77e0..ce9cd963ad 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -16,7 +16,6 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index b20a8c6cbb..1390b74975 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -176,7 +176,5 @@ void AM2315C::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2315C::get_setup_priority() const { return setup_priority::DATA; } - } // namespace am2315c } // namespace esphome diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h index c8d01beeaa..d7baf01cae 100644 --- a/esphome/components/am2315c/am2315c.h +++ b/esphome/components/am2315c/am2315c.h @@ -33,7 +33,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; void setup() override; - float get_setup_priority() const override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index 055be2aeee..7fef3bb3a6 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -51,7 +51,6 @@ void AM2320Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2320Component::get_setup_priority() const { return setup_priority::DATA; } bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { if (!this->write_bytes(a_register, data, 2)) { diff --git a/esphome/components/am2320/am2320.h b/esphome/components/am2320/am2320.h index da1e87cf65..708dbb632e 100644 --- a/esphome/components/am2320/am2320.h +++ b/esphome/components/am2320/am2320.h @@ -11,7 +11,6 @@ class AM2320Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 93038d3160..260de82d14 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -384,7 +384,6 @@ void APDS9960::process_dataset_(int up, int down, int left, int right) { } } } -float APDS9960::get_setup_priority() const { return setup_priority::DATA; } bool APDS9960::is_proximity_enabled_() const { return #ifdef USE_SENSOR diff --git a/esphome/components/apds9960/apds9960.h b/esphome/components/apds9960/apds9960.h index 2a0fbb5c19..4574b70a42 100644 --- a/esphome/components/apds9960/apds9960.h +++ b/esphome/components/apds9960/apds9960.h @@ -32,7 +32,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void loop() override; diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h index a990f815fe..2e526ca825 100644 --- a/esphome/components/aqi/aqi_sensor.h +++ b/esphome/components/aqi/aqi_sensor.h @@ -10,7 +10,6 @@ class AQISensor : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; } void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; } diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 93a0bff5b3..dd0ab714f7 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -41,8 +41,6 @@ void AS3935Component::dump_config() { #endif } -float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } - void AS3935Component::loop() { if (!this->irq_pin_->digital_read()) return; diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index dc590c268e..5dff1cb0ae 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -74,7 +74,6 @@ class AS3935Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; } diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp index feb8f6cebf..1c0f4bad2c 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -22,8 +22,6 @@ static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R -float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; } - void AS5600Sensor::dump_config() { LOG_SENSOR("", "AS5600 Sensor", this); ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h index 0af9b01ae5..d471be49b5 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -14,7 +14,6 @@ class AS5600Sensor : public PollingComponent, public Parented, public: void update() override; void dump_config() override; - float get_setup_priority() const override; void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } diff --git a/esphome/components/as7341/as7341.cpp b/esphome/components/as7341/as7341.cpp index 893eaa850f..1e78d814c8 100644 --- a/esphome/components/as7341/as7341.cpp +++ b/esphome/components/as7341/as7341.cpp @@ -58,8 +58,6 @@ void AS7341Component::dump_config() { LOG_SENSOR(" ", "NIR", this->nir_); } -float AS7341Component::get_setup_priority() const { return setup_priority::DATA; } - void AS7341Component::update() { this->read_channels(this->channel_readings_); diff --git a/esphome/components/as7341/as7341.h b/esphome/components/as7341/as7341.h index aed7996cef..3ede9d4aa4 100644 --- a/esphome/components/as7341/as7341.h +++ b/esphome/components/as7341/as7341.h @@ -78,7 +78,6 @@ class AS7341Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_f1_sensor(sensor::Sensor *f1_sensor) { this->f1_ = f1_sensor; } diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index cadc06ac6b..2203dd0d71 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -146,7 +146,6 @@ void ATM90E26Component::dump_config() { LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); } -float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; } uint16_t ATM90E26Component::read16_(uint8_t a_register) { uint8_t data[2]; diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h index 3c098d7e91..d15a53ea43 100644 --- a/esphome/components/atm90e26/atm90e26.h +++ b/esphome/components/atm90e26/atm90e26.h @@ -13,7 +13,6 @@ class ATM90E26Component : public PollingComponent, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; } diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index bd7c667c25..045fb7cf45 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -265,6 +265,4 @@ void BH1750Sensor::fail_and_reset_() { this->state_ = IDLE; } -float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace esphome::bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 0460427954..39dbd1d6a9 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -21,7 +21,6 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void dump_config() override; void update() override; void loop() override; - float get_setup_priority() const override; protected: // State machine states diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index c5d4c9c0a5..f396888fd1 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -199,7 +199,6 @@ void BME280Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_)); } -float BME280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bme280_base/bme280_base.h b/esphome/components/bme280_base/bme280_base.h index 0f55ad0101..00781d05b2 100644 --- a/esphome/components/bme280_base/bme280_base.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -76,7 +76,6 @@ class BME280Component : public PollingComponent { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 16435ccfee..5e52c84b3d 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -233,8 +233,6 @@ void BME680Component::dump_config() { } } -float BME680Component::get_setup_priority() const { return setup_priority::DATA; } - void BME680Component::update() { uint8_t meas_control = 0; // No need to fetch, we're setting all fields meas_control |= (this->temperature_oversampling_ & 0b111) << 5; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index cfa7aaca20..d48a42823b 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -99,7 +99,6 @@ class BME680Component : public PollingComponent, public i2c::I2CDevice { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index d969c8fd98..392d071b31 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -181,8 +181,6 @@ void BME680BSECComponent::dump_config() { LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); } -float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } - void BME680BSECComponent::loop() { this->run_(); diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index e52dbe964b..ec919f31df 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -64,7 +64,6 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 91383c8d45..1a42c9d54b 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -106,8 +106,6 @@ void BME68xBSEC2Component::dump_config() { #endif } -float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } - void BME68xBSEC2Component::loop() { this->run_(); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 86d3e5dfbf..8f4d8f61c2 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -48,7 +48,6 @@ class BME68xBSEC2Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; } diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index 4fcc3edb82..1e8c91d7b7 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -263,7 +263,6 @@ void BMI160Component::update() { this->status_clear_warning(); } -float BMI160Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmi160 } // namespace esphome diff --git a/esphome/components/bmi160/bmi160.h b/esphome/components/bmi160/bmi160.h index 47691a4de9..16cab69733 100644 --- a/esphome/components/bmi160/bmi160.h +++ b/esphome/components/bmi160/bmi160.h @@ -14,8 +14,6 @@ class BMI160Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 657da34f9b..9a383b2654 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -131,7 +131,6 @@ bool BMP085Component::set_mode_(uint8_t mode) { ESP_LOGV(TAG, "Setting mode to 0x%02X", mode); return this->write_byte(BMP085_REGISTER_CONTROL, mode); } -float BMP085Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmp085 } // namespace esphome diff --git a/esphome/components/bmp085/bmp085.h b/esphome/components/bmp085/bmp085.h index d84b4d43ef..c7315827e0 100644 --- a/esphome/components/bmp085/bmp085.h +++ b/esphome/components/bmp085/bmp085.h @@ -18,8 +18,6 @@ class BMP085Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - protected: struct CalibrationData { int16_t ac1, ac2, ac3; diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 728eead521..de685e7c27 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -148,7 +148,6 @@ void BMP280Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); } -float BMP280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index a47a794e96..836eafaf8b 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -64,7 +64,6 @@ class BMP280Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index acc28d4e85..d7d9972170 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -179,7 +179,6 @@ void BMP3XXComponent::dump_config() { ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); } } -float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.h b/esphome/components/bmp3xx_base/bmp3xx_base.h index 50f92e04c1..8d2312231b 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.h +++ b/esphome/components/bmp3xx_base/bmp3xx_base.h @@ -73,7 +73,6 @@ class BMP3XXComponent : public PollingComponent { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index 4293d7af07..302c83d7d3 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -7,8 +7,6 @@ namespace cd74hc4067 { static const char *const TAG = "cd74hc4067"; -float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } - void CD74HC4067Component::setup() { this->pin_s0_->setup(); this->pin_s1_->setup(); diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h index 6193513575..76ebc1ebbe 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.h +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -13,7 +13,6 @@ class CD74HC4067Component : public Component { /// Set up the internal sensor array. void setup() override; void dump_config() override; - float get_setup_priority() const override; /// setting pin active by setting the right combination of the four multiplexer input pins void activate_pin(uint8_t pin); diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index ad089bbe7d..8c33e56457 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -10,8 +10,6 @@ namespace cm1106 { class CM1106Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index 901aeaf259..fb5e156da9 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -10,8 +10,6 @@ namespace combination { class CombinationComponent : public Component, public sensor::Sensor { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - /// @brief Logs all source sensor's names virtual void log_source_sensors() = 0; diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 482636dd81..7c5ee833a4 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -62,8 +62,6 @@ void CSE7761Component::dump_config() { this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); } -float CSE7761Component::get_setup_priority() const { return setup_priority::DATA; } - void CSE7761Component::update() { if (this->data_.ready) { this->get_data_(); diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h index 71846cdcab..289c5e7e19 100644 --- a/esphome/components/cse7761/cse7761.h +++ b/esphome/components/cse7761/cse7761.h @@ -28,7 +28,6 @@ class CSE7761Component : public PollingComponent, public uart::UARTDevice { void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index c36e57c929..df4872deac 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -37,7 +37,6 @@ void CSE7766Component::loop() { this->raw_data_index_ = (this->raw_data_index_ + 1) % 24; } } -float CSE7766Component::get_setup_priority() const { return setup_priority::DATA; } bool CSE7766Component::check_byte_() { uint8_t index = this->raw_data_index_; diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 8902eafe3c..efddccd3c5 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -23,7 +23,6 @@ class CSE7766Component : public Component, public uart::UARTDevice { void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } void loop() override; - float get_setup_priority() const override; void dump_config() override; protected: diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 402cf9fee7..5dfaeeff39 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -159,7 +159,6 @@ void CurrentBasedCover::dump_config() { this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } -float CurrentBasedCover::get_setup_priority() const { return setup_priority::DATA; } void CurrentBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index f7993f1550..76bd85cdf7 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -14,7 +14,6 @@ class CurrentBasedCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 2d270cc56e..90ccee78f8 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -104,8 +104,6 @@ void DalyBmsComponent::loop() { } } -float DalyBmsComponent::get_setup_priority() const { return setup_priority::DATA; } - void DalyBmsComponent::request_data_(uint8_t data_id) { uint8_t request_message[DALY_FRAME_SIZE]; diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index e6d476bcdd..1983ba0ef1 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -72,7 +72,6 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void update() override; void loop() override; - float get_setup_priority() const override; void set_address(uint8_t address) { this->addr_ = address; } protected: diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 276ea24717..fef247f168 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -63,8 +63,6 @@ void DHT::update() { } } -float DHT::get_setup_priority() const { return setup_priority::DATA; } - void DHT::set_dht_model(DHTModel model) { this->model_ = model; this->is_auto_detect_ = model == DHT_MODEL_AUTO_DETECT; diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 9047dd2c96..4671ee6f27 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -51,8 +51,6 @@ class DHT : public PollingComponent { void dump_config() override; /// Update sensor values and push them to the frontend. void update() override; - /// HARDWARE_LATE setup priority. - float get_setup_priority() const override; protected: bool read_sensor_(float *temperature, float *humidity, bool report_errors); diff --git a/esphome/components/dht12/dht12.cpp b/esphome/components/dht12/dht12.cpp index 445d150be0..1d884daad6 100644 --- a/esphome/components/dht12/dht12.cpp +++ b/esphome/components/dht12/dht12.cpp @@ -49,7 +49,7 @@ void DHT12Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float DHT12Component::get_setup_priority() const { return setup_priority::DATA; } + bool DHT12Component::read_data_(uint8_t *data) { if (!this->read_bytes(0, data, 5)) { ESP_LOGW(TAG, "Updating DHT12 failed!"); diff --git a/esphome/components/dht12/dht12.h b/esphome/components/dht12/dht12.h index 2a706039ba..ab19d7c723 100644 --- a/esphome/components/dht12/dht12.h +++ b/esphome/components/dht12/dht12.h @@ -11,7 +11,6 @@ class DHT12Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index 6b6f9622fa..aa0a77cdd8 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -98,8 +98,6 @@ void DPS310Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float DPS310Component::get_setup_priority() const { return setup_priority::DATA; } - void DPS310Component::update() { if (!this->update_in_progress_) { this->update_in_progress_ = true; diff --git a/esphome/components/dps310/dps310.h b/esphome/components/dps310/dps310.h index 50e7d93c8a..dce220d44b 100644 --- a/esphome/components/dps310/dps310.h +++ b/esphome/components/dps310/dps310.h @@ -40,7 +40,6 @@ class DPS310Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index adbd7b5487..5c0e98290b 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -26,8 +26,6 @@ void DS1307Component::dump_config() { RealTimeClock::dump_config(); } -float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } - void DS1307Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index f7f06253b7..1712056006 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -12,7 +12,6 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 40a728d025..f801769d27 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -43,8 +43,6 @@ void DutyCycleSensor::update() { this->last_update_ = now; } -float DutyCycleSensor::get_setup_priority() const { return setup_priority::DATA; } - void IRAM_ATTR DutyCycleSensorStore::gpio_intr(DutyCycleSensorStore *arg) { const bool new_level = arg->pin.digital_read(); if (new_level == arg->last_level) diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index ffb1802e14..ffb8e3b622 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -22,7 +22,6 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override; - float get_setup_priority() const override; void dump_config() override; void update() override; diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 602e31db14..22f28be9bc 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -55,8 +55,6 @@ void EE895Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float EE895Component::get_setup_priority() const { return setup_priority::DATA; } - void EE895Component::update() { write_command_(TEMPERATURE_ADDRESS, 2); this->set_timeout(50, [this]() { diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h index 83bd7c6e82..259b7c524b 100644 --- a/esphome/components/ee895/ee895.h +++ b/esphome/components/ee895/ee895.h @@ -14,7 +14,6 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.cpp b/esphome/components/emc2101/sensor/emc2101_sensor.cpp index 2a199f48e9..3014c7da07 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.cpp +++ b/esphome/components/emc2101/sensor/emc2101_sensor.cpp @@ -7,8 +7,6 @@ namespace emc2101 { static const char *const TAG = "EMC2101.sensor"; -float EMC2101Sensor::get_setup_priority() const { return setup_priority::DATA; } - void EMC2101Sensor::dump_config() { ESP_LOGCONFIG(TAG, "Emc2101 sensor:"); LOG_SENSOR(" ", "Internal temperature", this->internal_temperature_sensor_); diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.h b/esphome/components/emc2101/sensor/emc2101_sensor.h index 3e8dcebc8e..3e033f58a7 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.h +++ b/esphome/components/emc2101/sensor/emc2101_sensor.h @@ -15,8 +15,6 @@ class EMC2101Sensor : public PollingComponent { void dump_config() override; /** Used by ESPHome framework. */ void update() override; - /** Used by ESPHome framework. */ - float get_setup_priority() const override; /** Used by ESPHome framework. */ void set_internal_temperature_sensor(sensor::Sensor *sensor) { this->internal_temperature_sensor_ = sensor; } diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index e28f024136..ea8a5ec186 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -111,7 +111,7 @@ void EndstopCover::dump_config() { LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); } -float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } + void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index 6f72b2b805..32ede12335 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -13,7 +13,6 @@ class EndstopCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; Trigger<> *get_open_trigger() { return &this->open_trigger_; } Trigger<> *get_close_trigger() { return &this->close_trigger_; } diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index 98a300f5d7..8bee9bfb18 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -136,8 +136,6 @@ void ENS210Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } - void ENS210Component::update() { // Execute a single measurement if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h index 0fb6ff634d..ae2bf81b5f 100644 --- a/esphome/components/ens210/ens210.h +++ b/esphome/components/ens210/ens210.h @@ -10,7 +10,6 @@ namespace ens210 { /// This class implements support for the ENS210 relative humidity and temperature i2c sensor. class ENS210Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override; void dump_config() override; void setup() override; void update() override; diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 5466d2e7ef..cfe06b1673 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -235,8 +235,6 @@ void ESP32Camera::loop() { this->single_requesters_ = 0; } -float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } - /* ---------------- constructors ---------------- */ ESP32Camera::ESP32Camera() { this->config_.pin_pwdn = -1; diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index e97eb27c70..eea93b7e01 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -159,7 +159,6 @@ class ESP32Camera : public camera::Camera { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; /* public API (specific) */ void start_stream(camera::CameraRequester requester) override; void stop_stream(camera::CameraRequester requester) override; diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 812c746301..7f45f2ccb4 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -51,7 +51,6 @@ class ESP32TouchComponent : public Component { void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index ddf38f2f55..8b381564b2 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -77,8 +77,6 @@ void GDK101Component::dump_config() { #endif // USE_TEXT_SENSOR } -float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } - bool GDK101Component::read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len) { uint8_t retry = NUMBER_OF_READ_RETRIES; bool status = false; diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index f250a42a54..abe417e0f9 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -40,7 +40,6 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 6613187b20..972f2ce60c 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -20,7 +20,6 @@ class GP8403Component : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_model(GP8403Model model) { this->model_ = model; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp index 4d0f77df1b..4c2d367b24 100644 --- a/esphome/components/hc8/hc8.cpp +++ b/esphome/components/hc8/hc8.cpp @@ -86,8 +86,6 @@ void HC8Component::calibrate(uint16_t baseline) { this->flush(); } -float HC8Component::get_setup_priority() const { return setup_priority::DATA; } - void HC8Component::dump_config() { ESP_LOGCONFIG(TAG, "HC8:\n" diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h index 7711fb8c97..74257fab14 100644 --- a/esphome/components/hc8/hc8.h +++ b/esphome/components/hc8/hc8.h @@ -11,8 +11,6 @@ namespace esphome::hc8 { class HC8Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index 7ad0764f1f..a5bece82c4 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -16,8 +16,6 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index f037ee9d8b..d0fd697d8f 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -48,7 +48,6 @@ void HLW8012Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Energy", this->energy_sensor_); } -float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { // HLW8012 has 50% duty cycle pulse_counter::pulse_counter_t raw_cf = this->cf_store_.read_raw_value(); diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index 312391f533..8a13ec07d8 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -31,7 +31,6 @@ class HLW8012Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_initial_mode(HLW8012InitialMode initial_mode) { diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 00fb85397c..9343b47823 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -31,8 +31,6 @@ void HM3301Component::dump_config() { LOG_SENSOR(" ", "AQI", this->aqi_sensor_); } -float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } - void HM3301Component::update() { if (this->read(data_buffer_, 29) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Read result failed"); diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index e155ed6b4b..6b10a5e237 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -23,7 +23,6 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index 101493ad91..b62381a287 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -83,7 +83,6 @@ void HMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); } -float HMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } void HMC5883LComponent::update() { uint16_t raw_x, raw_y, raw_z; if (!this->read_byte_16(HMC5883L_REGISTER_DATA_X_MSB, &raw_x) || diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 06fba2af9d..8eae0f7a50 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -39,7 +39,6 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_oversampling(HMC5883LOversampling oversampling) { oversampling_ = oversampling; } diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index e72c5a21f5..d039892073 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -11,8 +11,6 @@ void HomeassistantTime::dump_config() { RealTimeClock::dump_config(); } -float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } - void HomeassistantTime::setup() { global_homeassistant_time = this; } void HomeassistantTime::update() { api::global_api_server->request_time(); } diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 36e28ea16b..7b5842fefd 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -13,7 +13,6 @@ class HomeassistantTime : public time::RealTimeClock { void update() override; void dump_config() override; void set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); } - float get_setup_priority() const override; }; extern HomeassistantTime *global_homeassistant_time; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp index 5f2b009972..904672d136 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -91,7 +91,5 @@ void HoneywellHIComponent::dump_config() { LOG_UPDATE_INTERVAL(this); } -float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace honeywell_hih_i2c } // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h index 4457eab1da..79140f7399 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.h +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -11,7 +11,6 @@ namespace honeywell_hih_i2c { class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override; void loop() override; void update() override; diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index cde6886109..972e72c170 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -43,7 +43,6 @@ void HTE501Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } void HTE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; this->write(address_1, 2); diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index a7072d5bdb..b47daf9157 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -13,7 +13,6 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index c5d91d3dd0..58a28b213f 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -143,7 +143,5 @@ uint8_t HTU21DComponent::get_heater_level() { return raw_heater & 0xF; } -float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace htu21d } // namespace esphome diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 277c6ca3e5..594be78326 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -28,8 +28,6 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_level(uint8_t level); uint8_t get_heater_level(); - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp index 562078aacb..4bb38a11a2 100644 --- a/esphome/components/htu31d/htu31d.cpp +++ b/esphome/components/htu31d/htu31d.cpp @@ -259,11 +259,5 @@ void HTU31DComponent::set_heater_state(bool desired) { } } -/** - * Sets the startup priority for this component. - * - * @returns The startup priority - */ -float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; } } // namespace htu31d } // namespace esphome diff --git a/esphome/components/htu31d/htu31d.h b/esphome/components/htu31d/htu31d.h index 9462133ced..24d85243cc 100644 --- a/esphome/components/htu31d/htu31d.h +++ b/esphome/components/htu31d/htu31d.h @@ -20,8 +20,6 @@ class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_state(bool desired); bool is_heater_enabled(); - float get_setup_priority() const override; - protected: bool reset_(); uint32_t read_serial_num_(); diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 67ec4549df..f2e3234127 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -22,7 +22,6 @@ void HX711Sensor::dump_config() { LOG_PIN(" SCK Pin: ", this->sck_pin_); LOG_UPDATE_INTERVAL(this); } -float HX711Sensor::get_setup_priority() const { return setup_priority::DATA; } void HX711Sensor::update() { uint32_t result; if (this->read_sensor_(&result)) { diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index a92bb9945d..37723ee81f 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -23,7 +23,6 @@ class HX711Sensor : public sensor::Sensor, public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 4872d68610..983a0a6649 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -284,7 +284,5 @@ void HydreonRGxxComponent::process_line_() { } } -float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hydreon_rgxx } // namespace esphome diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h index 76b0985a24..e3f9798a93 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.h +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -53,8 +53,6 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - void set_disable_led(bool disable_led) { this->disable_led_ = disable_led; } protected: diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp index f187e054a8..4c0e3cd96e 100644 --- a/esphome/components/hyt271/hyt271.cpp +++ b/esphome/components/hyt271/hyt271.cpp @@ -46,7 +46,5 @@ void HYT271Component::update() { this->status_clear_warning(); }); } -float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hyt271 } // namespace esphome diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h index 64f32a651c..19409d830c 100644 --- a/esphome/components/hyt271/hyt271.h +++ b/esphome/components/hyt271/hyt271.h @@ -16,8 +16,6 @@ class HYT271Component : public PollingComponent, public i2c::I2CDevice { /// Update the sensor values (temperature+humidity). void update() override; - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index ea8c5cea9d..278017651b 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -151,8 +151,6 @@ void INA219Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); } -float INA219Component::get_setup_priority() const { return setup_priority::DATA; } - void INA219Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; diff --git a/esphome/components/ina219/ina219.h b/esphome/components/ina219/ina219.h index 115fa886e0..bcadb65e36 100644 --- a/esphome/components/ina219/ina219.h +++ b/esphome/components/ina219/ina219.h @@ -13,7 +13,6 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void on_powerdown() override; diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index c4d4fb896e..cbc44c9a1a 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -104,8 +104,6 @@ void INA226Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); } -float INA226Component::get_setup_priority() const { return setup_priority::DATA; } - void INA226Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h index 61214fea0e..0aa66ff765 100644 --- a/esphome/components/ina226/ina226.h +++ b/esphome/components/ina226/ina226.h @@ -45,7 +45,6 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 7185d21810..de01c99a19 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -79,8 +79,6 @@ void INA2XX::setup() { this->state_ = State::IDLE; } -float INA2XX::get_setup_priority() const { return setup_priority::DATA; } - void INA2XX::update() { ESP_LOGD(TAG, "Updating"); if (this->is_ready() && this->state_ == State::IDLE) { diff --git a/esphome/components/ina2xx_base/ina2xx_base.h b/esphome/components/ina2xx_base/ina2xx_base.h index ba0999b28e..104c384a0d 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.h +++ b/esphome/components/ina2xx_base/ina2xx_base.h @@ -114,7 +114,6 @@ enum INAModel : uint8_t { INA_UNKNOWN = 0, INA_228, INA_229, INA_238, INA_239, I class INA2XX : public PollingComponent { public: void setup() override; - float get_setup_priority() const override; void update() override; void loop() override; void dump_config() override; diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 8243764147..d03183e002 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -113,7 +113,6 @@ void INA3221Component::update() { } } -float INA3221Component::get_setup_priority() const { return setup_priority::DATA; } void INA3221Component::set_shunt_resistance(int channel, float resistance_ohm) { this->channels_[channel].shunt_resistance_ = resistance_ohm; } diff --git a/esphome/components/ina3221/ina3221.h b/esphome/components/ina3221/ina3221.h index f593badc09..3769df77aa 100644 --- a/esphome/components/ina3221/ina3221.h +++ b/esphome/components/ina3221/ina3221.h @@ -12,7 +12,6 @@ class INA3221Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; void set_bus_voltage_sensor(int channel, sensor::Sensor *obj) { this->channels_[channel].bus_voltage_sensor_ = obj; } void set_shunt_voltage_sensor(int channel, sensor::Sensor *obj) { diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index e5fa035682..534939f9af 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -30,8 +30,6 @@ void KamstrupKMPComponent::dump_config() { this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8); } -float KamstrupKMPComponent::get_setup_priority() const { return setup_priority::DATA; } - void KamstrupKMPComponent::update() { if (this->heat_energy_sensor_ != nullptr) { this->command_queue_.push(CMD_HEAT_ENERGY); diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index c9cc9c5a39..f84e360132 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -83,7 +83,6 @@ class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice { void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_volume_sensor(sensor::Sensor *sensor) { this->volume_sensor_ = sensor; } void dump_config() override; - float get_setup_priority() const override; void update() override; void loop() override; void add_custom_sensor(sensor::Sensor *sensor, uint16_t command) { diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 36f6d74ba0..186686e472 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -47,8 +47,6 @@ void KMeterISOComponent::setup() { } } -float KMeterISOComponent::get_setup_priority() const { return setup_priority::DATA; } - void KMeterISOComponent::update() { uint8_t read_buf[4]; diff --git a/esphome/components/kmeteriso/kmeteriso.h b/esphome/components/kmeteriso/kmeteriso.h index c8bed662b0..6f1978105f 100644 --- a/esphome/components/kmeteriso/kmeteriso.h +++ b/esphome/components/kmeteriso/kmeteriso.h @@ -17,7 +17,6 @@ class KMeterISOComponent : public PollingComponent, public i2c::I2CDevice { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void setup() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.cpp b/esphome/components/m5stack_8angle/m5stack_8angle.cpp index 5a9a5e8c9d..2de900c21d 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.cpp +++ b/esphome/components/m5stack_8angle/m5stack_8angle.cpp @@ -69,7 +69,5 @@ int8_t M5Stack8AngleComponent::read_switch() { } } -float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace m5stack_8angle } // namespace esphome diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.h b/esphome/components/m5stack_8angle/m5stack_8angle.h index 831b1422fd..4942518054 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.h +++ b/esphome/components/m5stack_8angle/m5stack_8angle.h @@ -21,7 +21,6 @@ class M5Stack8AngleComponent : public i2c::I2CDevice, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); int8_t read_switch(); diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index e8cf4d5ab1..dfd59f1e7d 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -81,8 +81,6 @@ void MAX17043Component::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_); } -float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; } - void MAX17043Component::sleep_mode() { if (!this->is_failed()) { if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) { diff --git a/esphome/components/max17043/max17043.h b/esphome/components/max17043/max17043.h index 540b977789..f477ce5948 100644 --- a/esphome/components/max17043/max17043.h +++ b/esphome/components/max17043/max17043.h @@ -11,7 +11,6 @@ class MAX17043Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void sleep_mode(); diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index b5be3106cf..99129880f4 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -31,7 +31,6 @@ void MAX31855Sensor::dump_config() { ESP_LOGCONFIG(TAG, " Reference temperature disabled."); } } -float MAX31855Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX31855Sensor::read_data_() { this->enable(); delay(1); diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index 822e256587..b755d240f2 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -18,7 +18,6 @@ class MAX31855Sensor : public sensor::Sensor, void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index cc573cbc53..ff65c8c5c9 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -197,7 +197,5 @@ uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { return value; } -float MAX31856Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace max31856 } // namespace esphome diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h index 8d64cfe8bc..a27ababa2e 100644 --- a/esphome/components/max31856/max31856.h +++ b/esphome/components/max31856/max31856.h @@ -76,7 +76,6 @@ class MAX31856Sensor : public sensor::Sensor, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_filter(MAX31856ConfigFilter filter) { this->filter_ = filter; } void set_thermocouple_type(MAX31856ThermocoupleType thermocouple_type) { this->thermocouple_type_ = thermocouple_type; diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index a9c5204cf5..09b8368d07 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -90,8 +90,6 @@ void MAX31865Sensor::dump_config() { (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); } -float MAX31865Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MAX31865Sensor::read_data_() { // Read temperature, disable V_BIAS (save power) const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG); diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h index b83753a678..440c6523a6 100644 --- a/esphome/components/max31865/max31865.h +++ b/esphome/components/max31865/max31865.h @@ -34,7 +34,6 @@ class MAX31865Sensor : public sensor::Sensor, void set_num_rtd_wires(uint8_t rtd_wires) { rtd_wires_ = rtd_wires; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 928fc47696..8b8e38c1ea 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -51,8 +51,6 @@ void MAX44009Sensor::dump_config() { } } -float MAX44009Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MAX44009Sensor::update() { // update sensor illuminance value float lux = this->read_illuminance_(); diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index c85d1c1028..59eea66ed9 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -16,7 +16,6 @@ class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_mode(MAX44009Mode mode); bool set_continuous_mode(); diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index 54e0330ff7..c329cdfd42 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -23,7 +23,6 @@ void MAX6675Sensor::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); } -float MAX6675Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX6675Sensor::read_data_() { this->enable(); delay(1); diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h index ab0f06b041..f0db4a6c26 100644 --- a/esphome/components/max6675/max6675.h +++ b/esphome/components/max6675/max6675.h @@ -14,7 +14,6 @@ class MAX6675Sensor : public sensor::Sensor, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp index 81eb0a812f..ee052e9fb7 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp @@ -7,8 +7,6 @@ namespace mcp3008 { static const char *const TAG = "mcp3008.sensor"; -float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MCP3008Sensor::dump_config() { ESP_LOGCONFIG(TAG, "MCP3008Sensor:\n" diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.h b/esphome/components/mcp3008/sensor/mcp3008_sensor.h index ebaeab966f..9478d38e74 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.h +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.h @@ -19,7 +19,6 @@ class MCP3008Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index e673537be1..711448cf44 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -7,8 +7,6 @@ namespace mcp3204 { static const char *const TAG = "mcp3204.sensor"; -float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MCP3204Sensor::dump_config() { LOG_SENSOR("", "MCP3204 Sensor", this); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 5665b80b98..2bf75a9c1e 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -19,7 +19,6 @@ class MCP3204Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp index 088d33887f..ed12e52239 100644 --- a/esphome/components/mcp9808/mcp9808.cpp +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -73,7 +73,6 @@ void MCP9808Sensor::update() { this->publish_state(temp); this->status_clear_warning(); } -float MCP9808Sensor::get_setup_priority() const { return setup_priority::DATA; } } // namespace mcp9808 } // namespace esphome diff --git a/esphome/components/mcp9808/mcp9808.h b/esphome/components/mcp9808/mcp9808.h index 19aa3117c3..894e4599d0 100644 --- a/esphome/components/mcp9808/mcp9808.h +++ b/esphome/components/mcp9808/mcp9808.h @@ -11,7 +11,6 @@ class MCP9808Sensor : public sensor::Sensor, public PollingComponent, public i2c public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; }; diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index b6b4031e16..bccea7d423 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -137,8 +137,6 @@ bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *respo return this->read_array(response, MHZ19_RESPONSE_LENGTH); } -float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; } - void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index a27f1c31eb..e577b98537 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -22,8 +22,6 @@ enum MHZ19DetectionRange { class MHZ19Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index 8181ece94c..60413b32d7 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -37,7 +37,6 @@ void MICS4514Component::dump_config() { LOG_SENSOR(" ", "Hydrogen", this->hydrogen_sensor_); LOG_SENSOR(" ", "Ammonia", this->ammonia_sensor_); } -float MICS4514Component::get_setup_priority() const { return setup_priority::DATA; } void MICS4514Component::update() { if (!this->warmed_up_) { return; diff --git a/esphome/components/mics_4514/mics_4514.h b/esphome/components/mics_4514/mics_4514.h index d2fefc3630..e7271314c8 100644 --- a/esphome/components/mics_4514/mics_4514.h +++ b/esphome/components/mics_4514/mics_4514.h @@ -19,7 +19,6 @@ class MICS4514Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index 21a5b3a829..ee52f9b9ab 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -132,8 +132,6 @@ void MLX90393Cls::dump_config() { LOG_SENSOR(" ", "Temperature", this->t_sensor_); } -float MLX90393Cls::get_setup_priority() const { return setup_priority::DATA; } - void MLX90393Cls::update() { MLX90393::txyz data; diff --git a/esphome/components/mlx90393/sensor_mlx90393.h b/esphome/components/mlx90393/sensor_mlx90393.h index 8a6f3321f9..845ae87e09 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.h +++ b/esphome/components/mlx90393/sensor_mlx90393.h @@ -25,7 +25,6 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90 public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_drdy_gpio(GPIOPin *pin) { drdy_pin_ = pin; } diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 8e53b9e3c3..8a514cbc26 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -71,8 +71,6 @@ void MLX90614Component::dump_config() { LOG_SENSOR(" ", "Object", this->object_sensor_); } -float MLX90614Component::get_setup_priority() const { return setup_priority::DATA; } - void MLX90614Component::update() { uint8_t emissivity[3]; if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) { diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index fa6fb523bb..bf081c3e90 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -12,7 +12,6 @@ class MLX90614Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; void set_ambient_sensor(sensor::Sensor *ambient_sensor) { ambient_sensor_ = ambient_sensor; } void set_object_sensor(sensor::Sensor *object_sensor) { object_sensor_ = object_sensor; } diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index d6321eae8f..1cbc84191f 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -91,8 +91,6 @@ void MMC5603Component::dump_config() { LOG_SENSOR(" ", "Heading", this->heading_sensor_); } -float MMC5603Component::get_setup_priority() const { return setup_priority::DATA; } - void MMC5603Component::update() { uint8_t ctrl0 = (this->auto_set_reset_) ? 0x21 : 0x01; if (!this->write_byte(MMC56X3_CTRL0_REG, ctrl0)) { diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index 09718bd3b7..f827e27e04 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -17,7 +17,6 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_datarate(MMC5603Datarate datarate) { datarate_ = datarate; } diff --git a/esphome/components/mmc5983/mmc5983.cpp b/esphome/components/mmc5983/mmc5983.cpp index 1e0065020c..b038084a72 100644 --- a/esphome/components/mmc5983/mmc5983.cpp +++ b/esphome/components/mmc5983/mmc5983.cpp @@ -133,7 +133,5 @@ void MMC5983Component::dump_config() { LOG_SENSOR(" ", "Z", this->z_sensor_); } -float MMC5983Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace mmc5983 } // namespace esphome diff --git a/esphome/components/mmc5983/mmc5983.h b/esphome/components/mmc5983/mmc5983.h index d425418904..3e87e54daa 100644 --- a/esphome/components/mmc5983/mmc5983.h +++ b/esphome/components/mmc5983/mmc5983.h @@ -12,7 +12,6 @@ class MMC5983Component : public PollingComponent, public i2c::I2CDevice { void update() override; void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index ecbee11c48..91a84d061a 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -140,7 +140,6 @@ void MPU6050Component::update() { this->status_clear_warning(); } -float MPU6050Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace mpu6050 } // namespace esphome diff --git a/esphome/components/mpu6050/mpu6050.h b/esphome/components/mpu6050/mpu6050.h index ab410105c0..cc7c3620df 100644 --- a/esphome/components/mpu6050/mpu6050.h +++ b/esphome/components/mpu6050/mpu6050.h @@ -14,8 +14,6 @@ class MPU6050Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp index 6fdf7b8684..68b77b59c9 100644 --- a/esphome/components/mpu6886/mpu6886.cpp +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -146,7 +146,5 @@ void MPU6886Component::update() { this->status_clear_warning(); } -float MPU6886Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace mpu6886 } // namespace esphome diff --git a/esphome/components/mpu6886/mpu6886.h b/esphome/components/mpu6886/mpu6886.h index 04551ae56d..96e2bf61a1 100644 --- a/esphome/components/mpu6886/mpu6886.h +++ b/esphome/components/mpu6886/mpu6886.h @@ -14,8 +14,6 @@ class MPU6886Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 5a7622e783..7ed73400c8 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -38,7 +38,6 @@ void MS5611Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float MS5611Component::get_setup_priority() const { return setup_priority::DATA; } void MS5611Component::update() { // request temperature reading if (!this->write_bytes(MS5611_CMD_CONV_D2 + 0x08, nullptr, 0)) { diff --git a/esphome/components/ms5611/ms5611.h b/esphome/components/ms5611/ms5611.h index 476db79612..7e4806f319 100644 --- a/esphome/components/ms5611/ms5611.h +++ b/esphome/components/ms5611/ms5611.h @@ -11,7 +11,6 @@ class MS5611Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index 56dc919968..e46bfed193 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -287,7 +287,6 @@ void MSA3xxComponent::update() { this->status_.never_published = false; this->status_clear_warning(); } -float MSA3xxComponent::get_setup_priority() const { return setup_priority::DATA; } void MSA3xxComponent::set_offset(float offset_x, float offset_y, float offset_z) { this->offset_x_ = offset_x; diff --git a/esphome/components/msa3xx/msa3xx.h b/esphome/components/msa3xx/msa3xx.h index 644109dab0..439d3b5f4d 100644 --- a/esphome/components/msa3xx/msa3xx.h +++ b/esphome/components/msa3xx/msa3xx.h @@ -220,8 +220,6 @@ class MSA3xxComponent : public PollingComponent, public i2c::I2CDevice { void loop() override; void update() override; - float get_setup_priority() const override; - void set_model(Model model) { this->model_ = model; } void set_offset(float offset_x, float offset_y, float offset_z); void set_range(Range range) { this->range_ = range; } diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 5edbc79862..937239b98d 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -296,8 +296,6 @@ void NAU7802Sensor::loop() { } } -float NAU7802Sensor::get_setup_priority() const { return setup_priority::DATA; } - void NAU7802Sensor::update() { if (!this->is_data_ready_()) { ESP_LOGW(TAG, "No measurements ready!"); diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h index 05452851ca..ae39e167a4 100644 --- a/esphome/components/nau7802/nau7802.h +++ b/esphome/components/nau7802/nau7802.h @@ -62,7 +62,6 @@ class NAU7802Sensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e57a258edb..fd6ce0a24b 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -193,7 +193,6 @@ void Nextion::dump_config() { #endif } -float Nextion::get_setup_priority() const { return setup_priority::DATA; } void Nextion::update() { if (!this->is_setup()) { return; diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c543e14bfe..c42ddba9b5 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1048,7 +1048,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void setup() override; void set_brightness(float brightness) { this->brightness_ = brightness; } - float get_setup_priority() const override; void update() override; void loop() override; void set_writer(const nextion_writer_t &writer); diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index c531d2ec8f..995abdff37 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -29,8 +29,6 @@ void NPI19Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } -float NPI19Component::get_setup_priority() const { return setup_priority::DATA; } - i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pressure) { // initiate data read from device i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND)); diff --git a/esphome/components/npi19/npi19.h b/esphome/components/npi19/npi19.h index df289dffc1..8e6a8e3bfa 100644 --- a/esphome/components/npi19/npi19.h +++ b/esphome/components/npi19/npi19.h @@ -15,7 +15,6 @@ class NPI19Component : public PollingComponent, public i2c::I2CDevice { this->raw_pressure_sensor_ = raw_pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index cc500ba429..e2097bdd77 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -12,7 +12,6 @@ void NTC::setup() { this->process_(this->sensor_->state); } void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this); } -float NTC::get_setup_priority() const { return setup_priority::DATA; } void NTC::process_(float value) { if (std::isnan(value)) { this->publish_state(NAN); diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h index c8592e0fe8..a0c72340de 100644 --- a/esphome/components/ntc/ntc.h +++ b/esphome/components/ntc/ntc.h @@ -14,7 +14,6 @@ class NTC : public Component, public sensor::Sensor { void set_c(double c) { c_ = c; } void setup() override; void dump_config() override; - float get_setup_priority() const override; protected: void process_(float value); diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index f5f7ab9412..a942e45035 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -116,7 +116,5 @@ void OPT3001Sensor::update() { }); } -float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace opt3001 } // namespace esphome diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h index ae3fde5c54..3bce9f0aeb 100644 --- a/esphome/components/opt3001/opt3001.h +++ b/esphome/components/opt3001/opt3001.h @@ -12,7 +12,6 @@ class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c public: void dump_config() override; void update() override; - float get_setup_priority() const override; protected: // checks if one-shot is complete before reading the result and returning it diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index f38b60b55d..03ed78654f 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -26,8 +26,6 @@ void PCF85063Component::dump_config() { RealTimeClock::dump_config(); } -float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } - void PCF85063Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h index b7034d4f00..697837f223 100644 --- a/esphome/components/pcf85063/pcf85063.h +++ b/esphome/components/pcf85063/pcf85063.h @@ -12,7 +12,6 @@ class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 2090936bb6..dc68807aef 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -26,8 +26,6 @@ void PCF8563Component::dump_config() { RealTimeClock::dump_config(); } -float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } - void PCF8563Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h index 81aa816b42..cd37d05816 100644 --- a/esphome/components/pcf8563/pcf8563.h +++ b/esphome/components/pcf8563/pcf8563.h @@ -12,7 +12,6 @@ class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp index c466c4bb25..fe8890e777 100644 --- a/esphome/components/pm1006/pm1006.cpp +++ b/esphome/components/pm1006/pm1006.cpp @@ -44,8 +44,6 @@ void PM1006Component::loop() { } } -float PM1006Component::get_setup_priority() const { return setup_priority::DATA; } - uint8_t PM1006Component::pm1006_checksum_(const uint8_t *command_data, uint8_t length) const { uint8_t sum = 0; for (uint8_t i = 0; i < length; i++) { diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h index 98637dad71..6b6332e1e3 100644 --- a/esphome/components/pm1006/pm1006.h +++ b/esphome/components/pm1006/pm1006.h @@ -18,8 +18,6 @@ class PM1006Component : public PollingComponent, public uart::UARTDevice { void loop() override; void update() override; - float get_setup_priority() const override; - protected: optional check_byte_() const; void parse_data_(); diff --git a/esphome/components/pm2005/pm2005.h b/esphome/components/pm2005/pm2005.h index 219fbae5cb..e788569b7e 100644 --- a/esphome/components/pm2005/pm2005.h +++ b/esphome/components/pm2005/pm2005.h @@ -14,8 +14,6 @@ enum SensorType { class PM2005Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - void set_sensor_type(SensorType sensor_type) { this->sensor_type_ = sensor_type; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { this->pm_1_0_sensor_ = pm_1_0_sensor; } diff --git a/esphome/components/pmwcs3/pmwcs3.cpp b/esphome/components/pmwcs3/pmwcs3.cpp index 95638851b5..2ed7789c53 100644 --- a/esphome/components/pmwcs3/pmwcs3.cpp +++ b/esphome/components/pmwcs3/pmwcs3.cpp @@ -53,8 +53,6 @@ void PMWCS3Component::water_calibration() { void PMWCS3Component::update() { this->read_data_(); } -float PMWCS3Component::get_setup_priority() const { return setup_priority::DATA; } - void PMWCS3Component::dump_config() { ESP_LOGCONFIG(TAG, "PMWCS3"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h index d63c516586..b1e26eec4f 100644 --- a/esphome/components/pmwcs3/pmwcs3.h +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -14,7 +14,6 @@ class PMWCS3Component : public PollingComponent, public i2c::I2CDevice { public: void update() override; void dump_config() override; - float get_setup_priority() const override; void set_e25_sensor(sensor::Sensor *e25_sensor) { e25_sensor_ = e25_sensor; } void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; } diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 733810c242..5366aab54e 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -426,8 +426,6 @@ bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) { return false; } -float PN532::get_setup_priority() const { return setup_priority::DATA; } - void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 488ec4af3b..73a6c15164 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -35,7 +35,6 @@ class PN532 : public PollingComponent { void dump_config() override; void update() override; - float get_setup_priority() const override; void loop() override; void on_powerdown() override { powerdown(); } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 9a7630a7be..007deb66e5 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -125,8 +125,6 @@ void PulseMeterSensor::loop() { } } -float PulseMeterSensor::get_setup_priority() const { return setup_priority::DATA; } - void PulseMeterSensor::dump_config() { LOG_SENSOR("", "Pulse Meter", this); LOG_PIN(" Pin: ", this->pin_); diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 748bab29ac..5800c4ec42 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -27,7 +27,6 @@ class PulseMeterSensor : public sensor::Sensor, public Component { void setup() override; void loop() override; - float get_setup_priority() const override; void dump_config() override; protected: diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 61b5356c90..1dc7caaf16 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -192,8 +192,6 @@ void PylontechComponent::process_line_(std::string &buffer) { } } -float PylontechComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace pylontech } // namespace esphome diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h index 10c669ad9a..5727928a60 100644 --- a/esphome/components/pylontech/pylontech.h +++ b/esphome/components/pylontech/pylontech.h @@ -34,8 +34,6 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - void register_listener(PylontechListener *listener) { this->listeners_.push_back(listener); } protected: diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 693614581c..bc2adb5cfe 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -86,8 +86,6 @@ void QMC5883LComponent::dump_config() { LOG_PIN(" DRDY Pin: ", this->drdy_pin_); } -float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } - void QMC5883LComponent::update() { i2c::ErrorCode err; uint8_t status = false; diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 5ba7180e23..21ef9c2a17 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -31,7 +31,6 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; } diff --git a/esphome/components/rd03d/rd03d.h b/esphome/components/rd03d/rd03d.h index 7413fe38f2..8bf7b423be 100644 --- a/esphome/components/rd03d/rd03d.h +++ b/esphome/components/rd03d/rd03d.h @@ -42,7 +42,6 @@ class RD03DComponent : public Component, public uart::UARTDevice { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_SENSOR void set_target_count_sensor(sensor::Sensor *sensor) { this->target_count_sensor_ = sensor; } diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index 51790069d2..810087ab7f 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -16,7 +16,6 @@ namespace resampler { class ResamplerSpeaker : public Component, public speaker::Speaker { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } void setup() override; void loop() override; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index c652944120..38fd14375d 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -235,7 +235,6 @@ void RotaryEncoderSensor::loop() { } } -float RotaryEncoderSensor::get_setup_priority() const { return setup_priority::DATA; } void RotaryEncoderSensor::set_restore_mode(RotaryEncoderRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 14442f0565..865554cd4d 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -82,8 +82,6 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { void dump_config() override; void loop() override; - float get_setup_priority() const override; - void add_on_clockwise_callback(std::function callback) { this->on_clockwise_callback_.add(std::move(callback)); } diff --git a/esphome/components/rx8130/rx8130.h b/esphome/components/rx8130/rx8130.h index 6694c763cd..979da3e19c 100644 --- a/esphome/components/rx8130/rx8130.h +++ b/esphome/components/rx8130/rx8130.h @@ -14,8 +14,6 @@ class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice { void dump_config() override; void read_time(); void write_time(); - /// Ensure RTC is initialized at the correct time in the setup sequence - float get_setup_priority() const override { return setup_priority::DATA; } protected: void stop_(bool stop); diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index d4ab04e7cd..6f6cc1ebd8 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -114,7 +114,5 @@ void SDP3XComponent::read_pressure_() { this->status_clear_warning(); } -float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace sdp3x } // namespace esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h index e3d3533c74..afb58d47c8 100644 --- a/esphome/components/sdp3x/sdp3x.h +++ b/esphome/components/sdp3x/sdp3x.h @@ -17,7 +17,6 @@ class SDP3XComponent : public PollingComponent, public sensirion_common::Sensiri void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } protected: diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 4e12c0e322..cdfd7544ad 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -108,8 +108,6 @@ void SDS011Component::loop() { } } -float SDS011Component::get_setup_priority() const { return setup_priority::DATA; } - void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } void SDS011Component::sds011_write_command_(const uint8_t *command_data) { diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h index 1f404601b1..3be74e66d1 100644 --- a/esphome/components/sds011/sds011.h +++ b/esphome/components/sds011/sds011.h @@ -21,8 +21,6 @@ class SDS011Component : public Component, public uart::UARTDevice { void dump_config() override; void loop() override; - float get_setup_priority() const override; - void set_update_interval(uint32_t val) { /* ignore */ } void set_update_interval_min(uint8_t update_interval_min); diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index d473df43c7..bd3dec6fb8 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -72,8 +72,6 @@ void SHT3XDComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; } - void SHT3XDComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 74f155121b..43f1a4d8e2 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -17,7 +17,6 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_heater_enabled(bool heater_enabled) { heater_enabled_ = heater_enabled; } diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index aca305b88d..ec12a5babd 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -63,8 +63,6 @@ void SHTCXComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float SHTCXComponent::get_setup_priority() const { return setup_priority::DATA; } - void SHTCXComponent::update() { if (this->status_has_warning()) { ESP_LOGW(TAG, "Retrying communication"); diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index 084d3bfc35..f9778dce8d 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -17,7 +17,6 @@ class SHTCXComponent : public PollingComponent, public sensirion_common::Sensiri void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void soft_reset(); void sleep(); diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index c8dfb4c7bd..1bcb964264 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -44,8 +44,6 @@ void SMT100Component::loop() { } } -float SMT100Component::get_setup_priority() const { return setup_priority::DATA; } - void SMT100Component::dump_config() { ESP_LOGCONFIG(TAG, "SMT100:"); diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index 86827607dc..df8803e1c6 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -17,8 +17,6 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { void loop() override; void update() override; - float get_setup_priority() const override; - void set_counts_sensor(sensor::Sensor *counts_sensor) { this->counts_sensor_ = counts_sensor; } void set_permittivity_sensor(sensor::Sensor *permittivity_sensor) { this->permittivity_sensor_ = permittivity_sensor; diff --git a/esphome/components/sonoff_d1/sonoff_d1.h b/esphome/components/sonoff_d1/sonoff_d1.h index 19ff83f378..20bea23287 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.h +++ b/esphome/components/sonoff_d1/sonoff_d1.h @@ -53,7 +53,6 @@ class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, publi void setup() override{}; void loop() override; void dump_config() override; - float get_setup_priority() const override { return esphome::setup_priority::DATA; } // Custom methods void set_use_rm433_remote(const bool use_rm433_remote) { this->use_rm433_remote_ = use_rm433_remote; } diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp index 4cc7286ba9..34f83027db 100644 --- a/esphome/components/spi_device/spi_device.cpp +++ b/esphome/components/spi_device/spi_device.cpp @@ -23,7 +23,5 @@ void SPIDeviceComponent::dump_config() { } } -float SPIDeviceComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace spi_device } // namespace esphome diff --git a/esphome/components/spi_device/spi_device.h b/esphome/components/spi_device/spi_device.h index d8aef440a7..e3aa74aaf0 100644 --- a/esphome/components/spi_device/spi_device.h +++ b/esphome/components/spi_device/spi_device.h @@ -13,8 +13,6 @@ class SPIDeviceComponent : public Component, void setup() override; void dump_config() override; - float get_setup_priority() const override; - protected: }; diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index eee2aca73e..8713b0b6b8 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -41,7 +41,7 @@ void STS3XComponent::dump_config() { LOG_SENSOR(" ", "STS3x", this); } -float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; } + void STS3XComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 8f806a3471..6c1dd2b244 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -14,7 +14,6 @@ class STS3XComponent : public sensor::Sensor, public PollingComponent, public se public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; }; diff --git a/esphome/components/sy6970/sy6970.h b/esphome/components/sy6970/sy6970.h index bacc072f9b..2225dd781b 100644 --- a/esphome/components/sy6970/sy6970.h +++ b/esphome/components/sy6970/sy6970.h @@ -87,7 +87,6 @@ class SY6970Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } // Listener registration void add_listener(SY6970Listener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index c2ac88ee2e..75f9ed108e 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -86,7 +86,6 @@ void T6615Component::query_ppm_() { this->send_ppm_command_(); } -float T6615Component::get_setup_priority() const { return setup_priority::DATA; } void T6615Component::dump_config() { ESP_LOGCONFIG(TAG, "T6615:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h index fb53032e8d..69c406a5ba 100644 --- a/esphome/components/t6615/t6615.h +++ b/esphome/components/t6615/t6615.h @@ -22,8 +22,6 @@ enum class T6615Command : uint8_t { class T6615Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void loop() override; void update() override; void dump_config() override; diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index abf3839e00..969ef3671e 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -61,7 +61,5 @@ void TC74Component::read_temperature_() { this->status_clear_warning(); } -float TC74Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace tc74 } // namespace esphome diff --git a/esphome/components/tc74/tc74.h b/esphome/components/tc74/tc74.h index 5d70c05420..f3ce225ff4 100644 --- a/esphome/components/tc74/tc74.h +++ b/esphome/components/tc74/tc74.h @@ -15,8 +15,6 @@ class TC74Component : public PollingComponent, public i2c::I2CDevice, public sen /// Update the sensor value (temperature). void update() override; - float get_setup_priority() const override; - protected: /// Internal method to read the temperature from the component after it has been scheduled. void read_temperature_(); diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index e4e5547595..4fe87de0ca 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -56,7 +56,6 @@ void TCS34725Component::dump_config() { LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_); } -float TCS34725Component::get_setup_priority() const { return setup_priority::DATA; } /*! * @brief Converts the raw R/G/B values to color temperature in degrees diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 23985e8221..85bb383e4b 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -52,7 +52,6 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { } void setup() override; - float get_setup_priority() const override; void update() override; void dump_config() override; diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 06481b628b..00a62247f9 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -43,7 +43,6 @@ void TEE501Component::dump_config() { LOG_SENSOR(" ", "TEE501", this); } -float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } void TEE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; this->write(address_1, 2); diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h index 2437ac92eb..62a6f1c944 100644 --- a/esphome/components/tee501/tee501.h +++ b/esphome/components/tee501/tee501.h @@ -12,7 +12,6 @@ class TEE501Component : public sensor::Sensor, public PollingComponent, public i public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/tem3200/tem3200.cpp b/esphome/components/tem3200/tem3200.cpp index b31496142c..9c305f8f6f 100644 --- a/esphome/components/tem3200/tem3200.cpp +++ b/esphome/components/tem3200/tem3200.cpp @@ -51,8 +51,6 @@ void TEM3200Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } -float TEM3200Component::get_setup_priority() const { return setup_priority::DATA; } - i2c::ErrorCode TEM3200Component::read_(uint8_t &status, uint16_t &raw_temperature, uint16_t &raw_pressure) { uint8_t response[4] = {0x00, 0x00, 0x00, 0x00}; diff --git a/esphome/components/tem3200/tem3200.h b/esphome/components/tem3200/tem3200.h index c84a2aba21..37589b2a06 100644 --- a/esphome/components/tem3200/tem3200.h +++ b/esphome/components/tem3200/tem3200.h @@ -15,7 +15,6 @@ class TEM3200Component : public PollingComponent, public i2c::I2CDevice { this->raw_pressure_sensor_ = raw_pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 0aef4b8e85..f6a3048bd4 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -51,7 +51,7 @@ void TimeBasedCover::loop() { this->last_publish_time_ = now; } } -float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; } + CoverTraits TimeBasedCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_stop(true); diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index d2457cae7a..0adc5cb370 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -12,7 +12,6 @@ class TimeBasedCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; Trigger<> *get_open_trigger() { return &this->open_trigger_; } Trigger<> *get_close_trigger() { return &this->close_trigger_; } diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index 7390d9fcc9..99f6753ddc 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -46,7 +46,5 @@ void TMP102Component::update() { }); } -float TMP102Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace tmp102 } // namespace esphome diff --git a/esphome/components/tmp102/tmp102.h b/esphome/components/tmp102/tmp102.h index 657b48c7cf..fe860a3819 100644 --- a/esphome/components/tmp102/tmp102.h +++ b/esphome/components/tmp102/tmp102.h @@ -11,8 +11,6 @@ class TMP102Component : public PollingComponent, public i2c::I2CDevice, public s public: void dump_config() override; void update() override; - - float get_setup_priority() const override; }; } // namespace tmp102 diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index c9eff41399..f8f52266e0 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -45,7 +45,7 @@ void TMP117Component::dump_config() { } LOG_SENSOR(" ", "Temperature", this); } -float TMP117Component::get_setup_priority() const { return setup_priority::DATA; } + bool TMP117Component::read_data_(int16_t *data) { if (!this->read_byte_16(0, (uint16_t *) data)) { ESP_LOGW(TAG, "Updating TMP117 failed!"); diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h index 162dbb64db..f501ee270c 100644 --- a/esphome/components/tmp117/tmp117.h +++ b/esphome/components/tmp117/tmp117.h @@ -11,7 +11,6 @@ class TMP117Component : public PollingComponent, public i2c::I2CDevice, public s public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_config(uint16_t config) { config_ = config; }; diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index 1442dd176c..cb4c38a83c 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -144,7 +144,7 @@ void TSL2561Sensor::set_integration_time(TSL2561IntegrationTime integration_time } void TSL2561Sensor::set_gain(TSL2561Gain gain) { this->gain_ = gain; } void TSL2561Sensor::set_is_cs_package(bool package_cs) { this->package_cs_ = package_cs; } -float TSL2561Sensor::get_setup_priority() const { return setup_priority::DATA; } + bool TSL2561Sensor::tsl2561_write_byte(uint8_t a_register, uint8_t value) { return this->write_byte(a_register | TSL2561_COMMAND_BIT, value); } diff --git a/esphome/components/tsl2561/tsl2561.h b/esphome/components/tsl2561/tsl2561.h index c54f41fb81..a8f0aef90f 100644 --- a/esphome/components/tsl2561/tsl2561.h +++ b/esphome/components/tsl2561/tsl2561.h @@ -67,7 +67,6 @@ class TSL2561Sensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; bool tsl2561_read_byte(uint8_t a_register, uint8_t *value); bool tsl2561_read_uint(uint8_t a_register, uint16_t *value); diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 999e42e949..42c524a074 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -247,8 +247,6 @@ void TSL2591Component::set_power_save_mode(bool enable) { this->power_save_mode_ void TSL2591Component::set_name(const char *name) { this->name_ = name; } -float TSL2591Component::get_setup_priority() const { return setup_priority::DATA; } - bool TSL2591Component::is_adc_valid() { uint8_t status; if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) { diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index fa302b14b0..84c92b6ba9 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -249,8 +249,6 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; /** Used by ESPHome framework. */ void update() override; - /** Used by ESPHome framework. */ - float get_setup_priority() const override; protected: const char *name_; diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index a6df61c053..6bc5f0bb51 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -38,8 +38,6 @@ void Tx20Component::loop() { } } -float Tx20Component::get_setup_priority() const { return setup_priority::DATA; } - std::string Tx20Component::get_wind_cardinal_direction() const { return this->wind_cardinal_direction_; } void Tx20Component::decode_and_publish_() { diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h index 95a9517227..d1673f99f2 100644 --- a/esphome/components/tx20/tx20.h +++ b/esphome/components/tx20/tx20.h @@ -35,7 +35,6 @@ class Tx20Component : public Component { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index a38737aff5..541f7d2b70 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -29,8 +29,6 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - /// Set the maximum time in µs to wait for the echo to return void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 584b8abfb2..2e5686008b 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -32,7 +32,6 @@ void VersionTextSensor::setup() { version_str[sizeof(version_str) - 1] = '\0'; this->publish_state(version_str); } -float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index 6813da7830..b7d8001120 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -11,7 +11,6 @@ class VersionTextSensor : public text_sensor::TextSensor, public Component { void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; - float get_setup_priority() const override; protected: bool hide_timestamp_{false}; diff --git a/esphome/components/wts01/wts01.h b/esphome/components/wts01/wts01.h index 298595a5d6..aae90c2c77 100644 --- a/esphome/components/wts01/wts01.h +++ b/esphome/components/wts01/wts01.h @@ -13,7 +13,6 @@ class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Compo public: void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: uint8_t buffer_[PACKET_SIZE]; From 4a579700a014f7beb2caebb93a21e64da23d5ff1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:52:14 +1100 Subject: [PATCH 086/251] [cover] Add operation-based triggers and fix repeated trigger firing (#13471) --- esphome/components/cover/__init__.py | 83 ++++++++++++++++------ esphome/components/cover/automation.h | 53 ++++++++------ esphome/components/cover/cover.cpp | 3 - esphome/components/cover/cover.h | 4 +- tests/components/template/common-base.yaml | 38 ++++++++++ 5 files changed, 133 insertions(+), 48 deletions(-) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 383daee083..41774f3d71 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg @@ -9,6 +11,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_MQTT_ID, + CONF_ON_IDLE, CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, @@ -32,9 +35,10 @@ from esphome.const import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass +from esphome.types import ConfigType, TemplateArgsType IS_PLATFORM_COMPONENT = True @@ -53,6 +57,8 @@ DEVICE_CLASSES = [ DEVICE_CLASS_WINDOW, ] +_LOGGER = logging.getLogger(__name__) + cover_ns = cg.esphome_ns.namespace("cover") Cover = cover_ns.class_("Cover", cg.EntityBase) @@ -83,14 +89,29 @@ ControlAction = cover_ns.class_("ControlAction", automation.Action) CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action) CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition) CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) - -# Triggers -CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template()) +CoverOpenedTrigger = cover_ns.class_( + "CoverOpenedTrigger", automation.Trigger.template() +) CoverClosedTrigger = cover_ns.class_( "CoverClosedTrigger", automation.Trigger.template() ) +CoverTrigger = cover_ns.class_("CoverTrigger", automation.Trigger.template()) +# Cover-specific constants CONF_ON_CLOSED = "on_closed" +CONF_ON_OPENED = "on_opened" +CONF_ON_OPENING = "on_opening" +CONF_ON_CLOSING = "on_closing" + +TRIGGERS = { + CONF_ON_OPEN: CoverOpenedTrigger, # Deprecated, use on_opened + CONF_ON_OPENED: CoverOpenedTrigger, + CONF_ON_CLOSED: CoverClosedTrigger, + CONF_ON_CLOSING: CoverTrigger.template(CoverOperation.COVER_OPERATION_CLOSING), + CONF_ON_OPENING: CoverTrigger.template(CoverOperation.COVER_OPERATION_OPENING), + CONF_ON_IDLE: CoverTrigger.template(CoverOperation.COVER_OPERATION_IDLE), +} + _COVER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) @@ -111,16 +132,14 @@ _COVER_SCHEMA = ( cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), - cv.Optional(CONF_ON_OPEN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), - } - ), - cv.Optional(CONF_ON_CLOSED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), - } - ), + **{ + cv.Optional(conf): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class), + } + ) + for conf, trigger_class in TRIGGERS.items() + }, } ) ) @@ -157,12 +176,14 @@ async def setup_cover_core_(var, config): if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) - for conf in config.get(CONF_ON_OPEN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_CLOSED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + if CONF_ON_OPEN in config: + _LOGGER.warning( + "'on_open' is deprecated, use 'on_opened'. Will be removed in 2026.8.0" + ) + for trigger_conf in TRIGGERS: + for conf in config.get(trigger_conf, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) @@ -258,6 +279,26 @@ async def cover_control_to_code(config, action_id, template_arg, args): return var +COVER_CONDITION_SCHEMA = cv.maybe_simple_value( + {cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID +) + + +async def cover_condition_to_code( + config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType +) -> MockObj: + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +automation.register_condition( + "cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) +automation.register_condition( + "cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index c0345a7cc6..12ec46725d 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -90,44 +90,53 @@ template class CoverPublishAction : public Action { Cover *cover_; }; -template class CoverIsOpenCondition : public Condition { +template class CoverPositionCondition : public Condition { public: - CoverIsOpenCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } + CoverPositionCondition(Cover *cover) : cover_(cover) {} + + bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); } protected: Cover *cover_; }; -template class CoverIsClosedCondition : public Condition { +template using CoverIsOpenCondition = CoverPositionCondition; +template using CoverIsClosedCondition = CoverPositionCondition; + +template class CoverPositionTrigger : public Trigger<> { public: - CoverIsClosedCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } + CoverPositionTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + if (a_cover->position != this->last_position_) { + this->last_position_ = a_cover->position; + if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + this->trigger(); + } + }); + } protected: - Cover *cover_; + float last_position_{NAN}; }; -class CoverOpenTrigger : public Trigger<> { +using CoverOpenedTrigger = CoverPositionTrigger; +using CoverClosedTrigger = CoverPositionTrigger; + +template class CoverTrigger : public Trigger<> { public: - CoverOpenTrigger(Cover *a_cover) { + CoverTrigger(Cover *a_cover) { a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_open()) { - this->trigger(); + auto current_op = a_cover->current_operation; + if (current_op == OP) { + if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { + this->trigger(); + } } + this->last_operation_ = current_op; }); } -}; -class CoverClosedTrigger : public Trigger<> { - public: - CoverClosedTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_closed()) { - this->trigger(); - } - }); - } + protected: + optional last_operation_{}; }; - } // namespace esphome::cover diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 0d9e7e8ffb..37cb908d9f 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -10,9 +10,6 @@ namespace esphome::cover { static const char *const TAG = "cover"; -const float COVER_OPEN = 1.0f; -const float COVER_CLOSED = 0.0f; - const LogString *cover_command_to_str(float pos) { if (pos == COVER_OPEN) { return LOG_STR("OPEN"); diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index e5427ceaa8..0af48f75de 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -10,8 +10,8 @@ namespace esphome::cover { -const extern float COVER_OPEN; -const extern float COVER_CLOSED; +static constexpr float COVER_OPEN = 1.0f; +static constexpr float COVER_CLOSED = 0.0f; #define LOG_COVER(prefix, type, obj) \ if ((obj) != nullptr) { \ diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 9dece7c3a5..d1849efaf7 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -245,6 +245,44 @@ cover: stop_action: - logger.log: stop_action optimistic: true + - platform: template + name: "Template Cover with Triggers" + id: template_cover_with_triggers + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + on_open: + - logger.log: "Cover on_open (deprecated)" + on_opened: + - logger.log: "Cover fully opened" + on_closed: + - logger.log: "Cover fully closed" + on_opening: + - logger.log: "Cover started opening" + on_closing: + - logger.log: "Cover started closing" + on_idle: + - logger.log: "Cover stopped moving" + - logger.log: "Cover stopped moving" + - if: + condition: + cover.is_open: template_cover_with_triggers + then: + logger.log: Cover is open + - if: + condition: + cover.is_closed: template_cover_with_triggers + then: + logger.log: Cover is closed number: - platform: template From 43d9d6fe641e2d9651f25429b1a147b2c811fe20 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:12:42 -0500 Subject: [PATCH 087/251] [esp32] Restore develop branch for dev platform version, bump platformio (#13759) Co-authored-by: Claude --- esphome/components/esp32/__init__.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4c53b42e6f..77753e277f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -517,7 +517,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = { "recommended": cv.Version(55, 3, 36), "latest": cv.Version(55, 3, 36), - "dev": cv.Version(55, 3, 36), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/requirements.txt b/requirements.txt index 821324262b..bb118c5f9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tornado==6.5.4 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile +platformio==6.1.19 esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 From 13ddf267bbb6e15e1d30c43f9a4511c964972d9f Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 4 Feb 2026 21:18:24 +0100 Subject: [PATCH 088/251] [nrf52,zigbee] update warnings (#13761) --- esphome/components/zigbee/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index c044148b32..7e917a9d70 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -84,8 +84,8 @@ def validate_number_of_ep(config: ConfigType) -> None: raise cv.Invalid("At least one zigbee device need to be included") count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if count == 1: - raise cv.Invalid( - "Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888" + _LOGGER.warning( + "Single endpoint requires ZHA or at leatst Zigbee2MQTT 2.8.0. For older versions of Zigbee2MQTT use multiple endpoints" ) if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode: raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}") @@ -151,7 +151,7 @@ def consume_endpoint(config: ConfigType) -> ConfigType: return config if CONF_NAME in config and " " in config[CONF_NAME]: _LOGGER.warning( - "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", + "Spaces in '%s' requires ZHA or at least Zigbee2MQTT 2.8.0. For older version of Zigbee2MQTT use '%s'", config[CONF_NAME], config[CONF_NAME].replace(" ", "_"), ) From 67dfa5e2bc837f98d8f58d613073881d96c0d7fd Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:39:03 +0100 Subject: [PATCH 089/251] [epaper_spi] Validate BUSY pin as input instead of output (#13764) --- esphome/components/epaper_spi/display.py | 78 +++++++++---------- tests/component_tests/epaper_spi/test_init.py | 53 +++++++++++++ 2 files changed, 88 insertions(+), 43 deletions(-) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 8cc7b2663c..13f66691b2 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -76,50 +76,42 @@ def model_schema(config): model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s") ) cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required - return ( - display.FULL_DISPLAY_SCHEMA.extend( - spi.spi_device_schema( - cs_pin_required=False, - default_mode="MODE0", - default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), - ) - ) - .extend( - { - model.option(pin): pins.gpio_output_pin_schema - for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) - } - ) - .extend( - { - cv.Optional(CONF_ROTATION, default=0): validate_rotation, - cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), - cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( - update_interval, cv.Range(min=minimum_update_interval) - ), - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - } - ), - cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), - model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, - cv.GenerateID(): cv.declare_id(class_name), - cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), - cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, - model.option(CONF_ENABLE_PIN): cv.ensure_list( - pins.gpio_output_pin_schema - ), - model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( - map_sequence - ), - model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(max=core.TimePeriod(milliseconds=500)), - ), - } + return display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), ) + ).extend( + { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( + update_interval, cv.Range(min=minimum_update_interval) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), + model.option(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + model.option(CONF_CS_PIN): pins.gpio_output_pin_schema, + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + model.option(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } ) diff --git a/tests/component_tests/epaper_spi/test_init.py b/tests/component_tests/epaper_spi/test_init.py index 71e66cd043..a9f5735fca 100644 --- a/tests/component_tests/epaper_spi/test_init.py +++ b/tests/component_tests/epaper_spi/test_init.py @@ -289,3 +289,56 @@ def test_model_with_full_update_every( "full_update_every": 10, } ) + + +def test_busy_pin_input_mode_ssd1677( + set_core_config: SetCoreConfigCallable, + set_component_config: Callable[[str, Any], None], +) -> None: + """Test that busy_pin has input mode and cs/dc/reset pins have output mode for ssd1677.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + # Configure SPI component which is required by epaper_spi + set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) + + result = run_schema_validation( + { + "id": "test_display", + "model": "ssd1677", + "dc_pin": 21, + "busy_pin": 22, + "reset_pin": 23, + "cs_pin": 5, + "dimensions": { + "width": 200, + "height": 200, + }, + } + ) + + # Verify that busy_pin has input mode set + assert CONF_BUSY_PIN in result + busy_pin_config = result[CONF_BUSY_PIN] + assert "mode" in busy_pin_config + assert busy_pin_config["mode"]["input"] is True + + # Verify that cs_pin has output mode set + assert CONF_CS_PIN in result + cs_pin_config = result[CONF_CS_PIN] + assert "mode" in cs_pin_config + assert cs_pin_config["mode"]["output"] is True + + # Verify that dc_pin has output mode set + assert CONF_DC_PIN in result + dc_pin_config = result[CONF_DC_PIN] + assert "mode" in dc_pin_config + assert dc_pin_config["mode"]["output"] is True + + # Verify that reset_pin has output mode set + assert CONF_RESET_PIN in result + reset_pin_config = result[CONF_RESET_PIN] + assert "mode" in reset_pin_config + assert reset_pin_config["mode"]["output"] is True From 89fc5ebc978325698052579a49c5af780f758b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:18:03 +0100 Subject: [PATCH 090/251] Fix bare hostname ping fallback in dashboard (#13760) --- esphome/dashboard/dns.py | 21 +++++++- tests/dashboard/status/test_dns.py | 82 +++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py index 58867f7bc1..eb4a87dbfb 100644 --- a/esphome/dashboard/dns.py +++ b/esphome/dashboard/dns.py @@ -3,11 +3,16 @@ from __future__ import annotations import asyncio from contextlib import suppress from ipaddress import ip_address +import logging from icmplib import NameLookupError, async_resolve RESOLVE_TIMEOUT = 3.0 +_LOGGER = logging.getLogger(__name__) + +_RESOLVE_EXCEPTIONS = (TimeoutError, NameLookupError, UnicodeError) + async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: """Wrap the icmplib async_resolve function.""" @@ -16,7 +21,21 @@ async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: try: async with asyncio.timeout(RESOLVE_TIMEOUT): return await async_resolve(hostname) - except (TimeoutError, NameLookupError, UnicodeError) as ex: + except _RESOLVE_EXCEPTIONS as ex: + # If the hostname ends with .local and resolution failed, + # try the bare hostname as a fallback since mDNS may not be + # working on the system but unicast DNS might resolve it + if hostname.endswith(".local"): + bare_hostname = hostname[:-6] # Remove ".local" + try: + async with asyncio.timeout(RESOLVE_TIMEOUT): + result = await async_resolve(bare_hostname) + _LOGGER.debug( + "Bare hostname %s resolved to %s", bare_hostname, result + ) + return result + except _RESOLVE_EXCEPTIONS: + _LOGGER.debug("Bare hostname %s also failed to resolve", bare_hostname) return ex diff --git a/tests/dashboard/status/test_dns.py b/tests/dashboard/status/test_dns.py index 9ca48ba2d8..f7c4992079 100644 --- a/tests/dashboard/status/test_dns.py +++ b/tests/dashboard/status/test_dns.py @@ -3,11 +3,12 @@ from __future__ import annotations import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch +from icmplib import NameLookupError import pytest -from esphome.dashboard.dns import DNSCache +from esphome.dashboard.dns import DNSCache, _async_resolve_wrapper @pytest.fixture @@ -119,3 +120,80 @@ def test_async_resolve_not_called(dns_cache_fixture: DNSCache) -> None: result = dns_cache_fixture.get_cached_addresses("valid.com", now) assert result == ["192.168.1.10"] mock_resolve.assert_not_called() + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_ip_address() -> None: + """Test _async_resolve_wrapper returns IP address directly.""" + result = await _async_resolve_wrapper("192.168.1.10") + assert result == ["192.168.1.10"] + + result = await _async_resolve_wrapper("2001:db8::1") + assert result == ["2001:db8::1"] + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_fallback_success() -> None: + """Test _async_resolve_wrapper falls back to bare hostname for .local.""" + mock_resolve = AsyncMock() + # First call (device.local) fails, second call (device) succeeds + mock_resolve.side_effect = [ + NameLookupError("device.local"), + ["192.168.1.50"], + ] + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + assert result == ["192.168.1.50"] + assert mock_resolve.call_count == 2 + mock_resolve.assert_any_call("device.local") + mock_resolve.assert_any_call("device") + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_fallback_both_fail() -> None: + """Test _async_resolve_wrapper returns exception when both fail.""" + mock_resolve = AsyncMock() + original_exception = NameLookupError("device.local") + mock_resolve.side_effect = [ + original_exception, + NameLookupError("device"), + ] + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + # Should return the original exception, not the fallback exception + assert result is original_exception + assert mock_resolve.call_count == 2 + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_non_local_no_fallback() -> None: + """Test _async_resolve_wrapper doesn't fallback for non-.local hostnames.""" + mock_resolve = AsyncMock() + original_exception = NameLookupError("device.example.com") + mock_resolve.side_effect = original_exception + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.example.com") + + assert result is original_exception + # Should only try the original hostname, no fallback + assert mock_resolve.call_count == 1 + mock_resolve.assert_called_once_with("device.example.com") + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_success_no_fallback() -> None: + """Test _async_resolve_wrapper doesn't fallback when .local succeeds.""" + mock_resolve = AsyncMock(return_value=["192.168.1.50"]) + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + assert result == ["192.168.1.50"] + # Should only try once since it succeeded + assert mock_resolve.call_count == 1 + mock_resolve.assert_called_once_with("device.local") From a5568248753345080b8e5524f7f886ddbaf9d8dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:19:13 +0100 Subject: [PATCH 091/251] [logger] Refactor to reduce code duplication and flash size (#13750) --- esphome/components/logger/logger.cpp | 55 +-- esphome/components/logger/logger.h | 407 +++++++++--------- esphome/components/logger/logger_esp32.cpp | 4 +- esphome/components/logger/logger_esp8266.cpp | 2 +- esphome/components/logger/logger_host.cpp | 4 +- .../components/logger/logger_libretiny.cpp | 2 +- esphome/components/logger/logger_rp2040.cpp | 2 +- esphome/components/logger/logger_zephyr.cpp | 4 +- 8 files changed, 234 insertions(+), 246 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 25243ff3f6..54b5670016 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -36,9 +36,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Fast path: main thread, no recursion (99.9% of all logs) if (is_main_task && !this->main_task_recursion_guard_) [[likely]] { - RecursionGuard guard(this->main_task_recursion_guard_); - // Format and send to both console and callbacks - this->log_message_to_buffer_and_send_(level, tag, line, format, args); + this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args); return; } @@ -101,12 +99,9 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; #endif char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety - uint16_t buffer_at = 0; // Initialize buffer position - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, - MAX_CONSOLE_LOG_MSG_SIZE); - // Add newline before writing to console - this->add_newline_to_buffer_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer, buffer_at); + LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + this->write_to_console_(buf); } // RAII guard automatically resets on return @@ -117,9 +112,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch if (level > this->level_for(tag) || global_recursion_guard_) return; - RecursionGuard guard(global_recursion_guard_); - // Format and send to both console and callbacks - this->log_message_to_buffer_and_send_(level, tag, line, format, args); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY @@ -135,28 +128,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas if (level > this->level_for(tag) || global_recursion_guard_) return; - RecursionGuard guard(global_recursion_guard_); - this->tx_buffer_at_ = 0; - - // Write header, format body directly from flash, and write footer - this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_, - reinterpret_cast(format), args); - this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - - // Ensure null termination - uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_; - this->tx_buffer_[null_pos] = '\0'; - - // Listeners get message first (before console write) -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); -#endif - - // Write to console - this->write_tx_buffer_to_console_(); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_STORE_LOG_STR_IN_FLASH @@ -215,10 +187,11 @@ void Logger::process_messages_() { logger::TaskLogBufferHost::LogMessage *message; while (this->log_buffer_->get_message_main_loop(&message)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text, - message->text_length); + message->text_length, buf); this->log_buffer_->release_message_main_loop(); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #elif defined(USE_ESP32) logger::TaskLogBuffer::LogMessage *message; @@ -226,22 +199,24 @@ void Logger::process_messages_() { void *received_token; while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length); + message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(received_token); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #elif defined(USE_LIBRETINY) logger::TaskLogBufferLibreTiny::LogMessage *message; const char *text; while (this->log_buffer_->borrow_message_main_loop(&message, &text)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length); + message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #endif } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 40ac9a38aa..1678fed5f5 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,6 +2,7 @@ #include #include +#include #if defined(USE_ESP32) || defined(USE_HOST) #include #endif @@ -123,6 +124,163 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Buffer wrapper for log formatting functions +struct LogBuffer { + char *data; + uint16_t size; + uint16_t pos{0}; + // Replaces the null terminator with a newline for console output. + // Must be called after notify_listeners_() since listeners need null-terminated strings. + // Console output uses length-based writes (buf.pos), so null terminator is not needed. + void terminate_with_newline() { + if (this->pos < this->size) { + this->data[this->pos++] = '\n'; + } else if (this->size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + this->data[this->size - 1] = '\n'; + this->pos = this->size; + } + } + void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) { + // Early return if insufficient space - intentionally don't update pos to prevent partial writes + if (this->pos + MAX_HEADER_SIZE > this->size) + return; + + char *p = this->current_(); + + // Write ANSI color + this->write_ansi_color_(p, level); + + // Construct: [LEVEL][tag:line] + *p++ = '['; + if (level != 0) { + if (level >= 7) { + *p++ = 'V'; // VERY_VERBOSE = "VV" + *p++ = 'V'; + } else { + *p++ = LOG_LEVEL_LETTER_CHARS[level]; + } + } + *p++ = ']'; + *p++ = '['; + + // Copy tag + this->copy_string_(p, tag); + + *p++ = ':'; + + // Format line number without modulo operations + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + *p++ = '0' + thousands; + line -= thousands * 1000; + } + int hundreds = line / 100; + int remainder = line - hundreds * 100; + int tens = remainder / 10; + *p++ = '0' + hundreds; + *p++ = '0' + tens; + *p++ = '0' + (remainder - tens * 10); + *p++ = ']'; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) + // Write thread name with bold red color + if (thread_name != nullptr) { + this->write_ansi_color_(p, 1); // Bold red for thread name + *p++ = '['; + this->copy_string_(p, thread_name); + *p++ = ']'; + this->write_ansi_color_(p, level); // Restore original color + } +#endif + + *p++ = ':'; + *p++ = ' '; + + this->pos = p - this->data; + } + void HOT format_body(const char *format, va_list args) { + this->format_vsnprintf_(format, args); + this->finalize_(); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void HOT format_body_P(PGM_P format, va_list args) { + this->format_vsnprintf_P_(format, args); + this->finalize_(); + } +#endif + void write_body(const char *text, uint16_t text_length) { + this->write_(text, text_length); + this->finalize_(); + } + + private: + bool full_() const { return this->pos >= this->size; } + uint16_t remaining_() const { return this->size - this->pos; } + char *current_() { return this->data + this->pos; } + void write_(const char *value, uint16_t length) { + const uint16_t available = this->remaining_(); + const uint16_t copy_len = (length < available) ? length : available; + if (copy_len > 0) { + memcpy(this->current_(), value, copy_len); + this->pos += copy_len; + } + } + void finalize_() { + // Write color reset sequence + static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; + this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); + // Null terminate + this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; + } + void strip_trailing_newlines_() { + while (this->pos > 0 && this->data[this->pos - 1] == '\n') + this->pos--; + } + void process_vsnprintf_result_(int ret) { + if (ret < 0) + return; + const uint16_t rem = this->remaining_(); + this->pos += (ret >= rem) ? (rem - 1) : static_cast(ret); + this->strip_trailing_newlines_(); + } + void format_vsnprintf_(const char *format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args)); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void format_vsnprintf_P_(PGM_P format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); + } +#endif + // Write ANSI color escape sequence to buffer, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void write_ansi_color_(char *&p, uint8_t level) { + if (level == 0) + return; + // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes) + *p++ = '\033'; + *p++ = '['; + *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold + *p++ = ';'; + *p++ = '3'; + *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = 'm'; + } + // Copy string without null terminator, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void copy_string_(char *&p, const char *str) { + const size_t len = strlen(str); + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by + // piece + memcpy(p, str, len); + p += len; + } +}; + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -260,114 +418,83 @@ class Logger : public Component { #endif #endif void process_messages_(); - void write_msg_(const char *msg, size_t len); + void write_msg_(const char *msg, uint16_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator - // It's the caller's responsibility to initialize buffer_at (typically to 0) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, - va_list args, char *buffer, uint16_t *buffer_at, - uint16_t buffer_size) { + va_list args, LogBuffer &buf) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) - this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); + buf.write_header(level, tag, line, this->get_thread_name_()); #elif defined(USE_ZEPHYR) - char buff[MAX_POINTER_REPRESENTATION]; - this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size); + char tmp[MAX_POINTER_REPRESENTATION]; + buf.write_header(level, tag, line, this->get_thread_name_(tmp)); #else - this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); + buf.write_header(level, tag, line, nullptr); #endif - this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, args); - this->write_footer_to_buffer_(buffer, buffer_at, buffer_size); - - // Always ensure the buffer has a null terminator, even if we need to - // overwrite the last character of the actual content - if (*buffer_at >= buffer_size) { - buffer[buffer_size - 1] = '\0'; // Truncate and ensure null termination - } else { - buffer[*buffer_at] = '\0'; // Normal case, append null terminator - } + buf.format_body(format, args); } - // Helper to add newline to buffer before writing to console - // Modifies buffer_at to include the newline - inline void HOT add_newline_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - // Add newline - don't need to maintain null termination - // write_msg_ receives explicit length, so we can safely overwrite the null terminator - // This is safe because: - // 1. Callbacks already received the message (before we add newline) - // 2. write_msg_ receives the length explicitly (doesn't need null terminator) - if (*buffer_at < buffer_size) { - buffer[(*buffer_at)++] = '\n'; - } else if (buffer_size > 0) { - // Buffer was full - replace last char with newline to ensure it's visible - buffer[buffer_size - 1] = '\n'; - *buffer_at = buffer_size; - } +#ifdef USE_STORE_LOG_STR_IN_FLASH + // Format a log message with flash string format and write it to a buffer with header, footer, and null terminator + inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line, + const __FlashStringHelper *format, va_list args, + LogBuffer &buf) { + buf.write_header(level, tag, line, nullptr); + buf.format_body_P(reinterpret_cast(format), args); + } +#endif + + // Helper to notify log listeners + inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { +#ifdef USE_LOG_LISTENERS + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, buf.data, buf.pos); +#endif } - // Helper to write tx_buffer_ to console if logging is enabled - // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null - inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { - if (this->baud_rate_ > 0) { - uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; - this->add_newline_to_buffer_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); - this->write_msg_(this->tx_buffer_ + offset, *len_ptr); - } + // Helper to write log buffer to console (replaces null terminator with newline and writes) + inline void HOT write_to_console_(LogBuffer &buf) { + buf.terminate_with_newline(); + this->write_msg_(buf.data, buf.pos); + } + + // Helper to write log buffer to console if logging is enabled + inline void HOT write_log_buffer_to_console_(LogBuffer &buf) { + if (this->baud_rate_ > 0) + this->write_to_console_(buf); } // Helper to format and send a log message to both console and listeners - inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, - va_list args) { - // Format to tx_buffer and prepare for output - this->tx_buffer_at_ = 0; // Initialize buffer position - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - - // Listeners get message WITHOUT newline (for API/MQTT/syslog) -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); + // Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings + template + inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, + FormatType format, va_list args) { + RecursionGuard guard(recursion_guard); + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; +#ifdef USE_STORE_LOG_STR_IN_FLASH + if constexpr (std::is_same_v) { + this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf); + } else #endif - - // Console gets message WITH newline (if platform needs it) - this->write_tx_buffer_to_console_(); + { + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + } + this->notify_listeners_(level, tag, buf); + this->write_log_buffer_to_console_(buf); } #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Helper to format a pre-formatted message from the task log buffer and notify listeners // Used by process_messages_ to avoid code duplication between ESP32 and host platforms inline void HOT format_buffered_message_and_notify_(uint8_t level, const char *tag, uint16_t line, - const char *thread_name, const char *text, size_t text_length) { - this->tx_buffer_at_ = 0; - this->write_header_to_buffer_(level, tag, line, thread_name, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - this->write_body_to_buffer_(text, text_length, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->tx_buffer_[this->tx_buffer_at_] = '\0'; -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); -#endif + const char *thread_name, const char *text, uint16_t text_length, + LogBuffer &buf) { + buf.write_header(level, tag, line, thread_name); + buf.write_body(text, text_length); + this->notify_listeners_(level, tag, buf); } #endif - // Write the body of the log message to the buffer - inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, - uint16_t buffer_size) { - // Calculate available space - if (*buffer_at >= buffer_size) - return; - const uint16_t available = buffer_size - *buffer_at; - - // Determine copy length (minimum of remaining capacity and string length) - const size_t copy_len = (length < static_cast(available)) ? length : available; - - // Copy the data - if (copy_len > 0) { - memcpy(buffer + *buffer_at, value, copy_len); - *buffer_at += copy_len; - } - } - #ifndef USE_HOST const LogString *get_uart_selection_(); #endif @@ -421,7 +548,6 @@ class Logger : public Component { #endif // Group smaller types together at the end - uint16_t tx_buffer_at_{0}; uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) @@ -525,117 +651,6 @@ class Logger : public Component { } #endif - static inline void copy_string(char *buffer, uint16_t &pos, const char *str) { - const size_t len = strlen(str); - // Intentionally no null terminator, building larger string - memcpy(buffer + pos, str, len); // NOLINT(bugprone-not-null-terminated-result) - pos += len; - } - - static inline void write_ansi_color_for_level(char *buffer, uint16_t &pos, uint8_t level) { - if (level == 0) - return; - // Construct ANSI escape sequence: "\033[{bold};3{color}m" - // Example: "\033[1;31m" for ERROR (bold red) - buffer[pos++] = '\033'; - buffer[pos++] = '['; - buffer[pos++] = (level == 1) ? '1' : '0'; // Only ERROR is bold - buffer[pos++] = ';'; - buffer[pos++] = '3'; - buffer[pos++] = LOG_LEVEL_COLOR_DIGIT[level]; - buffer[pos++] = 'm'; - } - - inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, - char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - uint16_t pos = *buffer_at; - // Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes - if (pos + MAX_HEADER_SIZE > buffer_size) - return; - - // Construct: [LEVEL][tag:line]: - write_ansi_color_for_level(buffer, pos, level); - buffer[pos++] = '['; - if (level != 0) { - if (level >= 7) { - buffer[pos++] = 'V'; // VERY_VERBOSE = "VV" - buffer[pos++] = 'V'; - } else { - buffer[pos++] = LOG_LEVEL_LETTER_CHARS[level]; - } - } - buffer[pos++] = ']'; - buffer[pos++] = '['; - copy_string(buffer, pos, tag); - buffer[pos++] = ':'; - // Format line number without modulo operations (passed by value, safe to mutate) - if (line > 999) [[unlikely]] { - int thousands = line / 1000; - buffer[pos++] = '0' + thousands; - line -= thousands * 1000; - } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - buffer[pos++] = '0' + hundreds; - buffer[pos++] = '0' + tens; - buffer[pos++] = '0' + (remainder - tens * 10); - buffer[pos++] = ']'; - -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) - if (thread_name != nullptr) { - write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name - buffer[pos++] = '['; - copy_string(buffer, pos, thread_name); - buffer[pos++] = ']'; - write_ansi_color_for_level(buffer, pos, level); // Restore original color - } -#endif - - buffer[pos++] = ':'; - buffer[pos++] = ' '; - *buffer_at = pos; - } - - // Helper to process vsnprintf return value and strip trailing newlines. - // Updates buffer_at with the formatted length, handling truncation: - // - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator - // - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator - __attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at, - uint16_t remaining, int ret) { - if (ret < 0) - return; // Encoding error, do not increment buffer_at - *buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast(ret); - // Remove all trailing newlines right after formatting - while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') - (*buffer_at)--; - } - - inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, - va_list args) { - // Check remaining capacity in the buffer - if (*buffer_at >= buffer_size) - return; - const uint16_t remaining = buffer_size - *buffer_at; - process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args)); - } - -#ifdef USE_STORE_LOG_STR_IN_FLASH - // ESP8266 variant that reads format string directly from flash using vsnprintf_P - inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format, - va_list args) { - if (*buffer_at >= buffer_size) - return; - const uint16_t remaining = buffer_size - *buffer_at; - process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args)); - } -#endif - - inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; - this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); - } - #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 9defb6c166..dfa643d5e9 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -118,9 +118,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { - // Length is now always passed explicitly - no strlen() fallback needed - +void HOT Logger::write_msg_(const char *msg, uint16_t len) { #if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) // USB CDC/JTAG - single write including newline (already in buffer) // Use fwrite to stdout which goes through VFS to USB console diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 6cee1baca5..0a3433d132 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -28,7 +28,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) this->hw_serial_->write(msg, len); } diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 874cdabd22..be12b6df6a 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { static constexpr size_t TIMESTAMP_LEN = 10; // "[HH:MM:SS]" // tx_buffer_size_ defaults to 512, so 768 covers default + headroom char buffer[TIMESTAMP_LEN + 768]; @@ -15,7 +15,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { size_t pos = strftime(buffer, TIMESTAMP_LEN + 1, "[%H:%M:%S]", &timeinfo); // Copy message (with newline already included by caller) - size_t copy_len = std::min(len, sizeof(buffer) - pos); + size_t copy_len = std::min(static_cast(len), sizeof(buffer) - pos); memcpy(buffer + pos, msg, copy_len); pos += copy_len; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index cdf55e710c..aab8a97abf 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } +void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index be8252f56a..1f435031f6 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) this->hw_serial_->write(msg, len); } diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 41f53beec0..ef1702c5c1 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -63,7 +63,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) #ifdef CONFIG_PRINTK // Requires the debug component and an active SWD connection. @@ -73,7 +73,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { if (this->uart_dev_ == nullptr) { return; } - for (size_t i = 0; i < len; ++i) { + for (uint16_t i = 0; i < len; ++i) { uart_poll_out(this->uart_dev_, msg[i]); } } From 25c0073b2d60e99012264a2149696cd50970af44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:20:04 +0100 Subject: [PATCH 092/251] [web_server] Fix ESP8266 watchdog panic by deferring actions to main loop (#13765) --- esphome/components/web_server/web_server.cpp | 15 +-------------- esphome/components/web_server/web_server.h | 11 +++++------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index d30cb524f4..a3bf1e6b02 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -721,11 +721,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } if (action != SWITCH_ACTION_NONE) { -#ifdef USE_ESP8266 - execute_switch_action(obj, action); -#else this->defer([obj, action]() { execute_switch_action(obj, action); }); -#endif request->send(200); } else { request->send(404); @@ -1645,11 +1641,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } if (action != LOCK_ACTION_NONE) { -#ifdef USE_ESP8266 - execute_lock_action(obj, action); -#else this->defer([obj, action]() { execute_lock_action(obj, action); }); -#endif request->send(200); } else { request->send(404); @@ -2015,19 +2007,14 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur return; } -#ifdef USE_ESP8266 - // ESP8266 is single-threaded, call directly - call.set_raw_timings_base64url(encoded); - call.perform(); -#else // Defer to main loop for thread safety. Move encoded string into lambda to ensure // it outlives the call - set_raw_timings_base64url stores a pointer, so the string // must remain valid until perform() completes. + // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. this->defer([call, encoded = std::move(encoded)]() mutable { call.set_raw_timings_base64url(encoded); call.perform(); }); -#endif request->send(200); return; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 92a5c7edee..224c051ece 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -42,13 +42,12 @@ using ParamNameType = const __FlashStringHelper *; using ParamNameType = const char *; #endif -// ESP8266 is single-threaded, so actions can execute directly in request context. -// Multi-core platforms need to defer to main loop thread for thread safety. -#ifdef USE_ESP8266 -#define DEFER_ACTION(capture, action) action -#else +// All platforms need to defer actions to main loop thread. +// Multi-core platforms need this for thread safety. +// ESP8266 needs this because ESPAsyncWebServer callbacks run in "sys" context +// (SDK system context), not "cont" context (continuation/main loop). Calling +// yield() from sys context causes a panic in the Arduino core. #define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; }) -#endif /// Result of matching a URL against an entity struct EntityMatchResult { From c27870b15d076e61632efc1637629104817d1064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Feb 2026 06:36:40 +0100 Subject: [PATCH 093/251] [web_server] Add some more missing ESPHOME_F macros (#13748) --- esphome/components/web_server/web_server.cpp | 10 +++++----- esphome/components/web_server/web_server_v1.cpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a3bf1e6b02..42219f3aac 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -454,7 +454,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse(200, ""); + AsyncWebServerResponse *response = request->beginResponse(200, ESPHOME_F("")); response->addHeader(ESPHOME_F("Access-Control-Allow-Private-Network"), ESPHOME_F("true")); response->addHeader(ESPHOME_F("Private-Network-Access-Name"), App.get_name().c_str()); char mac_s[18]; @@ -1967,7 +1967,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Only allow transmit if the device supports it if (!obj->has_transmitter()) { - request->send(400, ESPHOME_F("text/plain"), "Device does not support transmission"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Device does not support transmission")); return; } @@ -1993,7 +1993,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) if (!request->hasParam(ESPHOME_F("data"))) { - request->send(400, ESPHOME_F("text/plain"), "Missing 'data' parameter"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); return; } @@ -2003,7 +2003,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Validate base64url is not empty if (encoded.empty()) { - request->send(400, ESPHOME_F("text/plain"), "Empty 'data' parameter"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter")); return; } @@ -2472,7 +2472,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { else { // No matching handler found - send 404 ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); - request->send(404, "text/plain", "Not Found"); + request->send(404, ESPHOME_F("text/plain"), ESPHOME_F("Not Found")); } } diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index ae4bbfa557..f7b90018dc 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -74,7 +74,7 @@ void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("text/html"); + AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); const std::string &title = App.get_name(); stream->print(ESPHOME_F("")); From 7bd8b08e16493259df06d0c3d5e4ac035bb44e25 Mon Sep 17 00:00:00 2001 From: Jas Strong <jasmine@electronpusher.org> Date: Thu, 5 Feb 2026 00:06:52 -0800 Subject: [PATCH 094/251] [rd03d] Revert incorrect field order swap (#13769) Co-authored-by: jas <jas@asspa.in> --- esphome/components/rd03d/rd03d.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index ba05abe8e0..090e4dcf32 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -132,18 +132,15 @@ void RD03DComponent::process_frame_() { // Header is 4 bytes, each target is 8 bytes uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); - // Extract raw bytes for this target - // Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution, - // actual radar output has Resolution before Speed (verified empirically - - // stationary targets were showing non-zero speed with original field order) + // Extract raw bytes for this target (per datasheet Table 5-2: X, Y, Speed, Resolution) uint8_t x_low = this->buffer_[offset + 0]; uint8_t x_high = this->buffer_[offset + 1]; uint8_t y_low = this->buffer_[offset + 2]; uint8_t y_high = this->buffer_[offset + 3]; - uint8_t res_low = this->buffer_[offset + 4]; - uint8_t res_high = this->buffer_[offset + 5]; - uint8_t speed_low = this->buffer_[offset + 6]; - uint8_t speed_high = this->buffer_[offset + 7]; + uint8_t speed_low = this->buffer_[offset + 4]; + uint8_t speed_high = this->buffer_[offset + 5]; + uint8_t res_low = this->buffer_[offset + 6]; + uint8_t res_high = this->buffer_[offset + 7]; // Decode values per RD-03D format int16_t x = decode_value(x_low, x_high); From be44d4801f578aeb4d6561f3a075872f4241724a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 5 Feb 2026 10:52:43 +0100 Subject: [PATCH 095/251] [esp32] Reduce Arduino build size by 44% and build time by 36% (#13623) --- esphome/components/bme680_bsec/__init__.py | 3 +- esphome/components/esp32/__init__.py | 266 +++++++++++++++++- esphome/components/esp32/const.py | 1 + esphome/components/espnow/__init__.py | 5 +- esphome/components/ethernet/__init__.py | 3 - .../i2s_audio/media_player/__init__.py | 1 + esphome/components/midea/climate.py | 5 +- esphome/components/network/__init__.py | 3 +- .../components/web_server_base/__init__.py | 5 +- esphome/components/wled/__init__.py | 3 + esphome/core/__init__.py | 10 + esphome/writer.py | 15 +- .../components/i2s_audio/test.esp32-ard.yaml | 16 ++ tests/unit_tests/test_core.py | 75 +++++ 14 files changed, 384 insertions(+), 27 deletions(-) create mode 100644 tests/components/i2s_audio/test.esp32-ard.yaml diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 06e641d34d..a86e061cd4 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -89,8 +89,9 @@ async def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) - # Although this component does not use SPI, the BSEC library requires the SPI library + # Although this component does not use SPI/Wire directly, the BSEC library requires them cg.add_library("SPI", None) + cg.add_library("Wire", None) cg.add_define("USE_BSEC") cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 77753e277f..e1e3164b68 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -46,10 +46,11 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache +from esphome.writer import clean_cmake_cache, rmtree from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa + KEY_ARDUINO_LIBRARIES, KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, @@ -152,6 +153,168 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation ) +# Additional IDF managed components to exclude for Arduino framework builds +# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome +# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because +# that's how managed components are registered in the IDF build system +# List includes direct dependencies from arduino-esp32/idf_component.yml +# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need) +ARDUINO_EXCLUDED_IDF_COMPONENTS = ( + "chmorgan__esp-libhelix-mp3", # MP3 decoder - not used + "espressif__cbor", # CBOR library - only used by RainMaker/Insights + "espressif__esp-dsp", # DSP library - not used + "espressif__esp-modbus", # Modbus - ESPHome has its own + "espressif__esp-sr", # Speech recognition - not used + "espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used + "espressif__esp-zigbee-lib", # Zigbee library - not used + "espressif__esp_diag_data_store", # Diagnostics - not used + "espressif__esp_diagnostics", # Diagnostics - not used + "espressif__esp_hosted", # ESP hosted - only for ESP32-P4 + "espressif__esp_insights", # ESP Insights - not used + "espressif__esp_modem", # Modem library - not used + "espressif__esp_rainmaker", # RainMaker - not used + "espressif__esp_rcp_update", # RCP update - RainMaker transitive dep + "espressif__esp_schedule", # Schedule - RainMaker transitive dep + "espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep + "espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4 + "espressif__json_generator", # JSON generator - RainMaker transitive dep + "espressif__json_parser", # JSON parser - RainMaker transitive dep + "espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly + "espressif__libsodium", # Crypto - ESPHome uses its own noise-c library + "espressif__network_provisioning", # Network provisioning - not used + "espressif__qrcode", # QR code - not used + "espressif__rmaker_common", # RainMaker common - not used + "joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem +) + +# Mapping of Arduino libraries to IDF managed components they require +# When an Arduino library is enabled via cg.add_library(), these components +# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS. +# +# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have +# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle +# their managed component dependencies. Our mapping is primarily needed for libraries +# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.) +# and to ensure the stubs are removed from our idf_component.yml overrides. +ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { + "BLE": ("esp_driver_gptimer",), + "BluetoothSerial": ("esp_driver_gptimer",), + "ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"), + "ESP_SR": ("espressif__esp-sr",), + "Ethernet": ("espressif__lan867x",), + "FFat": ("fatfs",), + "Insights": ( + "espressif__cbor", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", # Transitive dep from esp_insights + ), + "LittleFS": ("joltwallet__littlefs",), + "Matter": ("espressif__esp_matter",), + "PPP": ("espressif__esp_modem",), + "RainMaker": ( + # Direct deps from idf_component.yml + "espressif__cbor", + "espressif__esp_rainmaker", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", + "espressif__qrcode", + # Transitive deps from esp_rainmaker + "espressif__esp_rcp_update", + "espressif__esp_schedule", + "espressif__esp_secure_cert_mgr", + "espressif__json_generator", + "espressif__json_parser", + "espressif__network_provisioning", + ), + "SD": ("fatfs",), + "SD_MMC": ("fatfs",), + "SPIFFS": ("spiffs",), + "WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"), + "Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"), +} + +# Arduino library to Arduino library dependencies +# When enabling one library, also enable its dependencies +# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION +ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = { + "Ethernet": ("Network",), + "WiFi": ("Network",), +} + + +def _idf_component_stub_name(component: str) -> str: + """Get stub directory name from IDF component name. + + Component names are typically namespace__name (e.g., espressif__cbor). + Returns just the name part (e.g., cbor). If no namespace is present, + returns the original component name. + """ + _prefix, sep, suffix = component.partition("__") + return suffix if sep else component + + +def _idf_component_dep_name(component: str) -> str: + """Convert IDF component name to dependency format. + + Converts espressif__cbor to espressif/cbor. + """ + return component.replace("__", "/") + + +# Arduino libraries to disable by default when using Arduino framework +# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core +# (HardwareSerial, Print, Stream, GPIO functions which are always compiled) +# Components use cg.add_library() which auto-enables any they need +# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt +ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset( + { + "ArduinoOTA", + "AsyncUDP", + "BLE", + "BluetoothSerial", + "DNSServer", + "EEPROM", + "ESP_HostedOTA", + "ESP_I2S", + "ESP_NOW", + "ESP_SR", + "ESPmDNS", + "Ethernet", + "FFat", + "FS", + "Hash", + "HTTPClient", + "HTTPUpdate", + "Insights", + "LittleFS", + "Matter", + "NetBIOS", + "Network", + "NetworkClientSecure", + "OpenThread", + "PPP", + "Preferences", + "RainMaker", + "SD", + "SD_MMC", + "SimpleBLE", + "SPI", + "SPIFFS", + "Ticker", + "Update", + "USB", + "WebServer", + "WiFi", + "WiFiProv", + "Wire", + "Zigbee", + } +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -243,7 +406,13 @@ def set_core_data(config): CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} # Initialize with default exclusions - components can call include_builtin_idf_component() # to re-enable any they need - CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + # Add Arduino-specific managed component exclusions when using Arduino framework + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded + # Initialize Arduino library tracking - cg.add_library() auto-enables libraries + CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set() CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -391,6 +560,26 @@ def include_builtin_idf_component(name: str) -> None: CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) +def _enable_arduino_library(name: str) -> None: + """Enable an Arduino library that is disabled by default. + + This is called automatically by CORE.add_library() when a component adds + an Arduino library via cg.add_library(). Components should not call this + directly - just use cg.add_library("LibName", None). + + Args: + name: The library name (e.g., "Wire", "SPI", "WiFi") + """ + enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] + enabled_libs.add(name) + # Also enable any required Arduino library dependencies + for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()): + enabled_libs.add(dep_lib) + # Also enable any required IDF components + for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()): + include_builtin_idf_component(idf_component) + + def add_extra_script(stage: str, filename: str, path: Path): """Add an extra script to the project.""" key = f"{stage}:{filename}" @@ -1132,6 +1321,27 @@ async def _write_exclude_components() -> None: ) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libraries_sdkconfig() -> None: + """Write Arduino selective compilation sdkconfig after all components have added libraries. + + This must run at FINAL priority so that all components have had a chance to call + cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library(). + """ + if KEY_ESP32 not in CORE.data: + return + # Enable Arduino selective compilation to disable unused Arduino libraries + # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core + # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) + # cg.add_library() auto-enables needed libraries; users can also add + # libraries via esphome: libraries: config which calls cg.add_library() + add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + for lib in ARDUINO_DISABLED_LIBRARIES: + # Enable if explicitly requested, disable otherwise + add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs) + + @coroutine_with_priority(CoroPriority.FINAL) async def _add_yaml_idf_components(components: list[ConfigType]): """Add IDF components from YAML config with final priority to override code-added components.""" @@ -1550,6 +1760,11 @@ async def to_code(config): # Default exclusions are added in set_core_data() during config validation. CORE.add_job(_write_exclude_components) + # Write Arduino selective compilation sdkconfig at FINAL priority after all + # components have had a chance to call cg.add_library() to enable libraries they need. + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + CORE.add_job(_write_arduino_libraries_sdkconfig) + APP_PARTITION_SIZES = { "2MB": 0x0C0000, # 768 KB @@ -1630,11 +1845,49 @@ def _write_sdkconfig(): def _write_idf_component_yml(): yml_path = CORE.relative_build_path("src/idf_component.yml") + dependencies: dict[str, dict] = {} + + # For Arduino builds, override unused managed components from the Arduino framework + # by pointing them to empty stub directories using override_path + # This prevents the IDF component manager from downloading the real components + if CORE.using_arduino: + # Determine which IDF components are needed by enabled Arduino libraries + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + required_idf_components = { + comp + for lib in enabled_libs + for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ()) + } + + # Only stub components that are not required by any enabled Arduino library + components_to_stub = ( + set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components + ) + + stubs_dir = CORE.relative_build_path("component_stubs") + stubs_dir.mkdir(exist_ok=True) + for component_name in components_to_stub: + # Create stub directory with minimal CMakeLists.txt + stub_path = stubs_dir / _idf_component_stub_name(component_name) + stub_path.mkdir(exist_ok=True) + stub_cmake = stub_path / "CMakeLists.txt" + if not stub_cmake.exists(): + stub_cmake.write_text("idf_component_register()\n") + dependencies[_idf_component_dep_name(component_name)] = { + "version": "*", + "override_path": str(stub_path), + } + + # Remove stubs for components that are now required by enabled libraries + for component_name in required_idf_components: + stub_path = stubs_dir / _idf_component_stub_name(component_name) + if stub_path.exists(): + rmtree(stub_path) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - dependencies = {} for name, component in components.items(): - dependency = {} + dependency: dict[str, str] = {} if component[KEY_REF]: dependency["version"] = component[KEY_REF] if component[KEY_REPO]: @@ -1642,9 +1895,8 @@ def _write_idf_component_yml(): if component[KEY_PATH]: dependency["path"] = component[KEY_PATH] dependencies[name] = dependency - contents = yaml_util.dump({"dependencies": dependencies}) - else: - contents = "" + + contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else "" if write_file_if_changed(yml_path, contents): dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index db3eddebd5..7874c1c759 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -7,6 +7,7 @@ KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_COMPONENTS = "components" KEY_EXCLUDE_COMPONENTS = "exclude_components" +KEY_ARDUINO_LIBRARIES = "arduino_libraries" KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 1f5ca1104a..faeccd910e 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WIFI, ) -from esphome.core import CORE, HexInt +from esphome.core import HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -124,9 +124,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.using_arduino: - cg.add_library("WiFi", None) - # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks # This enables low-latency event processing instead of waiting for select() timeout socket.require_wake_loop_threadsafe() diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 23436cc5be..38489ceb2b 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -431,9 +431,6 @@ async def to_code(config): # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") - if CORE.using_arduino: - cg.add_library("WiFi", None) - if on_connect_config := config.get(CONF_ON_CONNECT): cg.add_define("USE_ETHERNET_CONNECT_TRIGGER") await automation.build_automation( diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 35c42e1b06..426b211f47 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -114,6 +114,7 @@ async def to_code(config): cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) + cg.add_library("WiFi", None) cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) cg.add_library("esphome/ESP32-audioI2S", "2.3.0") diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index b08a47afa9..8a3d4f22ba 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -30,7 +30,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_WATT, ) -from esphome.core import coroutine +from esphome.core import CORE, coroutine CODEOWNERS = ["@dudanov"] DEPENDENCIES = ["climate", "uart"] @@ -290,4 +290,7 @@ async def to_code(config): if CONF_HUMIDITY_SETPOINT in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) cg.add(var.set_humidity_setpoint_sensor(sens)) + # MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping) + if CORE.is_esp32: + cg.add_library("WiFi", None) cg.add_library("dudanov/MideaUART", "1.1.9") diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 5b63bbfce9..1f75b12178 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -137,8 +137,7 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") - if CORE.using_arduino and CORE.is_esp32: - cg.add_library("Networking", None) + # ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed # Apply high performance networking settings # Config can explicitly enable/disable, or default to component-driven behavior diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 6326b4d6ff..11408ae260 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -38,11 +38,8 @@ async def to_code(config): cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) return + # ESP32 uses IDF web server (early return above), so this is for other Arduino platforms if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("WiFi", None) - cg.add_library("FS", None) - cg.add_library("Update", None) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) if CORE.is_libretiny: diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index fb20a03010..49eb15dad6 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -3,6 +3,7 @@ from esphome.components.light.effects import register_addressable_effect from esphome.components.light.types import AddressableLightEffect import esphome.config_validation as cv from esphome.const import CONF_NAME, CONF_PORT +from esphome.core import CORE wled_ns = cg.esphome_ns.namespace("wled") WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect) @@ -27,4 +28,6 @@ async def wled_light_effect_to_code(config, effect_id): cg.add(effect.set_port(config[CONF_PORT])) cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK])) cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START])) + if CORE.is_esp32: + cg.add_library("WiFi", None) return effect diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 5308ad241e..484f679369 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -892,6 +892,16 @@ class EsphomeCore: library.name if "/" not in library.name else library.name.split("/")[-1] ) + # Auto-enable Arduino libraries on ESP32 Arduino builds + if self.is_esp32 and self.using_arduino: + from esphome.components.esp32 import ( + ARDUINO_DISABLED_LIBRARIES, + _enable_arduino_library, + ) + + if short_name in ARDUINO_DISABLED_LIBRARIES: + _enable_arduino_library(short_name) + if short_name not in self.platformio_libraries: _LOGGER.debug("Adding library: %s", library) self.platformio_libraries[short_name] = library diff --git a/esphome/writer.py b/esphome/writer.py index cb9c921693..661118e518 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -421,6 +421,11 @@ def _rmtree_error_handler( func(path) +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows.""" + shutil.rmtree(path, onerror=_rmtree_error_handler) + + def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): @@ -430,11 +435,11 @@ def clean_build(clear_pio_cache: bool = True): pioenvs = CORE.relative_pioenvs_path() if pioenvs.is_dir(): _LOGGER.info("Deleting %s", pioenvs) - shutil.rmtree(pioenvs, onerror=_rmtree_error_handler) + rmtree(pioenvs) piolibdeps = CORE.relative_piolibdeps_path() if piolibdeps.is_dir(): _LOGGER.info("Deleting %s", piolibdeps) - shutil.rmtree(piolibdeps, onerror=_rmtree_error_handler) + rmtree(piolibdeps) dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): _LOGGER.info("Deleting %s", dependencies_lock) @@ -455,7 +460,7 @@ def clean_build(clear_pio_cache: bool = True): cache_dir = Path(config.get("platformio", "cache_dir")) if cache_dir.is_dir(): _LOGGER.info("Deleting PlatformIO cache %s", cache_dir) - shutil.rmtree(cache_dir, onerror=_rmtree_error_handler) + rmtree(cache_dir) def clean_all(configuration: list[str]): @@ -480,7 +485,7 @@ def clean_all(configuration: list[str]): if item.is_file() and not item.name.endswith(".json"): item.unlink() elif item.is_dir() and item.name != "storage": - shutil.rmtree(item, onerror=_rmtree_error_handler) + rmtree(item) # Clean PlatformIO project files try: @@ -494,7 +499,7 @@ def clean_all(configuration: list[str]): path = Path(config.get("platformio", pio_dir)) if path.is_dir(): _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) - shutil.rmtree(path, onerror=_rmtree_error_handler) + rmtree(path) GITIGNORE_CONTENT = """# Gitignore settings for ESPHome diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml new file mode 100644 index 0000000000..4276b4f922 --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +substitutions: + i2s_bclk_pin: GPIO15 + i2s_lrclk_pin: GPIO4 + i2s_mclk_pin: GPIO5 + +<<: !include common.yaml + +wifi: + ssid: test + password: test1234 + +media_player: + - platform: i2s_audio + name: Test Media Player + dac_type: internal + mode: stereo diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 1fc8dab358..174b3fec85 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -780,3 +780,78 @@ class TestEsphomeCore: target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}} assert target.has_networking is False + + def test_add_library__esp32_arduino_enables_disabled_library(self, target): + """Test add_library auto-enables Arduino libraries on ESP32 Arduino builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("WiFi") + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp32_arduino_ignores_non_arduino_library(self, target): + """Test add_library doesn't enable libraries not in ARDUINO_DISABLED_LIBRARIES.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("SomeOtherLib", "1.0.0") + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "SomeOtherLib" in target.platformio_libraries + + def test_add_library__esp32_idf_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP32 IDF builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "esp-idf", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp8266_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP8266.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp8266", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__extracts_short_name_from_path(self, target): + """Test add_library extracts short name from library paths like owner/lib.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("arduino/Wire", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("Wire") + + assert "Wire" in target.platformio_libraries From ed8c0dc99d519f492e51d1f054f5ea9d5e07fe5b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Feb 2026 05:55:08 -0500 Subject: [PATCH 096/251] [esp32] Skip downloading precompiled Arduino libs (#13775) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 33 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index e1e3164b68..f5a8bc8994 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -95,6 +95,11 @@ CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" +ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" +ARDUINO_LIBS_NAME = f"{ARDUINO_FRAMEWORK_NAME}-libs" +ARDUINO_LIBS_PKG = f"pioarduino/{ARDUINO_LIBS_NAME}" + LOG_LEVELS_IDF = [ "NONE", "ERROR", @@ -599,14 +604,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool: def _format_framework_arduino_version(ver: cv.Version) -> str: - # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to - # a PIO pioarduino/framework-arduinoespressif32 value # 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz if ver >= cv.Version(3, 3, 6): filename = f"esp32-core-{ver}.tar.xz" else: filename = f"esp32-{ver}.zip" - return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" + return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: @@ -741,9 +744,7 @@ def _check_versions(config): CONF_SOURCE, _format_framework_arduino_version(version) ) if _is_framework_url(value[CONF_SOURCE]): - value[CONF_SOURCE] = ( - f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" - ) + value[CONF_SOURCE] = f"{ARDUINO_FRAMEWORK_PKG}@{value[CONF_SOURCE]}" else: if version < cv.Version(5, 0, 0): raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") @@ -1321,6 +1322,20 @@ async def _write_exclude_components() -> None: ) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libs_stub(stubs_dir: Path, idf_ver: cv.Version) -> None: + """Write stub package to skip downloading precompiled Arduino libs.""" + stubs_dir.mkdir(parents=True, exist_ok=True) + write_file_if_changed( + stubs_dir / "package.json", + f'{{"name":"{ARDUINO_LIBS_NAME}","version":"{idf_ver.major}.{idf_ver.minor}.{idf_ver.patch}"}}', + ) + write_file_if_changed( + stubs_dir / "tools.json", + '{"packages":[{"platforms":[{"toolsDependencies":[]}],"tools":[]}]}', + ) + + @coroutine_with_priority(CoroPriority.FINAL) async def _write_arduino_libraries_sdkconfig() -> None: """Write Arduino selective compilation sdkconfig after all components have added libraries. @@ -1451,6 +1466,12 @@ async def to_code(config): "platform_packages", [_format_framework_espidf_version(idf_ver, None)], ) + # Use stub package to skip downloading precompiled libs + stubs_dir = CORE.relative_build_path("arduino-libs-stub") + cg.add_platformio_option( + "platform_packages", [f"{ARDUINO_LIBS_PKG}@file://{stubs_dir}"] + ) + CORE.add_job(_write_arduino_libs_stub, stubs_dir, idf_ver) # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency if get_esp32_variant() == VARIANT_ESP32S2: From 9ea846144008ff78f082e0987132047b7092a726 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:41:17 -0500 Subject: [PATCH 097/251] [esp32] Remove specific claims from framework migration message (#13777) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f5a8bc8994..0c02f6fbee 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1169,8 +1169,8 @@ def _show_framework_migration_message(name: str, variant: str) -> None: + "(We've been warning about this change since ESPHome 2025.8.0)\n" + "\n" + "Why we made this change:\n" - + color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n") - + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") + + color(AnsiFore.GREEN, " ✨ Smaller firmware binaries\n") + + color(AnsiFore.GREEN, " ⚡ Faster compile times\n") + color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n") + color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n") + "\n" From bbdb202e2c53358dbd20076a748bc743500371e6 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:26:47 +0100 Subject: [PATCH 098/251] [epaper_spi] Refactor initialise for future use (#13774) --- esphome/components/epaper_spi/epaper_spi.cpp | 13 +++++++++---- esphome/components/epaper_spi/epaper_spi.h | 3 ++- esphome/components/epaper_spi/epaper_waveshare.cpp | 3 ++- esphome/components/epaper_spi/epaper_waveshare.h | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index db803305a5..ae1923a916 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -182,7 +182,9 @@ void EPaperBase::process_state_() { this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: - this->initialise(this->update_count_ != 0); + if (!this->initialise(this->update_count_ != 0)) { + return; // Not done yet, come back next loop + } this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: @@ -239,11 +241,9 @@ void EPaperBase::start_data_() { void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } -void EPaperBase::initialise(bool partial) { +void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) { size_t index = 0; - auto *sequence = this->init_sequence_; - auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { this->mark_failed(LOG_STR("Malformed init sequence")); @@ -266,6 +266,11 @@ void EPaperBase::initialise(bool partial) { } } +bool EPaperBase::initialise(bool partial) { + this->send_init_sequence_(this->init_sequence_, this->init_sequence_length_); + return true; +} + /** * Check and rotate coordinates based on the transform flags. * @param x diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 521543f026..a8c2fe9b56 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -115,7 +115,8 @@ class EPaperBase : public Display, bool is_idle_() const; void setup_pins_() const; virtual bool reset(); - virtual void initialise(bool partial); + virtual bool initialise(bool partial); + void send_init_sequence_(const uint8_t *sequence, size_t length); void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); bool rotate_coordinates_(int &x, int &y); diff --git a/esphome/components/epaper_spi/epaper_waveshare.cpp b/esphome/components/epaper_spi/epaper_waveshare.cpp index 8d382d86e7..7a7b4e22d3 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.cpp +++ b/esphome/components/epaper_spi/epaper_waveshare.cpp @@ -4,7 +4,7 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi.waveshare"; -void EpaperWaveshare::initialise(bool partial) { +bool EpaperWaveshare::initialise(bool partial) { EPaperBase::initialise(partial); if (partial) { this->cmd_data(0x32, this->partial_lut_, this->partial_lut_length_); @@ -17,6 +17,7 @@ void EpaperWaveshare::initialise(bool partial) { this->cmd_data(0x3C, {0x05}); } this->send_red_ = true; + return true; } void EpaperWaveshare::set_window() { diff --git a/esphome/components/epaper_spi/epaper_waveshare.h b/esphome/components/epaper_spi/epaper_waveshare.h index 6b894cfd09..15fb575ca0 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.h +++ b/esphome/components/epaper_spi/epaper_waveshare.h @@ -18,7 +18,7 @@ class EpaperWaveshare : public EPaperMono { partial_lut_length_(partial_lut_length) {} protected: - void initialise(bool partial) override; + bool initialise(bool partial) override; void set_window() override; void refresh_screen(bool partial) override; void deep_sleep() override; From f4e410f47f92199def4494525d121528b4504bc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 5 Feb 2026 14:56:43 +0100 Subject: [PATCH 099/251] [ci] Block new scanf() usage to prevent ~9.8KB flash bloat (#13657) --- script/ci-custom.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index b146c9867e..b5bec74fa7 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -756,6 +756,28 @@ def lint_no_sprintf(fname, match): ) +@lint_re_check( + # Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf + # Also match std:: prefixed versions + # [^\w] ensures we match function calls, not substrings + r"[^\w]((?:std::)?v?[fs]?scanf)\s*\(" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_scanf(fname, match): + func = match.group(1) + return ( + f"{highlight(func + '()')} is not allowed in new ESPHome code. The scanf family " + f"pulls in ~7KB flash on ESP8266 and ~9KB on ESP32, and ESPHome doesn't otherwise " + f"need this code.\n" + f"Please use alternatives:\n" + f" - {highlight('parse_number<T>(str)')} for parsing integers/floats from strings\n" + f" - {highlight('strtol()/strtof()')} for C-style number parsing with error checking\n" + f" - {highlight('parse_hex()')} for hex string parsing\n" + f" - Manual parsing for simple fixed formats\n" + f"(If strictly necessary, add `// NOLINT` to the end of the line)" + ) + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 081f953dc330cdf22e09c3286b9d957477f68c95 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:00:16 -0500 Subject: [PATCH 100/251] [core] Add capacity check to register_component_ (#13778) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/core/application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0e77be9ee4..0daabc0282 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -81,6 +81,10 @@ void Application::register_component_(Component *comp) { return; } } + if (this->components_.size() >= ESPHOME_COMPONENT_COUNT) { + ESP_LOGE(TAG, "Cannot register component %s - at capacity!", LOG_STR_ARG(comp->get_component_log_str())); + return; + } this->components_.push_back(comp); } void Application::setup() { From 55ef8393afd40baf100424c0ef0a01efa25ffc08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 5 Feb 2026 15:19:03 +0100 Subject: [PATCH 101/251] [api] Remove is_single parameter and fix batch buffer preparation (#13773) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 354 +++++++++------------- esphome/components/api/api_connection.h | 176 ++++------- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 32 +- script/api_protobuf/api_protobuf.py | 2 +- 5 files changed, 221 insertions(+), 345 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 839de29de7..2aa5956f24 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -300,7 +300,7 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { // Encodes a message to the buffer and returns the total number of bytes used, // including header and footer overhead. Returns 0 if the message doesn't fit. uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { #ifdef HAS_PROTO_MESSAGE_DUMP // If in log-only mode, just log and return if (conn->flags_.log_only_mode) { @@ -330,12 +330,9 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess // Get buffer size after allocation (which includes header padding) std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); - if (is_single || conn->flags_.batch_first_message) { - // Single message or first batch message - conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size); - if (conn->flags_.batch_first_message) { - conn->flags_.batch_first_message = false; - } + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller, just clear flag + conn->flags_.batch_first_message = false; } else { // Batch message second or later // Add padding for previous message footer + this message header @@ -365,24 +362,22 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary BinarySensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } -uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); ListEntitiesBinarySensorResponse msg; msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } #endif @@ -390,8 +385,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool APIConnection::send_cover_state(cover::Cover *cover) { return this->send_message_smart_(cover, CoverStateResponse::MESSAGE_TYPE, CoverStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast<cover::Cover *>(entity); CoverStateResponse msg; auto traits = cover->get_traits(); @@ -399,10 +393,9 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); - return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast<cover::Cover *>(entity); ListEntitiesCoverResponse msg; auto traits = cover->get_traits(); @@ -411,8 +404,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); msg.device_class = cover->get_device_class_ref(); - return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::cover_command(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -430,8 +422,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { bool APIConnection::send_fan_state(fan::Fan *fan) { return this->send_message_smart_(fan, FanStateResponse::MESSAGE_TYPE, FanStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast<fan::Fan *>(entity); FanStateResponse msg; auto traits = fan->get_traits(); @@ -445,10 +436,9 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast<enums::FanDirection>(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) msg.preset_mode = fan->get_preset_mode(); - return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast<fan::Fan *>(entity); ListEntitiesFanResponse msg; auto traits = fan->get_traits(); @@ -457,7 +447,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); msg.supported_preset_modes = &traits.supported_preset_modes(); - return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::fan_command(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) @@ -481,8 +471,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { bool APIConnection::send_light_state(light::LightState *light) { return this->send_message_smart_(light, LightStateResponse::MESSAGE_TYPE, LightStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast<light::LightState *>(entity); LightStateResponse resp; auto values = light->remote_values; @@ -501,10 +490,9 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * if (light->supports_effects()) { resp.effect = light->get_effect_name(); } - return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast<light::LightState *>(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); @@ -527,8 +515,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c } } msg.effects = &effects_list; - return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::light_command(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -568,17 +555,15 @@ bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { return this->send_message_smart_(sensor, SensorStateResponse::MESSAGE_TYPE, SensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast<sensor::Sensor *>(entity); SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast<sensor::Sensor *>(entity); ListEntitiesSensorResponse msg; msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); @@ -586,8 +571,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); - return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -596,23 +580,19 @@ bool APIConnection::send_switch_state(switch_::Switch *a_switch) { return this->send_message_smart_(a_switch, SwitchStateResponse::MESSAGE_TYPE, SwitchStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast<switch_::Switch *>(entity); SwitchStateResponse resp; resp.state = a_switch->state; - return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast<switch_::Switch *>(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); msg.device_class = a_switch->get_device_class_ref(); - return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -631,22 +611,19 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) TextSensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); TextSensorStateResponse resp; resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); - return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); ListEntitiesTextSensorResponse msg; msg.device_class = text_sensor->get_device_class_ref(); return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } #endif @@ -654,8 +631,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect bool APIConnection::send_climate_state(climate::Climate *climate) { return this->send_message_smart_(climate, ClimateStateResponse::MESSAGE_TYPE, ClimateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast<climate::Climate *>(entity); ClimateStateResponse resp; auto traits = climate->get_traits(); @@ -687,11 +663,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; - return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast<climate::Climate *>(entity); ListEntitiesClimateResponse msg; auto traits = climate->get_traits(); @@ -716,8 +690,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_presets = &traits.get_supported_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_swing_modes = &traits.get_supported_swing_modes(); - return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -750,17 +723,15 @@ bool APIConnection::send_number_state(number::Number *number) { return this->send_message_smart_(number, NumberStateResponse::MESSAGE_TYPE, NumberStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast<number::Number *>(entity); NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast<number::Number *>(entity); ListEntitiesNumberResponse msg; msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); @@ -769,8 +740,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::number_command(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -783,22 +753,19 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { bool APIConnection::send_date_state(datetime::DateEntity *date) { return this->send_message_smart_(date, DateStateResponse::MESSAGE_TYPE, DateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast<datetime::DateEntity *>(entity); DateStateResponse resp; resp.missing_state = !date->has_state(); resp.year = date->year; resp.month = date->month; resp.day = date->day; - return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast<datetime::DateEntity *>(entity); ListEntitiesDateResponse msg; - return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::date_command(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -811,22 +778,19 @@ void APIConnection::date_command(const DateCommandRequest &msg) { bool APIConnection::send_time_state(datetime::TimeEntity *time) { return this->send_message_smart_(time, TimeStateResponse::MESSAGE_TYPE, TimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast<datetime::TimeEntity *>(entity); TimeStateResponse resp; resp.missing_state = !time->has_state(); resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast<datetime::TimeEntity *>(entity); ListEntitiesTimeResponse msg; - return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::time_command(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -840,8 +804,7 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { return this->send_message_smart_(datetime, DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); DateTimeStateResponse resp; resp.missing_state = !datetime->has_state(); @@ -849,15 +812,12 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); ListEntitiesDateTimeResponse msg; - return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) @@ -871,25 +831,22 @@ bool APIConnection::send_text_state(text::Text *text) { return this->send_message_smart_(text, TextStateResponse::MESSAGE_TYPE, TextStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast<text::Text *>(entity); TextStateResponse resp; resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); - return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast<text::Text *>(entity); ListEntitiesTextResponse msg; msg.mode = static_cast<enums::TextMode>(text->traits.get_mode()); msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern_ref(); - return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::text_command(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) @@ -903,22 +860,19 @@ bool APIConnection::send_select_state(select::Select *select) { return this->send_message_smart_(select, SelectStateResponse::MESSAGE_TYPE, SelectStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast<select::Select *>(entity); SelectStateResponse resp; resp.state = select->current_option(); resp.missing_state = !select->has_state(); - return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast<select::Select *>(entity); ListEntitiesSelectResponse msg; msg.options = &select->traits.get_options(); - return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::select_command(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) @@ -928,13 +882,11 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast<button::Button *>(entity); ListEntitiesButtonResponse msg; msg.device_class = button->get_device_class_ref(); - return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -947,23 +899,20 @@ bool APIConnection::send_lock_state(lock::Lock *a_lock) { return this->send_message_smart_(a_lock, LockStateResponse::MESSAGE_TYPE, LockStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast<lock::Lock *>(entity); LockStateResponse resp; resp.state = static_cast<enums::LockState>(a_lock->state); - return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast<lock::Lock *>(entity); ListEntitiesLockResponse msg; msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::lock_command(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -986,16 +935,14 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { bool APIConnection::send_valve_state(valve::Valve *valve) { return this->send_message_smart_(valve, ValveStateResponse::MESSAGE_TYPE, ValveStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast<valve::Valve *>(entity); ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); - return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast<valve::Valve *>(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); @@ -1003,8 +950,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::valve_command(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1021,8 +967,7 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla return this->send_message_smart_(media_player, MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast<media_player::MediaPlayer *>(entity); MediaPlayerStateResponse resp; media_player::MediaPlayerState report_state = media_player->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING @@ -1031,11 +976,9 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast<enums::MediaPlayerState>(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast<media_player::MediaPlayer *>(entity); ListEntitiesMediaPlayerResponse msg; auto traits = media_player->get_traits(); @@ -1051,7 +994,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.sample_bytes = supported_format.sample_bytes; } return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1092,7 +1035,7 @@ void APIConnection::try_send_camera_image_() { msg.device_id = camera::Camera::instance()->get_device_id(); #endif - if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { + if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) { return; // Send failed, try again later } this->image_reader_->consume_data(to_send); @@ -1115,12 +1058,10 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) this->try_send_camera_image_(); } } -uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *camera = static_cast<camera::Camera *>(entity); ListEntitiesCameraResponse msg; - return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::camera_image(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) @@ -1305,22 +1246,22 @@ bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmCon AlarmControlPanelStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); ListEntitiesAlarmControlPanelResponse msg; msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, - conn, remaining_size, is_single); + conn, remaining_size); } void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) @@ -1357,8 +1298,7 @@ bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_hea return this->send_message_smart_(water_heater, WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast<water_heater::WaterHeater *>(entity); WaterHeaterStateResponse resp; resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode()); @@ -1369,10 +1309,9 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.state = wh->get_state(); resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast<water_heater::WaterHeater *>(entity); ListEntitiesWaterHeaterResponse msg; auto traits = wh->get_traits(); @@ -1381,8 +1320,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); - return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) { @@ -1411,20 +1349,18 @@ void APIConnection::send_event(event::Event *event) { event->get_last_event_type_index()); } uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { EventResponse resp; resp.event_type = event_type; - return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast<event::Event *>(entity); ListEntitiesEventResponse msg; msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1447,13 +1383,11 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent #endif #ifdef USE_INFRARED -uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *infrared = static_cast<infrared::Infrared *>(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); - return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1461,8 +1395,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection bool APIConnection::send_update_state(update::UpdateEntity *update) { return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); UpdateStateResponse resp; resp.missing_state = !update->has_state(); @@ -1478,15 +1411,13 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = StringRef(update->update_info.summary); resp.release_url = StringRef(update->update_info.release_url); } - return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); ListEntitiesUpdateResponse msg; msg.device_class = update->get_device_class_ref(); - return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::update_command(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1512,7 +1443,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char SubscribeLogsResponse msg; msg.level = static_cast<enums::LogLevel>(level); msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len); - return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE); + return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE); } void APIConnection::complete_authentication_() { @@ -1837,6 +1768,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } +bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { + ProtoSize size; + msg.calculate_size(size); + std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, size.get_size()); + msg.encode({&shared_buf}); + return this->send_buffer({&shared_buf}, message_type); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -1897,6 +1836,23 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me } } +bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, + uint8_t aux_data_index) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + auto &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, estimated_size); + DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; + if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && + this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_batch_item_(item); +#endif + return true; + } + } + return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); +} + bool APIConnection::schedule_batch_() { if (!this->flags_.batch_scheduled) { this->flags_.batch_scheduled = true; @@ -1925,10 +1881,21 @@ void APIConnection::process_batch_() { auto &shared_buf = this->parent_->get_shared_buffer_ref(); size_t num_items = this->deferred_batch_.size(); - // Fast path for single message - allocate exact size needed + // Cache these values to avoid repeated virtual calls + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + + // Pre-calculate exact buffer size needed based on message types + uint32_t total_estimated_size = num_items * (header_padding + footer_size); + for (size_t i = 0; i < num_items; i++) { + total_estimated_size += this->deferred_batch_[i].estimated_size; + } + + this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); + + // Fast path for single message - buffer already allocated above if (num_items == 1) { const auto &item = this->deferred_batch_[0]; - // Let dispatch_message_ calculate size and encode if it fits uint16_t payload_size = this->dispatch_message_(item, std::numeric_limits<uint16_t>::max(), true); @@ -1951,30 +1918,8 @@ void APIConnection::process_batch_() { // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage); - size_t message_count = 0; - - // Cache these values to avoid repeated virtual calls - const uint8_t header_padding = this->helper_->frame_header_padding(); - const uint8_t footer_size = this->helper_->frame_footer_size(); - - // Initialize buffer and tracking variables - shared_buf.clear(); - - // Pre-calculate exact buffer size needed based on message types - uint32_t total_estimated_size = num_items * (header_padding + footer_size); - for (size_t i = 0; i < this->deferred_batch_.size(); i++) { - const auto &item = this->deferred_batch_[i]; - total_estimated_size += item.estimated_size; - } - - // Calculate total overhead for all messages - // Reserve based on estimated size (much more accurate than 24-byte worst-case) - shared_buf.reserve(total_estimated_size); - this->flags_.batch_first_message = true; - size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits<uint16_t>::max(); - // Track where each message's header padding begins in the buffer // For plaintext: this is where the 6-byte header padding starts // For noise: this is where the 7-byte header padding starts @@ -1986,7 +1931,7 @@ void APIConnection::process_batch_() { const auto &item = this->deferred_batch_[i]; // Try to encode message via dispatch // The dispatch function calculates overhead to determine if the message fits - uint16_t payload_size = this->dispatch_message_(item, remaining_size, false); + uint16_t payload_size = this->dispatch_message_(item, remaining_size, i == 0); if (payload_size == 0) { // Message won't fit, stop processing @@ -2000,10 +1945,7 @@ void APIConnection::process_batch_() { // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, // as ensured by the static_assert in its definition. - new (&message_info[message_count++]) MessageInfo(item.message_type, current_offset, proto_payload_size); - - // Update tracking variables - items_processed++; + new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size); // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation if (items_processed == 1) { remaining_size = MAX_BATCH_PACKET_SIZE; @@ -2026,7 +1968,7 @@ void APIConnection::process_batch_() { // Send all collected messages APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, - std::span<const MessageInfo>(message_info, message_count)); + std::span<const MessageInfo>(message_info, items_processed)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); } @@ -2055,7 +1997,8 @@ void APIConnection::process_batch_() { // Dispatch message encoding based on message_type // Switch assigns function pointer, single call site for smaller code size uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, - bool is_single) { + bool batch_first) { + this->flags_.batch_first_message = batch_first; #ifdef USE_EVENT // Events need aux_data_index to look up event type from entity if (item.message_type == EventResponse::MESSAGE_TYPE) { @@ -2064,7 +2007,7 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, return 0; auto *event = static_cast<event::Event *>(item.entity); return try_send_event_response(event, StringRef::from_maybe_nullptr(event->get_event_type(item.aux_data_index)), - this, remaining_size, is_single); + this, remaining_size); } #endif @@ -2174,25 +2117,22 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, #undef CASE_STATE_INFO #undef CASE_INFO_ONLY - return func(item.entity, this, remaining_size, is_single); + return func(item.entity, this, remaining_size); } -uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { ListEntitiesDoneResponse resp; - return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { DisconnectRequest req; - return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { PingRequest req; - return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size); } #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b839a2a97b..40e4fd61c1 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -255,17 +255,7 @@ class APIConnection final : public APIServerConnection { void on_fatal_error() override; void on_no_setup_connection() override; - ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { - // FIXME: ensure no recursive writes can happen - - // Get header padding size - used for both reserve and insert - uint8_t header_padding = this->helper_->frame_header_padding(); - // Get shared buffer from parent server - std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, header_padding, - reserve_size + header_padding + this->helper_->frame_footer_size()); - return {&shared_buf}; - } + bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override; void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); @@ -277,6 +267,13 @@ class APIConnection final : public APIServerConnection { shared_buf.resize(header_padding); } + // Convenience overload - computes frame overhead internally + void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) { + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); + } + bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; @@ -298,21 +295,21 @@ class APIConnection final : public APIServerConnection { // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); + uint32_t remaining_size); // Helper to fill entity state base and encode message static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { + APIConnection *conn, uint32_t remaining_size) { msg.key = entity->get_object_id_hash(); #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + return encode_message_to_buffer(msg, message_type, conn, remaining_size); } // Helper to fill entity info base and encode message static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { + APIConnection *conn, uint32_t remaining_size) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); @@ -339,7 +336,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + return encode_message_to_buffer(msg, message_type, conn, remaining_size); } #ifdef USE_VOICE_ASSISTANT @@ -370,141 +367,108 @@ class APIConnection final : public APIServerConnection { } #ifdef USE_BINARY_SENSOR - static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_COVER - static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_FAN - static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LIGHT - static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SENSOR - static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SWITCH - static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT_SENSOR - static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CLIMATE - static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_NUMBER - static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATE - static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_TIME - static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATETIME - static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT - static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SELECT - static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_BUTTON - static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LOCK - static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_VALVE - static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_MEDIA_PLAYER - static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_ALARM_CONTROL_PANEL - static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_WATER_HEATER - static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_INFRARED - static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_EVENT static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); - static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + uint32_t remaining_size); + static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_UPDATE - static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CAMERA - static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif // Method for ListEntitiesDone batching - static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Method for DisconnectRequest batching - static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Batch message method for ping requests - static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // === Optimal member ordering for 32-bit systems === @@ -539,7 +503,7 @@ class APIConnection final : public APIServerConnection { #endif // Function pointer type for message encoding - using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); + using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size); // Generic batching mechanism for both state updates and entity info struct DeferredBatch { @@ -652,7 +616,7 @@ class APIConnection final : public APIServerConnection { // Dispatch message encoding based on message_type - replaces function pointer storage // Switch assigns pointer, single call site for smaller code size - uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool is_single); + uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool batch_first); #ifdef HAS_PROTO_MESSAGE_DUMP void log_batch_item_(const DeferredBatch::BatchItem &item) { @@ -684,19 +648,7 @@ class APIConnection final : public APIServerConnection { // Tries immediate send if should_send_immediately_() returns true and buffer has space // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) { - if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { - DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; - if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && - this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { -#ifdef HAS_PROTO_MESSAGE_DUMP - this->log_batch_item_(item); -#endif - return true; - } - } - return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); - } + uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED); // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 80a61c1041..e2bc1609ed 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -23,7 +23,7 @@ class APIServerConnectionBase : public ProtoService { DumpBuffer dump_buf; this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); #endif - return this->send_message_(msg, message_type); + return this->send_message_impl(msg, message_type); } virtual void on_hello_request(const HelloRequest &value){}; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 552b4a4625..92978f765f 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -957,32 +957,16 @@ class ProtoService { virtual bool is_connection_setup() = 0; virtual void on_fatal_error() = 0; virtual void on_no_setup_connection() = 0; - /** - * Create a buffer with a reserved size. - * @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint - * to optimize memory usage and avoid reallocations during encoding. - * Implementations should aim to allocate at least this size. - * @return A ProtoWriteBuffer object with the reserved size. - */ - virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - - // Optimized method that pre-allocates buffer based on message size - bool send_message_(const ProtoMessage &msg, uint8_t message_type) { - ProtoSize size; - msg.calculate_size(size); - uint32_t msg_size = size.get_size(); - - // Create a pre-sized buffer - auto buffer = this->create_buffer(msg_size); - - // Encode message into the buffer - msg.encode(buffer); - - // Send the buffer - return this->send_buffer(buffer, message_type); - } + /** + * Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending. + * This is the implementation method - callers should use send_message() which adds logging. + * @param msg The protobuf message to send. + * @param message_type The message type identifier. + * @return True if the message was sent successfully, false otherwise. + */ + virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0; // Authentication helper methods inline bool check_connection_setup_() { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 8baf6acf11..4021a062ca 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2848,7 +2848,7 @@ static const char *const TAG = "api.service"; hpp += " DumpBuffer dump_buf;\n" hpp += " this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));\n" hpp += "#endif\n" - hpp += " return this->send_message_(msg, message_type);\n" + hpp += " return this->send_message_impl(msg, message_type);\n" hpp += " }\n\n" # Add logging helper method implementations to cpp From ed4f00d4a3c80c54f6084305a640377f4f575105 Mon Sep 17 00:00:00 2001 From: Marek Beran <95686867+Bercek71@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:11:14 +0100 Subject: [PATCH 102/251] [vbus] Add DeltaSol BS/2 support with sensors and binary sensors (#13762) --- esphome/components/vbus/__init__.py | 1 + .../components/vbus/binary_sensor/__init__.py | 41 +++++++ .../vbus/binary_sensor/vbus_binary_sensor.cpp | 19 +++ .../vbus/binary_sensor/vbus_binary_sensor.h | 17 +++ esphome/components/vbus/sensor/__init__.py | 110 ++++++++++++++++++ .../components/vbus/sensor/vbus_sensor.cpp | 39 +++++++ esphome/components/vbus/sensor/vbus_sensor.h | 30 +++++ tests/components/vbus/common.yaml | 50 ++++++-- 8 files changed, 298 insertions(+), 9 deletions(-) diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index d916d7c064..5790a9cce0 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -16,6 +16,7 @@ CONF_VBUS_ID = "vbus_id" CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" CONF_DELTASOL_BS_2009 = "deltasol_bs_2009" +CONF_DELTASOL_BS2 = "deltasol_bs2" CONF_DELTASOL_C = "deltasol_c" CONF_DELTASOL_CS2 = "deltasol_cs2" CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index ae927656c0..70dda94300 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -27,6 +28,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) @@ -118,6 +120,28 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -275,6 +299,23 @@ async def to_code(config): ) cg.add(var.set_frost_protection_active_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index 4ccd149935..c1d7bc1b18 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -129,6 +129,25 @@ void DeltaSolCSPlusBSensor::handle_message(std::vector<uint8_t> &message) { this->s4_error_bsensor_->publish_state(message[20] & 8); } +void DeltaSolBS2BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolBS2BSensor::handle_message(std::vector<uint8_t> &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[10] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[10] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[10] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[10] & 8); +} + void VBusCustomBSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Binary Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index 146aa1b673..2decdde602 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -111,6 +111,23 @@ class DeltaSolCSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector<uint8_t> &message) override; }; +class DeltaSolBS2BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector<uint8_t> &message) override; +}; + class VBusCustomSubBSensor; class VBusCustomBSensor : public VBusListener, public Component { diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index fcff698ac0..ff8ef98a1a 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -43,6 +44,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2Sensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) @@ -227,6 +229,79 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -560,6 +635,41 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_VERSION]) cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index e81c0486d4..75c9ea1aee 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -214,6 +214,45 @@ void DeltaSolCSPlusSensor::handle_message(std::vector<uint8_t> &message) { this->flow_rate_sensor_->publish_state(get_u16(message, 38)); } +void DeltaSolBS2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBS2Sensor::handle_message(std::vector<uint8_t> &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 12)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) { + float heat_wh = get_u16(message, 16) + get_u16(message, 18) * 1000.0f + get_u16(message, 20) * 1000000.0f; + this->heat_quantity_sensor_->publish_state(heat_wh); + } + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 24) * 0.01f); +} + void VBusCustomSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index d5535b2019..cea2ee1c86 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -157,6 +157,36 @@ class DeltaSolCSPlusSensor : public VBusListener, public Component { void handle_message(std::vector<uint8_t> &message) override; }; +class DeltaSolBS2Sensor : public VBusListener, public Component { + public: + void dump_config() override; + + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector<uint8_t> &message) override; +}; + class VBusCustomSubSensor; class VBusCustomSensor : public VBusListener, public Component { diff --git a/tests/components/vbus/common.yaml b/tests/components/vbus/common.yaml index 33d9e2935d..5c771be922 100644 --- a/tests/components/vbus/common.yaml +++ b/tests/components/vbus/common.yaml @@ -4,11 +4,21 @@ binary_sensor: - platform: vbus model: deltasol_bs_plus relay1: - name: Relay 1 On + name: BS Plus Relay 1 On relay2: - name: Relay 2 On + name: BS Plus Relay 2 On sensor1_error: - name: Sensor 1 Error + name: BS Plus Sensor 1 Error + - platform: vbus + model: deltasol_bs2 + sensor1_error: + name: BS2 Sensor 1 Error + sensor2_error: + name: BS2 Sensor 2 Error + sensor3_error: + name: BS2 Sensor 3 Error + sensor4_error: + name: BS2 Sensor 4 Error - platform: vbus model: custom command: 0x100 @@ -23,14 +33,36 @@ sensor: - platform: vbus model: deltasol c temperature_1: - name: Temperature 1 + name: DeltaSol C Temperature 1 temperature_2: - name: Temperature 2 + name: DeltaSol C Temperature 2 temperature_3: - name: Temperature 3 + name: DeltaSol C Temperature 3 operating_hours_1: - name: Operating Hours 1 + name: DeltaSol C Operating Hours 1 heat_quantity: - name: Heat Quantity + name: DeltaSol C Heat Quantity time: - name: System Time + name: DeltaSol C System Time + - platform: vbus + model: deltasol_bs2 + temperature_1: + name: BS2 Temperature 1 + temperature_2: + name: BS2 Temperature 2 + temperature_3: + name: BS2 Temperature 3 + temperature_4: + name: BS2 Temperature 4 + pump_speed_1: + name: BS2 Pump Speed 1 + pump_speed_2: + name: BS2 Pump Speed 2 + operating_hours_1: + name: BS2 Operating Hours 1 + operating_hours_2: + name: BS2 Operating Hours 2 + heat_quantity: + name: BS2 Heat Quantity + version: + name: BS2 Firmware Version From c7729cb019021d332fa414e7e6f1a183127b6f0a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 03:51:13 -0500 Subject: [PATCH 103/251] [esp32] Use underscores in arduino_libs_stub folder name (#13785) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c02f6fbee..90f6035aba 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1467,7 +1467,7 @@ async def to_code(config): [_format_framework_espidf_version(idf_ver, None)], ) # Use stub package to skip downloading precompiled libs - stubs_dir = CORE.relative_build_path("arduino-libs-stub") + stubs_dir = CORE.relative_build_path("arduino_libs_stub") cg.add_platformio_option( "platform_packages", [f"{ARDUINO_LIBS_PKG}@file://{stubs_dir}"] ) From 6decdfad26772115bfddc4525ae9624c1ac75101 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:05:10 +0100 Subject: [PATCH 104/251] Bump github/codeql-action from 4.32.1 to 4.32.2 (#13781) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 817ea1d2be..51ea4085e0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 + uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1 + uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: category: "/language:${{matrix.language}}" From 8e461db301dcb00b04147c57d6a62f8639109a18 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:09:48 -0500 Subject: [PATCH 105/251] [ota] Fix CLI upload option shown when only http_request platform configured (#13784) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/__main__.py | 9 +++- tests/unit_tests/test_main.py | 85 ++++++++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 55297e8d9b..c86b5604e1 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -294,8 +294,13 @@ def has_api() -> bool: def has_ota() -> bool: - """Check if OTA is available.""" - return CONF_OTA in CORE.config + """Check if OTA upload is available (requires platform: esphome).""" + if CONF_OTA not in CORE.config: + return False + return any( + ota_item.get(CONF_PLATFORM) == CONF_ESPHOME + for ota_item in CORE.config[CONF_OTA] + ) def has_mqtt_ip_lookup() -> bool: diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3268f7ee87..c9aa446323 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -32,6 +32,7 @@ from esphome.__main__ import ( has_mqtt_ip_lookup, has_mqtt_logging, has_non_ip_address, + has_ota, has_resolvable_address, mqtt_get_ip, run_esphome, @@ -332,7 +333,9 @@ def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None: def test_choose_upload_log_host_with_ota_list() -> None: """Test with OTA as the only item in the list.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=["OTA"], @@ -345,7 +348,7 @@ def test_choose_upload_log_host_with_ota_list() -> None: @pytest.mark.usefixtures("mock_has_mqtt_logging") def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None: """Test with OTA list falling back to MQTT when no address.""" - setup_core(config={CONF_OTA: {}, "mqtt": {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], "mqtt": {}}) result = choose_upload_log_host( default=["OTA"], @@ -408,7 +411,9 @@ def test_choose_upload_log_host_with_serial_device_with_ports( def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None: """Test OTA device when OTA is configured.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default="OTA", @@ -475,7 +480,9 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None: @pytest.mark.usefixtures("mock_choose_prompt") def test_choose_upload_log_host_multiple_devices() -> None: """Test with multiple devices including special identifiers.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")] @@ -514,7 +521,9 @@ def test_choose_upload_log_host_no_defaults_with_serial_ports( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_no_defaults_with_ota() -> None: """Test interactive mode with OTA option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) with patch( "esphome.__main__.choose_prompt", return_value="192.168.1.100" @@ -575,7 +584,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -604,7 +617,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -632,7 +649,9 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_check_default_matches() -> None: """Test when check_default matches an available option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=None, @@ -704,7 +723,10 @@ def test_choose_upload_log_host_mixed_resolved_unresolved() -> None: def test_choose_upload_log_host_ota_both_conditions() -> None: """Test OTA device when both OTA and API are configured and enabled.""" - setup_core(config={CONF_OTA: {}, CONF_API: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}}, + address="192.168.1.100", + ) result = choose_upload_log_host( default="OTA", @@ -719,7 +741,7 @@ def test_choose_upload_log_host_ota_ip_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -744,7 +766,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -769,7 +791,7 @@ def test_choose_upload_log_host_ota_ip_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -794,7 +816,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -817,7 +839,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: @pytest.mark.usefixtures("mock_no_mqtt_logging") def test_choose_upload_log_host_no_address_with_ota_config() -> None: """Test OTA device when OTA is configured but no address is set.""" - setup_core(config={CONF_OTA: {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) with pytest.raises( EsphomeError, match="All specified devices .* could not be resolved" @@ -1532,10 +1554,43 @@ def test_has_mqtt() -> None: assert has_mqtt() is False # Test with other components but no MQTT - setup_core(config={CONF_API: {}, CONF_OTA: {}}) + setup_core(config={CONF_API: {}, CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) assert has_mqtt() is False +def test_has_ota() -> None: + """Test has_ota function. + + The has_ota function should only return True when OTA is configured + with platform: esphome, not when only platform: http_request is configured. + This is because CLI OTA upload only works with the esphome platform. + """ + # Test with OTA esphome platform configured + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) + assert has_ota() is True + + # Test with OTA http_request platform only (should return False) + # This is the bug scenario from issue #13783 + setup_core(config={CONF_OTA: [{CONF_PLATFORM: "http_request"}]}) + assert has_ota() is False + + # Test without OTA configured + setup_core(config={}) + assert has_ota() is False + + # Test with multiple OTA platforms including esphome + setup_core( + config={ + CONF_OTA: [{CONF_PLATFORM: "http_request"}, {CONF_PLATFORM: CONF_ESPHOME}] + } + ) + assert has_ota() is True + + # Test with empty OTA list + setup_core(config={CONF_OTA: []}) + assert has_ota() is False + + def test_get_port_type() -> None: """Test get_port_type function.""" From fef5d3f88f3f0fff512e39ac3e956bf84c2e958f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:10:22 -0500 Subject: [PATCH 106/251] [rdm6300] Add ID-20LA compatibility by skipping CR/LF bytes (#13779) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/components/rdm6300/rdm6300.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/rdm6300/rdm6300.cpp b/esphome/components/rdm6300/rdm6300.cpp index f9b6126c4b..04065982f6 100644 --- a/esphome/components/rdm6300/rdm6300.cpp +++ b/esphome/components/rdm6300/rdm6300.cpp @@ -34,6 +34,8 @@ void rdm6300::RDM6300Component::loop() { this->buffer_[this->read_state_ / 2] += value; } this->read_state_++; + } else if (data == 0x0D || data == 0x0A) { + // Skip CR/LF bytes (ID-20LA compatibility) } else if (data != RDM6300_END_BYTE) { ESP_LOGW(TAG, "Invalid end byte from RDM6300!"); this->read_state_ = RDM6300_STATE_WAITING_FOR_START; From 112a2c5d9276cdf61743ccfd18c404e9c9658092 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:11:08 +1100 Subject: [PATCH 107/251] [const] Move some constants to common (#13788) --- esphome/components/const/__init__.py | 6 ++++++ esphome/components/daly_bms/sensor.py | 4 +--- esphome/components/ina2xx_base/__init__.py | 4 ++-- esphome/components/pipsolar/sensor/__init__.py | 4 +--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index fcfafa0c1a..3201db5dfd 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -17,3 +17,9 @@ CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" + +ICON_CURRENT_DC = "mdi:current-dc" +ICON_SOLAR_PANEL = "mdi:solar-panel" +ICON_SOLAR_POWER = "mdi:solar-power" + +UNIT_AMPERE_HOUR = "Ah" diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 560bcef64e..aa92cfa86a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import ICON_CURRENT_DC, UNIT_AMPERE_HOUR import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_LEVEL, @@ -55,14 +56,11 @@ CONF_CELL_15_VOLTAGE = "cell_15_voltage" CONF_CELL_16_VOLTAGE = "cell_16_voltage" CONF_CELL_17_VOLTAGE = "cell_17_voltage" CONF_CELL_18_VOLTAGE = "cell_18_voltage" -ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" ICON_THERMOMETER_CHEVRON_DOWN = "mdi:thermometer-chevron-down" ICON_CAR_BATTERY = "mdi:car-battery" -UNIT_AMPERE_HOUR = "Ah" - TYPES = [ CONF_VOLTAGE, CONF_CURRENT, diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index ce68ad2726..15e2faba07 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import UNIT_AMPERE_HOUR import esphome.config_validation as cv from esphome.const import ( CONF_BUS_VOLTAGE, @@ -36,7 +37,6 @@ CONF_CHARGE_COULOMBS = "charge_coulombs" CONF_ENERGY_JOULES = "energy_joules" CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" CONF_RESET_ON_BOOT = "reset_on_boot" -UNIT_AMPERE_HOURS = "Ah" UNIT_COULOMB = "C" UNIT_JOULE = "J" UNIT_MILLIVOLT = "mV" @@ -180,7 +180,7 @@ INA2XX_SCHEMA = cv.Schema( ), cv.Optional(CONF_CHARGE): cv.maybe_simple_value( sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE_HOURS, + unit_of_measurement=UNIT_AMPERE_HOUR, accuracy_decimals=8, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index d08a877b55..8d3ba10d62 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import ICON_CURRENT_DC, ICON_SOLAR_PANEL, ICON_SOLAR_POWER import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_VOLTAGE, @@ -29,9 +30,6 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA DEPENDENCIES = ["uart"] -ICON_SOLAR_POWER = "mdi:solar-power" -ICON_SOLAR_PANEL = "mdi:solar-panel" -ICON_CURRENT_DC = "mdi:current-dc" # QPIRI sensors CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" From 7afd0eb1aa947cc5b081849eb6bc090763068127 Mon Sep 17 00:00:00 2001 From: Andrew Rankin <andrew@eiknet.com> Date: Fri, 6 Feb 2026 06:36:55 -0500 Subject: [PATCH 108/251] [esp32_ble] include sdkconfig.h before ESP-Hosted preprocessor guards (#13787) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- .../components/esp32_ble/ble_advertising.h | 9 ----- .../components/esp32_ble_beacon/__init__.py | 11 +++-- .../esp32_ble_beacon/esp32_ble_beacon.cpp | 10 ++++- .../esp32_ble_beacon/esp32_ble_beacon.h | 4 ++ esphome/config_validation.py | 11 +++++ .../config_validation/test_config.py | 40 ++++++++++++++++++- .../esp32_ble/test.esp32-p4-idf.yaml | 8 ++++ .../esp32_ble_beacon/test.esp32-p4-idf.yaml | 7 ++++ .../esp32_ble_client/test.esp32-p4-idf.yaml | 6 +++ .../esp32_ble_server/test.esp32-p4-idf.yaml | 4 ++ .../esp32_ble_tracker/test.esp32-p4-idf.yaml | 7 ++++ .../common/ble/esp32-p4-idf.yaml | 21 ++++++++++ 12 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 tests/components/esp32_ble/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_client/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_server/test.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml create mode 100644 tests/test_build_components/common/ble/esp32-p4-idf.yaml diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index d7f1eeac9d..3cfa6f548a 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -10,20 +10,11 @@ #ifdef USE_ESP32 #ifdef USE_ESP32_BLE_ADVERTISING -#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#include <esp_bt.h> -#endif #include <esp_gap_ble_api.h> #include <esp_gatts_api.h> namespace esphome::esp32_ble { -using raw_adv_data_t = struct { - uint8_t *data; - size_t length; - esp_power_level_t power_level; -}; - class ESPBTUUID; class BLEAdvertising { diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index ba5ae4331c..04c783980d 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -53,8 +53,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range( min=-128, max=0 ), - cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( - cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True) + cv.OnlyWithout(CONF_TX_POWER, "esp32_hosted", default="3dBm"): cv.All( + cv.conflicts_with_component("esp32_hosted"), + cv.decibel, + cv.enum(esp32_ble.TX_POWER_LEVELS, int=True), ), } ).extend(cv.COMPONENT_SCHEMA), @@ -82,7 +84,10 @@ async def to_code(config): cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL])) cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL])) cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) - cg.add(var.set_tx_power(config[CONF_TX_POWER])) + + # TX power control only available on native Bluetooth (not ESP-Hosted) + if CONF_TX_POWER in config: + cg.add(var.set_tx_power(config[CONF_TX_POWER])) cg.add_define("USE_ESP32_BLE_ADVERTISING") diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index f2aa7e762e..093273b399 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -36,11 +36,16 @@ void ESP32BLEBeacon::dump_config() { } } *bpos = '\0'; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" ", TX Power: %ddBm", uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, (this->tx_power_ * 3) - 12); +#else + ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d", + uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_); +#endif } float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } @@ -74,11 +79,14 @@ void ESP32BLEBeacon::on_advertise_() { ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_); ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(this->measured_power_); + esp_err_t err; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGD(TAG, "Setting BLE TX power"); - esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); } +#endif err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err)); diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 05afdc7379..7a0424f3aa 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -48,7 +48,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented void set_min_interval(uint16_t val) { this->min_interval_ = val; } void set_max_interval(uint16_t val) { this->max_interval_ = val; } void set_measured_power(int8_t val) { this->measured_power_ = val; } +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } +#endif void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; protected: @@ -60,7 +62,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented uint16_t min_interval_{}; uint16_t max_interval_{}; int8_t measured_power_{}; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID esp_power_level_t tx_power_{}; +#endif esp_ble_adv_params_t ble_adv_params_; bool advertising_{false}; }; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 55e13a7050..a9d1a72e5a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1403,6 +1403,17 @@ def requires_component(comp): return validator +def conflicts_with_component(comp): + """Validate that this option cannot be specified when the component `comp` is loaded.""" + + def validator(value): + if comp in CORE.loaded_integrations: + raise Invalid(f"This option is not compatible with component {comp}") + return value + + return validator + + uint8_t = int_range(min=0, max=255) uint16_t = int_range(min=0, max=65535) uint32_t = int_range(min=0, max=4294967295) diff --git a/tests/component_tests/config_validation/test_config.py b/tests/component_tests/config_validation/test_config.py index 1a9b9bc1f3..25d85d333b 100644 --- a/tests/component_tests/config_validation/test_config.py +++ b/tests/component_tests/config_validation/test_config.py @@ -1,10 +1,14 @@ """ -Test schema.extend functionality in esphome.config_validation. +Test config_validation functionality in esphome.config_validation. """ from typing import Any +import pytest +from voluptuous import Invalid + import esphome.config_validation as cv +from esphome.core import CORE def test_config_extend() -> None: @@ -49,3 +53,37 @@ def test_config_extend() -> None: assert validated["key2"] == "initial_value2" assert validated["extra_1"] == "value1" assert validated["extra_2"] == "value2" + + +def test_requires_component_passes_when_loaded() -> None: + """Test requires_component passes when the required component is loaded.""" + CORE.loaded_integrations.update({"wifi", "logger"}) + validator = cv.requires_component("wifi") + result = validator("test_value") + assert result == "test_value" + + +def test_requires_component_fails_when_not_loaded() -> None: + """Test requires_component raises Invalid when the required component is not loaded.""" + CORE.loaded_integrations.add("logger") + validator = cv.requires_component("wifi") + with pytest.raises(Invalid) as exc_info: + validator("test_value") + assert "requires component wifi" in str(exc_info.value) + + +def test_conflicts_with_component_passes_when_not_loaded() -> None: + """Test conflicts_with_component passes when the conflicting component is not loaded.""" + CORE.loaded_integrations.update({"wifi", "logger"}) + validator = cv.conflicts_with_component("esp32_hosted") + result = validator("test_value") + assert result == "test_value" + + +def test_conflicts_with_component_fails_when_loaded() -> None: + """Test conflicts_with_component raises Invalid when the conflicting component is loaded.""" + CORE.loaded_integrations.update({"wifi", "esp32_hosted"}) + validator = cv.conflicts_with_component("esp32_hosted") + with pytest.raises(Invalid) as exc_info: + validator("test_value") + assert "not compatible with component esp32_hosted" in str(exc_info.value) diff --git a/tests/components/esp32_ble/test.esp32-p4-idf.yaml b/tests/components/esp32_ble/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..4eeb7c2f18 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-p4-idf.yaml @@ -0,0 +1,8 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false diff --git a/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..b6e1845c50 --- /dev/null +++ b/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml @@ -0,0 +1,7 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +# tx_power is not supported on ESP-Hosted platforms +esp32_ble_beacon: + type: iBeacon + uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' diff --git a/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..e2496dd1ce --- /dev/null +++ b/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml @@ -0,0 +1,6 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: blec diff --git a/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..f202161cf3 --- /dev/null +++ b/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..d0f1e94a97 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml @@ -0,0 +1,7 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9 diff --git a/tests/test_build_components/common/ble/esp32-p4-idf.yaml b/tests/test_build_components/common/ble/esp32-p4-idf.yaml new file mode 100644 index 0000000000..dce923078a --- /dev/null +++ b/tests/test_build_components/common/ble/esp32-p4-idf.yaml @@ -0,0 +1,21 @@ +# Common BLE tracker configuration for ESP32-P4 IDF tests +# ESP32-P4 requires ESP-Hosted for Bluetooth via external coprocessor +# BLE client components share this tracker infrastructure +# Each component defines its own ble_client with unique MAC address + +esp32_hosted: + active_high: true + variant: ESP32C6 + reset_pin: GPIO54 + cmd_pin: GPIO19 + clk_pin: GPIO18 + d0_pin: GPIO14 + d1_pin: GPIO15 + d2_pin: GPIO16 + d3_pin: GPIO17 + +esp32_ble_tracker: + scan_parameters: + interval: 1100ms + window: 1100ms + active: true From e4ad2082bcc22d60244426e6b9fbf0dde51e73bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 15:26:16 +0100 Subject: [PATCH 109/251] [core] Add PROGMEM_STRING_TABLE macro for flash-optimized string lookups (#13659) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/cover/cover.cpp | 14 +--- .../components/light/light_json_schema.cpp | 37 +++------ esphome/components/logger/logger.cpp | 34 +++----- esphome/components/sensor/sensor.cpp | 20 ++--- esphome/components/sensor/sensor.h | 1 + esphome/components/valve/valve.cpp | 14 +--- .../wifi/wifi_component_esp8266.cpp | 43 ++++------ esphome/core/progmem.h | 83 +++++++++++++++++++ 8 files changed, 137 insertions(+), 109 deletions(-) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 37cb908d9f..0589aa2379 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -19,17 +19,11 @@ const LogString *cover_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *cover_operation_to_str(CoverOperation op) { - switch (op) { - case COVER_OPERATION_IDLE: - return LOG_STR("IDLE"); - case COVER_OPERATION_OPENING: - return LOG_STR("OPENING"); - case COVER_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + return CoverOperationStrings::get_log_str(static_cast<uint8_t>(op), CoverOperationStrings::LAST_INDEX); } Cover::Cover() : position{COVER_OPEN} {} diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 631f59221f..aaa1176f9f 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -9,32 +9,19 @@ namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Get JSON string for color mode. -// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would -// generate a large jump table. Converting to bit index (0-9) allows a compact switch. +// Color mode JSON strings - packed into flash with compile-time generated offsets. +// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff"). +PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", + "rgbww"); + +// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0). +// Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266. static ProgmemStr get_color_mode_json_str(ColorMode mode) { - switch (ColorModeBitPolicy::to_bit(mode)) { - case 1: - return ESPHOME_F("onoff"); - case 2: - return ESPHOME_F("brightness"); - case 3: - return ESPHOME_F("white"); - case 4: - return ESPHOME_F("color_temp"); - case 5: - return ESPHOME_F("cwww"); - case 6: - return ESPHOME_F("rgb"); - case 7: - return ESPHOME_F("rgbw"); - case 8: - return ESPHOME_F("rgbct"); - case 9: - return ESPHOME_F("rgbww"); - default: - return nullptr; - } + unsigned bit = ColorModeBitPolicy::to_bit(mode); + if (bit == 0) + return nullptr; + // bit is 1-9 for valid modes, so bit-1 is always valid (0-8). LAST_INDEX fallback never used. + return ColorModeStrings::get_progmem_str(bit - 1, ColorModeStrings::LAST_INDEX); } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 54b5670016..4cbd4f1bf1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -4,6 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::logger { @@ -241,34 +242,20 @@ UARTSelection Logger::get_uart() const { return this->uart_; } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } -#ifdef USE_STORE_LOG_STR_IN_FLASH -// ESP8266: PSTR() cannot be used in array initializers, so we need to declare -// each string separately as a global constant first -static const char LOG_LEVEL_NONE[] PROGMEM = "NONE"; -static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR"; -static const char LOG_LEVEL_WARN[] PROGMEM = "WARN"; -static const char LOG_LEVEL_INFO[] PROGMEM = "INFO"; -static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG"; -static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG"; -static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE"; -static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE"; +// Log level strings - packed into flash on ESP8266, indexed by log level (0-7) +PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); -static const LogString *const LOG_LEVELS[] = { - reinterpret_cast<const LogString *>(LOG_LEVEL_NONE), reinterpret_cast<const LogString *>(LOG_LEVEL_ERROR), - reinterpret_cast<const LogString *>(LOG_LEVEL_WARN), reinterpret_cast<const LogString *>(LOG_LEVEL_INFO), - reinterpret_cast<const LogString *>(LOG_LEVEL_CONFIG), reinterpret_cast<const LogString *>(LOG_LEVEL_DEBUG), - reinterpret_cast<const LogString *>(LOG_LEVEL_VERBOSE), reinterpret_cast<const LogString *>(LOG_LEVEL_VERY_VERBOSE), -}; -#else -static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; -#endif +static const LogString *get_log_level_str(uint8_t level) { + return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX); +} void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_])); + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)), + LOG_STR_ARG(get_log_level_str(this->current_level_))); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -287,7 +274,7 @@ void Logger::dump_config() { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif } @@ -295,7 +282,8 @@ void Logger::dump_config() { void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL))); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 3f2be02af2..ae2ee3e3d1 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::sensor { @@ -30,20 +31,13 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o } } +// State class strings indexed by StateClass enum (0-4): NONE, MEASUREMENT, TOTAL_INCREASING, TOTAL, MEASUREMENT_ANGLE +PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", "total", "measurement_angle"); +static_assert(StateClassStrings::COUNT == STATE_CLASS_LAST + 1, "StateClassStrings must match StateClass enum"); + const LogString *state_class_to_string(StateClass state_class) { - switch (state_class) { - case STATE_CLASS_MEASUREMENT: - return LOG_STR("measurement"); - case STATE_CLASS_TOTAL_INCREASING: - return LOG_STR("total_increasing"); - case STATE_CLASS_TOTAL: - return LOG_STR("total"); - case STATE_CLASS_MEASUREMENT_ANGLE: - return LOG_STR("measurement_angle"); - case STATE_CLASS_NONE: - default: - return LOG_STR(""); - } + // Fallback to index 0 (empty string for STATE_CLASS_NONE) if out of range + return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0); } Sensor::Sensor() : state(NAN), raw_state(NAN) {} diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index d9046020f6..f9a45cb1d0 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -32,6 +32,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, STATE_CLASS_MEASUREMENT_ANGLE = 4 }; +constexpr uint8_t STATE_CLASS_LAST = static_cast<uint8_t>(STATE_CLASS_MEASUREMENT_ANGLE); const LogString *state_class_to_string(StateClass state_class); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 607f614ef7..493ffd8da2 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -23,17 +23,11 @@ const LogString *valve_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Valve operation strings indexed by ValveOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(ValveOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *valve_operation_to_str(ValveOperation op) { - switch (op) { - case VALVE_OPERATION_IDLE: - return LOG_STR("IDLE"); - case VALVE_OPERATION_OPENING: - return LOG_STR("OPENING"); - case VALVE_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + return ValveOperationStrings::get_log_str(static_cast<uint8_t>(op), ValveOperationStrings::LAST_INDEX); } Valve::Valve() : position{VALVE_OPEN} {} diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c6bd40037d..0765fdc03b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -36,6 +36,7 @@ extern "C" { #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/util.h" namespace esphome::wifi { @@ -398,37 +399,23 @@ class WiFiMockClass : public ESP8266WiFiGenericClass { static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT }; +// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at last index +// Static asserts verify the SDK constants are contiguous as expected +static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4, + "AUTH_* constants are not contiguous"); +PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN"); + const LogString *get_auth_mode_str(uint8_t mode) { - switch (mode) { - case AUTH_OPEN: - return LOG_STR("OPEN"); - case AUTH_WEP: - return LOG_STR("WEP"); - case AUTH_WPA_PSK: - return LOG_STR("WPA PSK"); - case AUTH_WPA2_PSK: - return LOG_STR("WPA2 PSK"); - case AUTH_WPA_WPA2_PSK: - return LOG_STR("WPA/WPA2 PSK"); - default: - return LOG_STR("UNKNOWN"); - } -} -const LogString *get_op_mode_str(uint8_t mode) { - switch (mode) { - case WIFI_OFF: - return LOG_STR("OFF"); - case WIFI_STA: - return LOG_STR("STA"); - case WIFI_AP: - return LOG_STR("AP"); - case WIFI_AP_STA: - return LOG_STR("AP+STA"); - default: - return LOG_STR("UNKNOWN"); - } + return AuthModeStrings::get_log_str(mode, AuthModeStrings::LAST_INDEX); } +// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at last index +static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3, + "WIFI_* op mode constants are not contiguous"); +PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN"); + +const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); } + const LogString *get_disconnect_reason_str(uint8_t reason) { /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 4b897fb2de..6c6a5252cf 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -1,5 +1,11 @@ #pragma once +#include <array> +#include <cstddef> +#include <cstdint> + +#include "esphome/core/hal.h" // For PROGMEM definition + // Platform-agnostic macros for PROGMEM string handling // On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings // On other platforms: Use plain strings (no PROGMEM) @@ -32,3 +38,80 @@ using ProgmemStr = const __FlashStringHelper *; // Type for pointers to strings (no PROGMEM on non-ESP8266 platforms) using ProgmemStr = const char *; #endif + +namespace esphome { + +/// Helper for C++20 string literal template arguments +template<size_t N> struct FixedString { + char data[N]{}; + constexpr FixedString(const char (&str)[N]) { + for (size_t i = 0; i < N; ++i) + data[i] = str[i]; + } + constexpr size_t size() const { return N - 1; } // exclude null terminator +}; + +/// Compile-time string table that packs strings into a single blob with offset lookup. +/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266. +/// +/// Example: +/// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz"); +/// ProgmemStr str = MyStrings::get_progmem_str(idx, MyStrings::LAST_INDEX); // For ArduinoJson +/// const LogString *log_str = MyStrings::get_log_str(idx, MyStrings::LAST_INDEX); // For logging +/// +template<FixedString... Strs> struct ProgmemStringTable { + static constexpr size_t COUNT = sizeof...(Strs); + static constexpr size_t BLOB_SIZE = (0 + ... + (Strs.size() + 1)); + + /// Generate packed string blob at compile time + static constexpr auto make_blob() { + std::array<char, BLOB_SIZE> result{}; + size_t pos = 0; + auto copy = [&](const auto &str) { + for (size_t i = 0; i <= str.size(); ++i) + result[pos++] = str.data[i]; + }; + (copy(Strs), ...); + return result; + } + + /// Generate offset table at compile time (uint8_t limits blob to 255 bytes) + static constexpr auto make_offsets() { + static_assert(COUNT > 0, "PROGMEM_STRING_TABLE must contain at least one string"); + static_assert(COUNT <= 255, "PROGMEM_STRING_TABLE supports at most 255 strings with uint8_t indices"); + static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings"); + std::array<uint8_t, COUNT> result{}; + size_t pos = 0, idx = 0; + ((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...); + return result; + } +}; + +// Forward declaration for LogString (defined in log.h) +struct LogString; + +/// Instantiate a ProgmemStringTable with PROGMEM storage. +/// Creates: Name::get_progmem_str(idx, fallback), Name::get_log_str(idx, fallback) +/// If idx >= COUNT, returns string at fallback. Use LAST_INDEX for common patterns. +#define PROGMEM_STRING_TABLE(Name, ...) \ + struct Name { \ + using Table = ::esphome::ProgmemStringTable<__VA_ARGS__>; \ + static constexpr size_t COUNT = Table::COUNT; \ + static constexpr uint8_t LAST_INDEX = COUNT - 1; \ + static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ + static constexpr auto BLOB PROGMEM = Table::make_blob(); \ + static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ + static const char *get_(uint8_t idx, uint8_t fallback) { \ + if (idx >= COUNT) \ + idx = fallback; \ + return &BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]; \ + } \ + static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \ + return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \ + } \ + static const ::esphome::LogString *get_log_str(uint8_t idx, uint8_t fallback) { \ + return reinterpret_cast<const ::esphome::LogString *>(get_(idx, fallback)); \ + } \ + } + +} // namespace esphome From c3622ef7fb18aed84c1b59357a37945955bb7086 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 15:52:41 +0100 Subject: [PATCH 110/251] [http_request] Fix chunked transfer encoding on Arduino platforms (#13790) --- .../http_request/http_request_arduino.cpp | 193 ++++++++++++++++-- .../http_request/http_request_arduino.h | 18 ++ .../http_request/ota/ota_http_request.cpp | 6 +- esphome/core/helpers.cpp | 2 +- esphome/core/helpers.h | 5 +- 5 files changed, 198 insertions(+), 26 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 2f12b58766..aee1f651bf 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -133,20 +133,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur // HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length). // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). - // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the - // early return check (bytes_read_ >= content_length) will never trigger. - // - // TODO: Chunked transfer encoding is NOT properly supported on Arduino. - // The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where - // esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr() - // returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead - // of decoded content. This wasn't noticed because requests would complete and payloads - // were only examined on IDF. The long transfer times were also masked by the misleading - // "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues: - // 1. Response body is corrupted - contains chunk size headers mixed with data - // 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout - // The proper fix would be to use getString() for chunked responses, which decodes chunks - // internally, but this buffers the entire response in memory. + // The read() method uses a chunked transfer encoding decoder (read_chunked_) to strip + // chunk framing and deliver only decoded content. When the final 0-size chunk is received, + // is_chunked_ is cleared and content_length is set to the actual decoded size, so + // is_read_complete() returns true and callers exit their read loops correctly. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; @@ -174,6 +164,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur // > 0: bytes read // 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF! // < 0: error/connection closed <-- connection closed returns -1, not 0 +// +// For chunked transfer encoding, read_chunked_() decodes chunk framing and delivers +// only the payload data. When the final 0-size chunk is received, it clears is_chunked_ +// and sets content_length = bytes_read_ so is_read_complete() returns true. int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); @@ -184,24 +178,42 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return HTTP_ERROR_CONNECTION_CLOSED; } + if (this->is_chunked_) { + int result = this->read_chunked_(buf, max_len, stream_ptr); + this->duration_ms += (millis() - start); + if (result > 0) { + return result; + } + // result <= 0: check for completion or errors + if (this->is_read_complete()) { + return 0; // Chunked transfer complete (final 0-size chunk received) + } + if (result < 0) { + return result; // Stream error during chunk decoding + } + // read_chunked_ returned 0: no data was available (available() was 0). + // This happens when the TCP buffer is empty - either more data is in flight, + // or the connection dropped. Arduino's connected() returns false only when + // both the remote has closed AND the receive buffer is empty, so any buffered + // data is fully drained before we report the drop. + if (!stream_ptr->connected()) { + return HTTP_ERROR_CONNECTION_CLOSED; + } + return 0; // No data yet, caller should retry + } + + // Non-chunked path int available_data = stream_ptr->available(); - // For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when - // cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read. size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data)); if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (non-chunked only) - // For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false if (this->is_read_complete()) { return 0; // All content read successfully } - // No data available - check if connection is still open - // For chunked encoding, !connected() after reading means EOF (all chunks received) - // For known content_length with bytes_read_ < content_length, it means connection dropped if (!stream_ptr->connected()) { - return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked + return HTTP_ERROR_CONNECTION_CLOSED; } return 0; // No data yet, caller should retry } @@ -215,6 +227,143 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return read_len; } +void HttpContainerArduino::chunk_header_complete_() { + if (this->chunk_remaining_ == 0) { + this->chunk_state_ = ChunkedState::CHUNK_TRAILER; + this->chunk_remaining_ = 1; // repurpose as at-start-of-line flag + } else { + this->chunk_state_ = ChunkedState::CHUNK_DATA; + } +} + +// Chunked transfer encoding decoder +// +// On Arduino, getStreamPtr() returns raw TCP data. For chunked responses, this includes +// chunk framing (size headers, CRLF delimiters) mixed with payload data. This decoder +// strips the framing and delivers only decoded content to the caller. +// +// Chunk format (RFC 9112 Section 7.1): +// <hex-size>[;extension]\r\n +// <data bytes>\r\n +// ... +// 0\r\n +// [trailer-field\r\n]* +// \r\n +// +// Non-blocking: only processes bytes already in the TCP receive buffer. +// State (chunk_state_, chunk_remaining_) is preserved between calls, so partial +// chunk headers or split \r\n sequences resume correctly on the next call. +// Framing bytes (hex sizes, \r\n) may be consumed without producing output; +// the caller sees 0 and retries via the normal read timeout logic. +// +// WiFiClient::read() returns -1 on error despite available() > 0 (connection reset +// between check and read). On any stream error (c < 0 or readBytes <= 0), we return +// already-decoded data if any; otherwise HTTP_ERROR_CONNECTION_CLOSED. The error +// will surface again on the next call since the stream stays broken. +// +// Returns: > 0 decoded bytes, 0 no data available, < 0 error +int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream) { + int total_decoded = 0; + + while (total_decoded < (int) max_len && this->chunk_state_ != ChunkedState::COMPLETE) { + // Non-blocking: only process what's already buffered + if (stream->available() == 0) + break; + + // CHUNK_DATA reads multiple bytes; handle before the single-byte switch + if (this->chunk_state_ == ChunkedState::CHUNK_DATA) { + // Only read what's available, what fits in buf, and what remains in this chunk + size_t to_read = + std::min({max_len - (size_t) total_decoded, this->chunk_remaining_, (size_t) stream->available()}); + if (to_read == 0) + break; + App.feed_wdt(); + int read_len = stream->readBytes(buf + total_decoded, to_read); + if (read_len <= 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + total_decoded += read_len; + this->chunk_remaining_ -= read_len; + this->bytes_read_ += read_len; + if (this->chunk_remaining_ == 0) + this->chunk_state_ = ChunkedState::CHUNK_DATA_TRAIL; + continue; + } + + // All other states consume a single byte + int c = stream->read(); + if (c < 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + + switch (this->chunk_state_) { + // Parse hex chunk size, one byte at a time: "<hex>[;ext]\r\n" + // Note: if no hex digits are parsed (e.g., bare \r\n), chunk_remaining_ stays 0 + // and is treated as the final chunk. This is intentionally lenient — on embedded + // devices, rejecting malformed framing is less useful than terminating cleanly. + // Overflow of chunk_remaining_ from extremely long hex strings (>8 digits on + // 32-bit) is not checked; >4GB chunks are unrealistic on embedded targets and + // would simply cause fewer bytes to be read from that chunk. + case ChunkedState::CHUNK_HEADER: + if (c == '\n') { + // \n terminates the size line; chunk_remaining_ == 0 means last chunk + this->chunk_header_complete_(); + } else { + uint8_t hex = parse_hex_char(c); + if (hex != INVALID_HEX_CHAR) { + this->chunk_remaining_ = (this->chunk_remaining_ << 4) | hex; + } else if (c != '\r') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER_EXT; // ';' starts extension, skip to \n + } + } + break; + + // Skip chunk extension bytes until \n (e.g., ";name=value\r\n") + case ChunkedState::CHUNK_HEADER_EXT: + if (c == '\n') { + this->chunk_header_complete_(); + } + break; + + // Consume \r\n trailing each chunk's data + case ChunkedState::CHUNK_DATA_TRAIL: + if (c == '\n') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER; + this->chunk_remaining_ = 0; // reset for next chunk's hex accumulation + } + // else: \r is consumed silently, next iteration gets \n + break; + + // Consume optional trailer headers and terminating empty line after final chunk. + // Per RFC 9112 Section 7.1: "0\r\n" is followed by optional "field\r\n" lines + // and a final "\r\n". chunk_remaining_ is repurposed as a flag: 1 = at start + // of line (may be the empty terminator), 0 = mid-line (reading a trailer field). + case ChunkedState::CHUNK_TRAILER: + if (c == '\n') { + if (this->chunk_remaining_ != 0) { + this->chunk_state_ = ChunkedState::COMPLETE; // Empty line terminates trailers + } else { + this->chunk_remaining_ = 1; // End of trailer field, at start of next line + } + } else if (c != '\r') { + this->chunk_remaining_ = 0; // Non-CRLF char: reading a trailer field + } + // \r doesn't change the flag — it's part of \r\n line endings + break; + + default: + break; + } + + if (this->chunk_state_ == ChunkedState::COMPLETE) { + // Clear chunked flag and set content_length to actual decoded size so + // is_read_complete() returns true and callers exit their read loops + this->is_chunked_ = false; + this->content_length = this->bytes_read_; + } + } + + return total_decoded; +} + void HttpContainerArduino::end() { watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); this->client_.end(); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d9b5af9d81..a1084b12d5 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -18,6 +18,17 @@ namespace esphome::http_request { class HttpRequestArduino; + +/// State machine for decoding chunked transfer encoding on Arduino +enum class ChunkedState : uint8_t { + CHUNK_HEADER, ///< Reading hex digits of chunk size + CHUNK_HEADER_EXT, ///< Skipping chunk extensions until \n + CHUNK_DATA, ///< Reading chunk data bytes + CHUNK_DATA_TRAIL, ///< Skipping \r\n after chunk data + CHUNK_TRAILER, ///< Consuming trailer headers after final 0-size chunk + COMPLETE, ///< Finished: final chunk and trailers consumed +}; + class HttpContainerArduino : public HttpContainer { public: int read(uint8_t *buf, size_t max_len) override; @@ -26,6 +37,13 @@ class HttpContainerArduino : public HttpContainer { protected: friend class HttpRequestArduino; HTTPClient client_{}; + + /// Decode chunked transfer encoding from the raw stream + int read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream); + /// Transition from chunk header to data or trailer based on parsed size + void chunk_header_complete_(); + ChunkedState chunk_state_{ChunkedState::CHUNK_HEADER}; + size_t chunk_remaining_{0}; ///< Bytes remaining in current chunk }; class HttpRequestArduino : public HttpRequestComponent { diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8f4ecfab2d..882def4d7f 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -133,8 +133,10 @@ uint8_t OtaHttpRequestComponent::do_ota_() { auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; - // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, - // but this is defensive code in case chunked transfer encoding support is added for OTA in the future. + // For non-chunked responses, COMPLETE is unreachable (loop condition checks bytes_read < content_length). + // For chunked responses, the decoder sets content_length = bytes_read when the final chunk arrives, + // which causes the loop condition to terminate. But COMPLETE can still be returned if the decoder + // finishes mid-read, so this is needed for correctness. if (result == HttpReadLoopResult::COMPLETE) break; if (result != HttpReadLoopResult::DATA) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1a5d22f8d8..c2f7f67d9a 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -295,7 +295,7 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { size_t chars = std::min(length, 2 * count); for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) { uint8_t val = parse_hex_char(*str); - if (val > 15) + if (val == INVALID_HEX_CHAR) return 0; data[i >> 1] = (i & 1) ? data[i >> 1] | val : val << 4; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9c7060cd1d..f7de34b6d5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -874,6 +874,9 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> optional< } /// Parse a hex character to its nibble value (0-15), returns 255 on invalid input +/// Returned by parse_hex_char() for non-hex characters. +static constexpr uint8_t INVALID_HEX_CHAR = 255; + constexpr uint8_t parse_hex_char(char c) { if (c >= '0' && c <= '9') return c - '0'; @@ -881,7 +884,7 @@ constexpr uint8_t parse_hex_char(char c) { return c - 'A' + 10; if (c >= 'a' && c <= 'f') return c - 'a' + 10; - return 255; + return INVALID_HEX_CHAR; } /// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) From ec477801cab5ac6d9b91a0359af640eeeab34086 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:23:19 +0100 Subject: [PATCH 111/251] [wifi] Defer ESP8266 WiFi listener callbacks from system context to main loop (#13789) --- esphome/components/wifi/wifi_component.cpp | 35 ++++++++ esphome/components/wifi/wifi_component.h | 79 ++++++++++++------- .../wifi/wifi_component_esp8266.cpp | 60 +++++++------- .../wifi/wifi_component_esp_idf.cpp | 21 ++--- .../wifi/wifi_component_libretiny.cpp | 21 ++--- .../components/wifi/wifi_component_pico_w.cpp | 17 +--- 6 files changed, 132 insertions(+), 101 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e9b78c9225..0fe98162f3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1470,6 +1470,14 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->notify_connect_state_listeners_(); #endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) + // On ESP8266, GOT_IP event may not fire for static IP configurations, + // so notify IP state listeners here as a fallback. + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + this->notify_ip_state_listeners_(); + } +#endif + return; } @@ -1481,7 +1489,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { } if (this->error_from_callback_) { + // ESP8266: logging done in callback, listeners deferred via pending_.disconnect + // Other platforms: just log generic failure message +#ifndef USE_ESP8266 ESP_LOGW(TAG, "Connecting to network failed (callback)"); +#endif this->retry_connect(); return; } @@ -2202,8 +2214,31 @@ void WiFiComponent::notify_connect_state_listeners_() { listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); } } + +void WiFiComponent::notify_disconnect_state_listeners_() { + constexpr uint8_t empty_bssid[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), empty_bssid); + } +} #endif // USE_WIFI_CONNECT_STATE_LISTENERS +#ifdef USE_WIFI_IP_STATE_LISTENERS +void WiFiComponent::notify_ip_state_listeners_() { + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +} +#endif // USE_WIFI_IP_STATE_LISTENERS + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS +void WiFiComponent::notify_scan_results_listeners_() { + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +} +#endif // USE_WIFI_SCAN_RESULTS_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 98f339809a..58f19c184a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -594,6 +594,9 @@ class WiFiComponent : public Component { void connect_soon_(); void wifi_loop_(); +#ifdef USE_ESP8266 + void process_pending_callbacks_(); +#endif bool wifi_mode_(optional<bool> sta, optional<bool> ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); @@ -635,6 +638,16 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) void notify_connect_state_listeners_(); + /// Notify connect state listeners of disconnection + void notify_disconnect_state_listeners_(); +#endif +#ifdef USE_WIFI_IP_STATE_LISTENERS + /// Notify IP state listeners with current addresses + void notify_ip_state_listeners_(); +#endif +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + /// Notify scan results listeners with current scan results + void notify_scan_results_listeners_(); #endif #ifdef USE_ESP8266 @@ -658,13 +671,13 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif + // Large/pointer-aligned members first FixedVector<WiFiAP> sta_; std::vector<WiFiSTAPriority> sta_priorities_; wifi_scan_vector_t<WiFiScanResult> scan_result_; #ifdef USE_WIFI_AP WiFiAP ap_; #endif - float output_power_{NAN}; #ifdef USE_WIFI_IP_STATE_LISTENERS StaticVector<WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS> ip_state_listeners_; #endif @@ -681,6 +694,15 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; #endif +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes @@ -688,7 +710,8 @@ class WiFiComponent : public Component { static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; - // Group all 32-bit integers together + // 4-byte members + float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; @@ -697,7 +720,7 @@ class WiFiComponent : public Component { uint32_t ap_timeout_{}; #endif - // Group all 8-bit values together + // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; @@ -708,17 +731,39 @@ class WiFiComponent : public Component { // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; uint8_t roaming_attempts_{0}; - #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ + bool error_from_callback_{false}; + RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; + RoamingState roaming_state_{RoamingState::IDLE}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; +#endif - // Group all boolean values together + // Bools and bitfields + // Pending listener callbacks deferred from platform callbacks to main loop. + struct { +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Deferred until state machine reaches STA_CONNECTED so wifi.connected + // condition returns true in listener automations. + bool connect_state : 1; +#ifdef USE_ESP8266 + // ESP8266: also defer disconnect notification to main loop + bool disconnect : 1; +#endif +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) + bool got_ip : 1; +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_SCAN_RESULTS_LISTENERS) + bool scan_complete : 1; +#endif + } pending_{}; bool has_ap_{false}; #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; #endif - bool error_from_callback_{false}; bool scan_done_{false}; bool ap_setup_{false}; bool ap_started_{false}; @@ -733,32 +778,10 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started - RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; bool skip_cooldown_next_cycle_{false}; bool post_connect_roaming_{true}; // Enabled by default - RoamingState roaming_state_{RoamingState::IDLE}; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) - WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; bool is_high_performance_mode_{false}; - - SemaphoreHandle_t high_performance_semaphore_{nullptr}; -#endif - -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Pending listener notifications deferred until state machine reaches appropriate state. - // Listeners are notified after state transitions complete so conditions like - // wifi.connected return correct values in automations. - // Uses bitfields to minimize memory; more flags may be added as needed. - struct { - bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED - } pending_{}; -#endif - -#ifdef USE_WIFI_CONNECT_TRIGGER - Trigger<> connect_trigger_; -#endif -#ifdef USE_WIFI_DISCONNECT_TRIGGER - Trigger<> disconnect_trigger_; #endif private: diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0765fdc03b..6e2adcbf04 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -506,16 +506,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // Defer listener notification until state machine reaches STA_CONNECTED // This ensures wifi.connected condition returns true in listener automations global_wifi_component->pending_.connect_state = true; -#endif - // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here -#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) - if (const WiFiAP *config = global_wifi_component->get_selected_sta_(); - config && config->get_manual_ip().has_value()) { - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), - global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1)); - } - } #endif break; } @@ -534,16 +524,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; - // IMPORTANT: Set error flag BEFORE notifying listeners. - // This ensures is_connected() returns false during listener callbacks, - // which is critical for proper reconnection logic (e.g., roaming). global_wifi_component->error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Notify listeners AFTER setting error flag so they see correct state - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + global_wifi_component->pending_.disconnect = true; #endif break; } @@ -555,8 +538,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); - // we can't call retry_connect() from this context, so disconnect immediately - // and notify main thread with error_from_callback_ wifi_station_disconnect(); global_wifi_component->error_from_callback_ = true; } @@ -570,10 +551,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), - global_wifi_component->get_dns_address(1)); - } + // Defer listener callbacks to main loop - system context has limited stack + global_wifi_component->pending_.got_ip = true; #endif break; } @@ -785,9 +764,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { needs_full ? "" : " (filtered)"); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : global_wifi_component->scan_results_listeners_) { - listener->on_wifi_scan_results(global_wifi_component->scan_result_); - } + this->pending_.scan_complete = true; // Defer listener callbacks to main loop #endif } @@ -974,7 +951,34 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(&ip.gw); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); } -void WiFiComponent::wifi_loop_() {} +void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } + +void WiFiComponent::process_pending_callbacks_() { + // Process callbacks deferred from ESP8266 SDK system context (~2KB stack) + // to main loop context (full stack). Connect state listeners are handled + // by notify_connect_state_listeners_() in the shared state machine code. + +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + if (this->pending_.disconnect) { + this->pending_.disconnect = false; + this->notify_disconnect_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_IP_STATE_LISTENERS + if (this->pending_.got_ip) { + this->pending_.got_ip = false; + this->notify_ip_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + if (this->pending_.scan_complete) { + this->pending_.scan_complete = false; + this->notify_scan_results_listeners_(); + } +#endif +} } // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 22bf4be483..d74d083954 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -753,9 +753,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif @@ -779,10 +777,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connecting = false; error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -793,9 +788,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #if USE_NETWORK_IPV6 @@ -804,9 +797,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #endif /* USE_NETWORK_IPV6 */ @@ -883,9 +874,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 285a520ef5..5fd9d7663b 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -468,9 +468,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } #endif @@ -527,10 +525,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif break; } @@ -553,18 +548,14 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { ESP_LOGV(TAG, "Got IPv6"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } @@ -708,9 +699,7 @@ void WiFiComponent::wifi_scan_done_callback_() { needs_full ? "" : " (filtered)"); WiFi.scanDelete(); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1ce36c2d93..818ad1059c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -264,9 +264,7 @@ void WiFiComponent::wifi_loop_() { ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } @@ -290,9 +288,7 @@ void WiFiComponent::wifi_loop_() { #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_had_ip = true; - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif } else if (!is_connected && s_sta_was_connected) { @@ -301,10 +297,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } @@ -322,9 +315,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } } From 44f308502e130c1bc8f7c9643c4e8a3e270f17bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:37:02 +0100 Subject: [PATCH 112/251] [gpio] Convert interrupt_type_to_string to PROGMEM_STRING_TABLE (#13795) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../gpio/binary_sensor/gpio_binary_sensor.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 7a35596194..38ebbc90e4 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -1,5 +1,6 @@ #include "gpio_binary_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace gpio { @@ -7,17 +8,12 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +// Interrupt type strings indexed by edge-triggered InterruptType values: +// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0). +PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE"); + static const LogString *interrupt_type_to_string(gpio::InterruptType type) { - switch (type) { - case gpio::INTERRUPT_RISING_EDGE: - return LOG_STR("RISING_EDGE"); - case gpio::INTERRUPT_FALLING_EDGE: - return LOG_STR("FALLING_EDGE"); - case gpio::INTERRUPT_ANY_EDGE: - return LOG_STR("ANY_EDGE"); - default: - return LOG_STR("UNKNOWN"); - } + return InterruptTypeStrings::get_log_str(static_cast<uint8_t>(type), 0); } static const LogString *gpio_mode_to_string(bool use_interrupt) { From b7dc975331f944c14452542a1ed95c019872e856 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:37:19 +0100 Subject: [PATCH 113/251] [core] Convert entity string lookups to PROGMEM_STRING_TABLE (#13794) --- .../alarm_control_panel_state.cpp | 31 ++--- esphome/components/climate/climate_mode.cpp | 115 ++++-------------- esphome/components/fan/fan.cpp | 13 +- esphome/components/lock/lock.cpp | 20 +-- .../components/water_heater/water_heater.cpp | 24 +--- 5 files changed, 48 insertions(+), 155 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp index 862c620497..b8d246c861 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -1,32 +1,15 @@ #include "alarm_control_panel_state.h" +#include "esphome/core/progmem.h" namespace esphome::alarm_control_panel { +// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9) +PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT", + "ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN"); + const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { - switch (state) { - case ACP_STATE_DISARMED: - return LOG_STR("DISARMED"); - case ACP_STATE_ARMED_HOME: - return LOG_STR("ARMED_HOME"); - case ACP_STATE_ARMED_AWAY: - return LOG_STR("ARMED_AWAY"); - case ACP_STATE_ARMED_NIGHT: - return LOG_STR("ARMED_NIGHT"); - case ACP_STATE_ARMED_VACATION: - return LOG_STR("ARMED_VACATION"); - case ACP_STATE_ARMED_CUSTOM_BYPASS: - return LOG_STR("ARMED_CUSTOM_BYPASS"); - case ACP_STATE_PENDING: - return LOG_STR("PENDING"); - case ACP_STATE_ARMING: - return LOG_STR("ARMING"); - case ACP_STATE_DISARMING: - return LOG_STR("DISARMING"); - case ACP_STATE_TRIGGERED: - return LOG_STR("TRIGGERED"); - default: - return LOG_STR("UNKNOWN"); - } + return AlarmControlPanelStateStrings::get_log_str(static_cast<uint8_t>(state), + AlarmControlPanelStateStrings::LAST_INDEX); } } // namespace esphome::alarm_control_panel diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index b153ee0424..c4dd19d503 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -1,109 +1,44 @@ #include "climate_mode.h" +#include "esphome/core/progmem.h" namespace esphome::climate { +// Climate mode strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO +PROGMEM_STRING_TABLE(ClimateModeStrings, "OFF", "HEAT_COOL", "COOL", "HEAT", "FAN_ONLY", "DRY", "AUTO", "UNKNOWN"); + const LogString *climate_mode_to_string(ClimateMode mode) { - switch (mode) { - case CLIMATE_MODE_OFF: - return LOG_STR("OFF"); - case CLIMATE_MODE_HEAT_COOL: - return LOG_STR("HEAT_COOL"); - case CLIMATE_MODE_AUTO: - return LOG_STR("AUTO"); - case CLIMATE_MODE_COOL: - return LOG_STR("COOL"); - case CLIMATE_MODE_HEAT: - return LOG_STR("HEAT"); - case CLIMATE_MODE_FAN_ONLY: - return LOG_STR("FAN_ONLY"); - case CLIMATE_MODE_DRY: - return LOG_STR("DRY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateModeStrings::get_log_str(static_cast<uint8_t>(mode), ClimateModeStrings::LAST_INDEX); } + +// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN +PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN"); + const LogString *climate_action_to_string(ClimateAction action) { - switch (action) { - case CLIMATE_ACTION_OFF: - return LOG_STR("OFF"); - case CLIMATE_ACTION_COOLING: - return LOG_STR("COOLING"); - case CLIMATE_ACTION_HEATING: - return LOG_STR("HEATING"); - case CLIMATE_ACTION_IDLE: - return LOG_STR("IDLE"); - case CLIMATE_ACTION_DRYING: - return LOG_STR("DRYING"); - case CLIMATE_ACTION_FAN: - return LOG_STR("FAN"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateActionStrings::get_log_str(static_cast<uint8_t>(action), ClimateActionStrings::LAST_INDEX); } +// Climate fan mode strings indexed by ClimateFanMode enum (0-9): ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, +// DIFFUSE, QUIET +PROGMEM_STRING_TABLE(ClimateFanModeStrings, "ON", "OFF", "AUTO", "LOW", "MEDIUM", "HIGH", "MIDDLE", "FOCUS", "DIFFUSE", + "QUIET", "UNKNOWN"); + const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - return LOG_STR("ON"); - case climate::CLIMATE_FAN_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_FAN_AUTO: - return LOG_STR("AUTO"); - case climate::CLIMATE_FAN_LOW: - return LOG_STR("LOW"); - case climate::CLIMATE_FAN_MEDIUM: - return LOG_STR("MEDIUM"); - case climate::CLIMATE_FAN_HIGH: - return LOG_STR("HIGH"); - case climate::CLIMATE_FAN_MIDDLE: - return LOG_STR("MIDDLE"); - case climate::CLIMATE_FAN_FOCUS: - return LOG_STR("FOCUS"); - case climate::CLIMATE_FAN_DIFFUSE: - return LOG_STR("DIFFUSE"); - case climate::CLIMATE_FAN_QUIET: - return LOG_STR("QUIET"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateFanModeStrings::get_log_str(static_cast<uint8_t>(fan_mode), ClimateFanModeStrings::LAST_INDEX); } +// Climate swing mode strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL +PROGMEM_STRING_TABLE(ClimateSwingModeStrings, "OFF", "BOTH", "VERTICAL", "HORIZONTAL", "UNKNOWN"); + const LogString *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_SWING_BOTH: - return LOG_STR("BOTH"); - case climate::CLIMATE_SWING_VERTICAL: - return LOG_STR("VERTICAL"); - case climate::CLIMATE_SWING_HORIZONTAL: - return LOG_STR("HORIZONTAL"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateSwingModeStrings::get_log_str(static_cast<uint8_t>(swing_mode), ClimateSwingModeStrings::LAST_INDEX); } +// Climate preset strings indexed by ClimatePreset enum (0-7): NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY +PROGMEM_STRING_TABLE(ClimatePresetStrings, "NONE", "HOME", "AWAY", "BOOST", "COMFORT", "ECO", "SLEEP", "ACTIVITY", + "UNKNOWN"); + const LogString *climate_preset_to_string(ClimatePreset preset) { - switch (preset) { - case climate::CLIMATE_PRESET_NONE: - return LOG_STR("NONE"); - case climate::CLIMATE_PRESET_HOME: - return LOG_STR("HOME"); - case climate::CLIMATE_PRESET_ECO: - return LOG_STR("ECO"); - case climate::CLIMATE_PRESET_AWAY: - return LOG_STR("AWAY"); - case climate::CLIMATE_PRESET_BOOST: - return LOG_STR("BOOST"); - case climate::CLIMATE_PRESET_COMFORT: - return LOG_STR("COMFORT"); - case climate::CLIMATE_PRESET_SLEEP: - return LOG_STR("SLEEP"); - case climate::CLIMATE_PRESET_ACTIVITY: - return LOG_STR("ACTIVITY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimatePresetStrings::get_log_str(static_cast<uint8_t>(preset), ClimatePresetStrings::LAST_INDEX); } } // namespace esphome::climate diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index a983babe1c..d70a2940bc 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -2,21 +2,18 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace fan { static const char *const TAG = "fan"; +// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN +PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN"); + const LogString *fan_direction_to_string(FanDirection direction) { - switch (direction) { - case FanDirection::FORWARD: - return LOG_STR("FORWARD"); - case FanDirection::REVERSE: - return LOG_STR("REVERSE"); - default: - return LOG_STR("UNKNOWN"); - } + return FanDirectionStrings::get_log_str(static_cast<uint8_t>(direction), FanDirectionStrings::LAST_INDEX); } FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 4c61418256..939c84720b 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -8,22 +8,12 @@ namespace esphome::lock { static const char *const TAG = "lock"; +// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING +// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range +PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING"); + const LogString *lock_state_to_string(LockState state) { - switch (state) { - case LOCK_STATE_LOCKED: - return LOG_STR("LOCKED"); - case LOCK_STATE_UNLOCKED: - return LOG_STR("UNLOCKED"); - case LOCK_STATE_JAMMED: - return LOG_STR("JAMMED"); - case LOCK_STATE_LOCKING: - return LOG_STR("LOCKING"); - case LOCK_STATE_UNLOCKING: - return LOG_STR("UNLOCKING"); - case LOCK_STATE_NONE: - default: - return LOG_STR("UNKNOWN"); - } + return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0); } Lock::Lock() : state(LOCK_STATE_NONE) {} diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 286addf7db..e6d1562352 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -233,25 +233,13 @@ void WaterHeater::set_visual_target_temperature_step_override(float visual_targe } #endif +// Water heater mode strings indexed by WaterHeaterMode enum (0-6): OFF, ECO, ELECTRIC, PERFORMANCE, HIGH_DEMAND, +// HEAT_PUMP, GAS +PROGMEM_STRING_TABLE(WaterHeaterModeStrings, "OFF", "ECO", "ELECTRIC", "PERFORMANCE", "HIGH_DEMAND", "HEAT_PUMP", "GAS", + "UNKNOWN"); + const LogString *water_heater_mode_to_string(WaterHeaterMode mode) { - switch (mode) { - case WATER_HEATER_MODE_OFF: - return LOG_STR("OFF"); - case WATER_HEATER_MODE_ECO: - return LOG_STR("ECO"); - case WATER_HEATER_MODE_ELECTRIC: - return LOG_STR("ELECTRIC"); - case WATER_HEATER_MODE_PERFORMANCE: - return LOG_STR("PERFORMANCE"); - case WATER_HEATER_MODE_HIGH_DEMAND: - return LOG_STR("HIGH_DEMAND"); - case WATER_HEATER_MODE_HEAT_PUMP: - return LOG_STR("HEAT_PUMP"); - case WATER_HEATER_MODE_GAS: - return LOG_STR("GAS"); - default: - return LOG_STR("UNKNOWN"); - } + return WaterHeaterModeStrings::get_log_str(static_cast<uint8_t>(mode), WaterHeaterModeStrings::LAST_INDEX); } void WaterHeater::dump_traits_(const char *tag) { From 368ef5687b95476c48659d6d3618772992d729fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:37:41 +0100 Subject: [PATCH 114/251] [update] Move update_state_to_string to update component and convert to PROGMEM_STRING_TABLE (#13796) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/update/update_entity.cpp | 9 +++++++++ esphome/components/update/update_entity.h | 2 ++ esphome/components/web_server/web_server.cpp | 19 +++++-------------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 515e4c2c18..7edea2fe22 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -2,12 +2,21 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace update { static const char *const TAG = "update"; +// Update state strings indexed by UpdateState enum (0-3): UNKNOWN, NO UPDATE, UPDATE AVAILABLE, INSTALLING +PROGMEM_STRING_TABLE(UpdateStateStrings, "UNKNOWN", "NO UPDATE", "UPDATE AVAILABLE", "INSTALLING"); + +const LogString *update_state_to_string(UpdateState state) { + return UpdateStateStrings::get_log_str(static_cast<uint8_t>(state), + static_cast<uint8_t>(UpdateState::UPDATE_STATE_UNKNOWN)); +} + void UpdateEntity::publish_state() { ESP_LOGD(TAG, "'%s' >>\n" diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 8eba78b44b..405346bee4 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -27,6 +27,8 @@ enum UpdateState : uint8_t { UPDATE_STATE_INSTALLING, }; +const LogString *update_state_to_string(UpdateState state); + class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { public: void publish_state(); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 42219f3aac..dfd602be6b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -29,6 +29,10 @@ #include "esphome/components/climate/climate.h" #endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif + #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif @@ -2104,19 +2108,6 @@ std::string WebServer::event_json_(event::Event *obj, StringRef event_type, Json #endif #ifdef USE_UPDATE -static const LogString *update_state_to_string(update::UpdateState state) { - switch (state) { - case update::UPDATE_STATE_NO_UPDATE: - return LOG_STR("NO UPDATE"); - case update::UPDATE_STATE_AVAILABLE: - return LOG_STR("UPDATE AVAILABLE"); - case update::UPDATE_STATE_INSTALLING: - return LOG_STR("INSTALLING"); - default: - return LOG_STR("UNKNOWN"); - } -} - void WebServer::on_update(update::UpdateEntity *obj) { this->events_.deferrable_send_state(obj, "state", update_state_json_generator); } @@ -2158,7 +2149,7 @@ std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_ JsonObject root = builder.root(); char buf[PSTR_LOCAL_SIZE]; - set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update_state_to_string(obj->state)), + set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update::update_state_to_string(obj->state)), obj->update_info.latest_version, start_config); if (start_config == DETAIL_ALL) { root[ESPHOME_F("current_version")] = obj->update_info.current_version; From c7c9ffe7e15b27a8c117ada0e8d9f3310d861b4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 17:38:03 +0100 Subject: [PATCH 115/251] [light] Convert color_mode_to_human to PROGMEM_STRING_TABLE using to_bit() (#13797) --- esphome/components/light/light_call.cpp | 26 +++++++------------------ 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 6d42dd1513..f4bb0c5d11 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,6 +4,7 @@ #include "light_state.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" +#include "esphome/core/progmem.h" namespace esphome::light { @@ -51,26 +52,13 @@ static void log_invalid_parameter(const char *name, const LogString *message) { return *this; \ } +// Color mode human-readable strings indexed by ColorModeBitPolicy::to_bit() (0-9) +// Index 0 is Unknown (for ColorMode::UNKNOWN), also used as fallback for out-of-range +PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature", + "Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white"); + static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::ON_OFF) - return LOG_STR("On/Off"); - if (color_mode == ColorMode::BRIGHTNESS) - return LOG_STR("Brightness"); - if (color_mode == ColorMode::WHITE) - return LOG_STR("White"); - if (color_mode == ColorMode::COLOR_TEMPERATURE) - return LOG_STR("Color temperature"); - if (color_mode == ColorMode::COLD_WARM_WHITE) - return LOG_STR("Cold/warm white"); - if (color_mode == ColorMode::RGB) - return LOG_STR("RGB"); - if (color_mode == ColorMode::RGB_WHITE) - return LOG_STR("RGBW"); - if (color_mode == ColorMode::RGB_COLD_WARM_WHITE) - return LOG_STR("RGB + cold/warm white"); - if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) - return LOG_STR("RGB + color temperature"); - return LOG_STR("Unknown"); + return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0); } // Helper to log percentage values From 2917057da81c7d815551c82c9d220f15bef4a53e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 18:08:30 +0100 Subject: [PATCH 116/251] [analyze-memory] Trace CSWTCH switch table symbols to source components (#13798) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/analyze_memory/__init__.py | 217 ++++++++++++++++++++++++++++- esphome/analyze_memory/cli.py | 50 +++++++ esphome/analyze_memory/const.py | 13 +- 3 files changed, 267 insertions(+), 13 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 63ef0e74ed..d8c941e76f 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -12,7 +12,6 @@ from .const import ( CORE_SUBCATEGORY_PATTERNS, DEMANGLED_PATTERNS, ESPHOME_COMPONENT_PATTERN, - SECTION_TO_ATTR, SYMBOL_PATTERNS, ) from .demangle import batch_demangle @@ -91,6 +90,17 @@ class ComponentMemory: bss_size: int = 0 # Uninitialized data (ram only) symbol_count: int = 0 + def add_section_size(self, section_name: str, size: int) -> None: + """Add size to the appropriate attribute for a section.""" + if section_name == ".text": + self.text_size += size + elif section_name == ".rodata": + self.rodata_size += size + elif section_name == ".data": + self.data_size += size + elif section_name == ".bss": + self.bss_size += size + @property def flash_total(self) -> int: """Total flash usage (text + rodata + data).""" @@ -167,12 +177,15 @@ class MemoryAnalyzer: self._elf_symbol_names: set[str] = set() # SDK symbols not in ELF (static/local symbols from closed-source libs) self._sdk_symbols: list[SDKSymbol] = [] + # CSWTCH symbols: list of (name, size, source_file, component) + self._cswtch_symbols: list[tuple[str, int, str, str]] = [] def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" self._parse_sections() self._parse_symbols() self._categorize_symbols() + self._analyze_cswtch_symbols() self._analyze_sdk_libraries() return dict(self.components) @@ -255,8 +268,7 @@ class MemoryAnalyzer: comp_mem.symbol_count += 1 # Update the appropriate size attribute based on section - if attr_name := SECTION_TO_ATTR.get(section_name): - setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size) + comp_mem.add_section_size(section_name, size) # Track uncategorized symbols if component == "other" and size > 0: @@ -372,6 +384,205 @@ class MemoryAnalyzer: return "Other Core" + def _find_object_files_dir(self) -> Path | None: + """Find the directory containing object files for this build. + + Returns: + Path to the directory containing .o files, or None if not found. + """ + # The ELF is typically at .pioenvs/<env>/firmware.elf + # Object files are in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/ + pioenvs_dir = self.elf_path.parent + if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")): + return pioenvs_dir + return None + + def _scan_cswtch_in_objects( + self, obj_dir: Path + ) -> dict[str, list[tuple[str, int]]]: + """Scan object files for CSWTCH symbols using a single nm invocation. + + Uses ``nm --print-file-name -S`` on all ``.o`` files at once. + Output format: ``/path/to/file.o:address size type name`` + + Args: + obj_dir: Directory containing object files (.pioenvs/<env>/) + + Returns: + Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples. + """ + cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + + if not self.nm_path: + return cswtch_map + + # Find all .o files recursively, sorted for deterministic output + obj_files = sorted(obj_dir.rglob("*.o")) + if not obj_files: + return cswtch_map + + _LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files)) + + # Single nm call with --print-file-name for all object files + result = run_tool( + [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files], + timeout=30, + ) + if result is None or result.returncode != 0: + return cswtch_map + + for line in result.stdout.splitlines(): + if "CSWTCH$" not in line: + continue + + # Split on last ":" that precedes a hex address. + # nm --print-file-name format: filepath:hex_addr hex_size type name + # We split from the right: find the last colon followed by hex digits. + parts_after_colon = line.rsplit(":", 1) + if len(parts_after_colon) != 2: + continue + + file_path = parts_after_colon[0] + fields = parts_after_colon[1].split() + # fields: [address, size, type, name] + if len(fields) < 4: + continue + + sym_name = fields[3] + if not sym_name.startswith("CSWTCH$"): + continue + + try: + size = int(fields[1], 16) + except ValueError: + continue + + # Get relative path from obj_dir for readability + try: + rel_path = str(Path(file_path).relative_to(obj_dir)) + except ValueError: + rel_path = file_path + + key = f"{sym_name}:{size}" + cswtch_map[key].append((rel_path, size)) + + return cswtch_map + + def _source_file_to_component(self, source_file: str) -> str: + """Map a source object file path to its component name. + + Args: + source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o' + + Returns: + Component name like '[esphome]wifi' or the source file if unknown. + """ + parts = Path(source_file).parts + + # ESPHome component: src/esphome/components/<name>/... + if "components" in parts: + idx = parts.index("components") + if idx + 1 < len(parts): + component_name = parts[idx + 1] + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + + # ESPHome core: src/esphome/core/... or src/esphome/... + if "core" in parts and "esphome" in parts: + return _COMPONENT_CORE + if "esphome" in parts and "components" not in parts: + return _COMPONENT_CORE + + # Framework/library files - return the first path component + # e.g., lib65b/ESPAsyncTCP/... -> lib65b + # FrameworkArduino/... -> FrameworkArduino + return parts[0] if parts else source_file + + def _analyze_cswtch_symbols(self) -> None: + """Analyze CSWTCH (GCC switch table) symbols by tracing to source objects. + + CSWTCH symbols are compiler-generated lookup tables for switch statements. + They are local symbols, so the same name can appear in different object files. + This method scans .o files to attribute them to their source components. + """ + obj_dir = self._find_object_files_dir() + if obj_dir is None: + _LOGGER.debug("No object files directory found, skipping CSWTCH analysis") + return + + # Scan object files for CSWTCH symbols + cswtch_map = self._scan_cswtch_in_objects(obj_dir) + if not cswtch_map: + _LOGGER.debug("No CSWTCH symbols found in object files") + return + + # Collect CSWTCH symbols from the ELF (already parsed in sections) + # Include section_name for re-attribution of component totals + elf_cswtch = [ + (symbol_name, size, section_name) + for section_name, section in self.sections.items() + for symbol_name, size, _ in section.symbols + if symbol_name.startswith("CSWTCH$") + ] + + _LOGGER.debug( + "Found %d CSWTCH symbols in ELF, %d unique in object files", + len(elf_cswtch), + len(cswtch_map), + ) + + # Match ELF CSWTCH symbols to source files and re-attribute component totals. + # _categorize_symbols() already ran and put these into "other" since CSWTCH$ + # names don't match any component pattern. We move the bytes to the correct + # component based on the object file mapping. + other_mem = self.components.get("other") + + for sym_name, size, section_name in elf_cswtch: + key = f"{sym_name}:{size}" + sources = cswtch_map.get(key, []) + + if len(sources) == 1: + source_file = sources[0][0] + component = self._source_file_to_component(source_file) + elif len(sources) > 1: + # Ambiguous - multiple object files have same CSWTCH name+size + source_file = "ambiguous" + component = "ambiguous" + _LOGGER.debug( + "Ambiguous CSWTCH %s (%d B) found in %d files: %s", + sym_name, + size, + len(sources), + ", ".join(src for src, _ in sources), + ) + else: + source_file = "unknown" + component = "unknown" + + self._cswtch_symbols.append((sym_name, size, source_file, component)) + + # Re-attribute from "other" to the correct component + if ( + component not in ("other", "unknown", "ambiguous") + and other_mem is not None + ): + other_mem.add_section_size(section_name, -size) + if component not in self.components: + self.components[component] = ComponentMemory(component) + self.components[component].add_section_size(section_name, size) + + # Sort by size descending + self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True) + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + _LOGGER.debug( + "CSWTCH analysis: %d symbols, %d bytes total", + len(self._cswtch_symbols), + total_size, + ) + def get_unattributed_ram(self) -> tuple[int, int, int]: """Get unattributed RAM sizes (SDK/framework overhead). diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 72a73dbdd4..bb0eb7723e 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -184,6 +184,52 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" ) + def _add_cswtch_analysis(self, lines: list[str]) -> None: + """Add CSWTCH (GCC switch table lookup) analysis section.""" + self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)") + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + lines.append( + f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B" + ) + lines.append("") + + # Group by component + by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list) + for sym_name, size, source_file, component in self._cswtch_symbols: + by_component[component].append((sym_name, size, source_file)) + + # Sort components by total size descending + sorted_components = sorted( + by_component.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ) + + for component, symbols in sorted_components: + comp_total = sum(s[1] for s in symbols) + lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):") + + # Group by source file within component + by_file: dict[str, list[tuple[str, int]]] = defaultdict(list) + for sym_name, size, source_file in symbols: + by_file[source_file].append((sym_name, size)) + + for source_file, file_symbols in sorted( + by_file.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ): + file_total = sum(s[1] for s in file_symbols) + lines.append( + f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)" + ) + for sym_name, size in sorted( + file_symbols, key=lambda x: x[1], reverse=True + ): + lines.append(f" {size:>6,} B {sym_name}") + lines.append("") + def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" components = sorted( @@ -471,6 +517,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append("") + # CSWTCH (GCC switch table) analysis + if self._cswtch_symbols: + self._add_cswtch_analysis(lines) + lines.append( "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." ) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 83547b1eb5..66866615a6 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -66,15 +66,6 @@ SECTION_MAPPING = { ), } -# Section to ComponentMemory attribute mapping -# Maps section names to the attribute name in ComponentMemory dataclass -SECTION_TO_ATTR = { - ".text": "text_size", - ".rodata": "rodata_size", - ".data": "data_size", - ".bss": "bss_size", -} - # Component identification rules # Symbol patterns: patterns found in raw symbol names SYMBOL_PATTERNS = { @@ -513,7 +504,9 @@ SYMBOL_PATTERNS = { "__FUNCTION__$", "DAYS_IN_MONTH", "_DAYS_BEFORE_MONTH", - "CSWTCH$", + # Note: CSWTCH$ symbols are GCC switch table lookup tables. + # They are attributed to their source object files via _analyze_cswtch_symbols() + # rather than being lumped into libc. "dst$", "sulp", "_strtol_l", # String to long with locale From f9192b5f75af343a41eb00b1cf82dd9b8380c90a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 18:20:46 +0100 Subject: [PATCH 117/251] [wifi] Avoid jump tables in LOG_STR switch statements to save ESP8266 RAM (#13799) --- esphome/components/wifi/wifi_component.cpp | 30 ++-- .../wifi/wifi_component_esp8266.cpp | 150 ++++++++---------- 2 files changed, 81 insertions(+), 99 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0fe98162f3..e350f990af 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -236,25 +236,23 @@ static const char *const TAG = "wifi"; /// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// └──────────────────────────────────────────────────────────────────────┘ +// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { - switch (phase) { - case WiFiRetryPhase::INITIAL_CONNECT: - return LOG_STR("INITIAL_CONNECT"); + if (phase == WiFiRetryPhase::INITIAL_CONNECT) + return LOG_STR("INITIAL_CONNECT"); #ifdef USE_WIFI_FAST_CONNECT - case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: - return LOG_STR("FAST_CONNECT_CYCLING"); + if (phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) + return LOG_STR("FAST_CONNECT_CYCLING"); #endif - case WiFiRetryPhase::EXPLICIT_HIDDEN: - return LOG_STR("EXPLICIT_HIDDEN"); - case WiFiRetryPhase::SCAN_CONNECTING: - return LOG_STR("SCAN_CONNECTING"); - case WiFiRetryPhase::RETRY_HIDDEN: - return LOG_STR("RETRY_HIDDEN"); - case WiFiRetryPhase::RESTARTING_ADAPTER: - return LOG_STR("RESTARTING"); - default: - return LOG_STR("UNKNOWN"); - } + if (phase == WiFiRetryPhase::EXPLICIT_HIDDEN) + return LOG_STR("EXPLICIT_HIDDEN"); + if (phase == WiFiRetryPhase::SCAN_CONNECTING) + return LOG_STR("SCAN_CONNECTING"); + if (phase == WiFiRetryPhase::RETRY_HIDDEN) + return LOG_STR("RETRY_HIDDEN"); + if (phase == WiFiRetryPhase::RESTARTING_ADAPTER) + return LOG_STR("RESTARTING"); + return LOG_STR("UNKNOWN"); } bool WiFiComponent::went_through_explicit_hidden_phase_() const { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 6e2adcbf04..6488de8dae 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -416,75 +416,65 @@ PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN"); const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); } +// Use if-chain instead of switch to avoid jump tables in RODATA (wastes RAM on ESP8266). +// A single switch would generate a sparse lookup table with ~175 default entries, wasting 700 bytes of RAM. +// Even split switches still generate smaller jump tables in RODATA. const LogString *get_disconnect_reason_str(uint8_t reason) { - /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the - * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM - * per entry. As there's ~175 default entries, this wastes 700 bytes of RAM. - */ - if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200 - switch (reason) { - case REASON_AUTH_EXPIRE: - return LOG_STR("Auth Expired"); - case REASON_AUTH_LEAVE: - return LOG_STR("Auth Leave"); - case REASON_ASSOC_EXPIRE: - return LOG_STR("Association Expired"); - case REASON_ASSOC_TOOMANY: - return LOG_STR("Too Many Associations"); - case REASON_NOT_AUTHED: - return LOG_STR("Not Authenticated"); - case REASON_NOT_ASSOCED: - return LOG_STR("Not Associated"); - case REASON_ASSOC_LEAVE: - return LOG_STR("Association Leave"); - case REASON_ASSOC_NOT_AUTHED: - return LOG_STR("Association not Authenticated"); - case REASON_DISASSOC_PWRCAP_BAD: - return LOG_STR("Disassociate Power Cap Bad"); - case REASON_DISASSOC_SUPCHAN_BAD: - return LOG_STR("Disassociate Supported Channel Bad"); - case REASON_IE_INVALID: - return LOG_STR("IE Invalid"); - case REASON_MIC_FAILURE: - return LOG_STR("Mic Failure"); - case REASON_4WAY_HANDSHAKE_TIMEOUT: - return LOG_STR("4-Way Handshake Timeout"); - case REASON_GROUP_KEY_UPDATE_TIMEOUT: - return LOG_STR("Group Key Update Timeout"); - case REASON_IE_IN_4WAY_DIFFERS: - return LOG_STR("IE In 4-Way Handshake Differs"); - case REASON_GROUP_CIPHER_INVALID: - return LOG_STR("Group Cipher Invalid"); - case REASON_PAIRWISE_CIPHER_INVALID: - return LOG_STR("Pairwise Cipher Invalid"); - case REASON_AKMP_INVALID: - return LOG_STR("AKMP Invalid"); - case REASON_UNSUPP_RSN_IE_VERSION: - return LOG_STR("Unsupported RSN IE version"); - case REASON_INVALID_RSN_IE_CAP: - return LOG_STR("Invalid RSN IE Cap"); - case REASON_802_1X_AUTH_FAILED: - return LOG_STR("802.1x Authentication Failed"); - case REASON_CIPHER_SUITE_REJECTED: - return LOG_STR("Cipher Suite Rejected"); - } - } - - switch (reason) { - case REASON_BEACON_TIMEOUT: - return LOG_STR("Beacon Timeout"); - case REASON_NO_AP_FOUND: - return LOG_STR("AP Not Found"); - case REASON_AUTH_FAIL: - return LOG_STR("Authentication Failed"); - case REASON_ASSOC_FAIL: - return LOG_STR("Association Failed"); - case REASON_HANDSHAKE_TIMEOUT: - return LOG_STR("Handshake Failed"); - case REASON_UNSPECIFIED: - default: - return LOG_STR("Unspecified"); - } + if (reason == REASON_AUTH_EXPIRE) + return LOG_STR("Auth Expired"); + if (reason == REASON_AUTH_LEAVE) + return LOG_STR("Auth Leave"); + if (reason == REASON_ASSOC_EXPIRE) + return LOG_STR("Association Expired"); + if (reason == REASON_ASSOC_TOOMANY) + return LOG_STR("Too Many Associations"); + if (reason == REASON_NOT_AUTHED) + return LOG_STR("Not Authenticated"); + if (reason == REASON_NOT_ASSOCED) + return LOG_STR("Not Associated"); + if (reason == REASON_ASSOC_LEAVE) + return LOG_STR("Association Leave"); + if (reason == REASON_ASSOC_NOT_AUTHED) + return LOG_STR("Association not Authenticated"); + if (reason == REASON_DISASSOC_PWRCAP_BAD) + return LOG_STR("Disassociate Power Cap Bad"); + if (reason == REASON_DISASSOC_SUPCHAN_BAD) + return LOG_STR("Disassociate Supported Channel Bad"); + if (reason == REASON_IE_INVALID) + return LOG_STR("IE Invalid"); + if (reason == REASON_MIC_FAILURE) + return LOG_STR("Mic Failure"); + if (reason == REASON_4WAY_HANDSHAKE_TIMEOUT) + return LOG_STR("4-Way Handshake Timeout"); + if (reason == REASON_GROUP_KEY_UPDATE_TIMEOUT) + return LOG_STR("Group Key Update Timeout"); + if (reason == REASON_IE_IN_4WAY_DIFFERS) + return LOG_STR("IE In 4-Way Handshake Differs"); + if (reason == REASON_GROUP_CIPHER_INVALID) + return LOG_STR("Group Cipher Invalid"); + if (reason == REASON_PAIRWISE_CIPHER_INVALID) + return LOG_STR("Pairwise Cipher Invalid"); + if (reason == REASON_AKMP_INVALID) + return LOG_STR("AKMP Invalid"); + if (reason == REASON_UNSUPP_RSN_IE_VERSION) + return LOG_STR("Unsupported RSN IE version"); + if (reason == REASON_INVALID_RSN_IE_CAP) + return LOG_STR("Invalid RSN IE Cap"); + if (reason == REASON_802_1X_AUTH_FAILED) + return LOG_STR("802.1x Authentication Failed"); + if (reason == REASON_CIPHER_SUITE_REJECTED) + return LOG_STR("Cipher Suite Rejected"); + if (reason == REASON_BEACON_TIMEOUT) + return LOG_STR("Beacon Timeout"); + if (reason == REASON_NO_AP_FOUND) + return LOG_STR("AP Not Found"); + if (reason == REASON_AUTH_FAIL) + return LOG_STR("Authentication Failed"); + if (reason == REASON_ASSOC_FAIL) + return LOG_STR("Association Failed"); + if (reason == REASON_HANDSHAKE_TIMEOUT) + return LOG_STR("Handshake Failed"); + return LOG_STR("Unspecified"); } // TODO: This callback runs in ESP8266 system context with limited stack (~2KB). @@ -645,21 +635,15 @@ void WiFiComponent::wifi_pre_setup_() { WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { station_status_t status = wifi_station_get_connect_status(); - switch (status) { - case STATION_GOT_IP: - return WiFiSTAConnectStatus::CONNECTED; - case STATION_NO_AP_FOUND: - return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; - ; - case STATION_CONNECT_FAIL: - case STATION_WRONG_PASSWORD: - return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; - case STATION_CONNECTING: - return WiFiSTAConnectStatus::CONNECTING; - case STATION_IDLE: - default: - return WiFiSTAConnectStatus::IDLE; - } + if (status == STATION_GOT_IP) + return WiFiSTAConnectStatus::CONNECTED; + if (status == STATION_NO_AP_FOUND) + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD) + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + if (status == STATION_CONNECTING) + return WiFiSTAConnectStatus::CONNECTING; + return WiFiSTAConnectStatus::IDLE; } bool WiFiComponent::wifi_scan_start_(bool passive) { static bool first_scan = false; From 238e40966f8a373ac634987fd30f871d55f0cf59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 18:33:26 +0100 Subject: [PATCH 118/251] [light] Move CSWTCH lookup table to PROGMEM in get_suitable_color_modes_mask_ (#13801) --- esphome/components/light/light_call.cpp | 88 ++++++++++++++----------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index f4bb0c5d11..3b4e136ba5 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -445,6 +445,52 @@ ColorMode LightCall::compute_color_mode_() { LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } +// PROGMEM lookup table for get_suitable_color_modes_mask_(). +// Maps 4-bit key (white | ct<<1 | cwww<<2 | rgb<<3) to color mode bitmask. +// Packed into uint8_t by right-shifting by PACK_SHIFT since the lower bits +// (UNKNOWN, ON_OFF, BRIGHTNESS) are never present in suitable mode masks. +static constexpr unsigned PACK_SHIFT = ColorModeBitPolicy::to_bit(ColorMode::WHITE); +// clang-format off +static constexpr uint8_t SUITABLE_COLOR_MODES[] PROGMEM = { + // [0] none - all modes with brightness + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [1] white only + static_cast<uint8_t>(ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [2] ct only + static_cast<uint8_t>(ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [3] white + ct + static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [4] cwww only + static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + 0, // [5] white + cwww (conflicting) + 0, // [6] ct + cwww (conflicting) + 0, // [7] white + ct + cwww (conflicting) + // [8] rgb only + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [9] rgb + white + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [10] rgb + ct + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [11] rgb + white + ct + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [12] rgb + cwww + static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + 0, // [13] rgb + white + cwww (conflicting) + 0, // [14] rgb + ct + cwww (conflicting) + 0, // [15] all (conflicting) +}; +// clang-format on + color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { bool has_white = this->has_white() && this->white_ > 0.0f; bool has_ct = this->has_color_temperature(); @@ -454,46 +500,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { (this->has_red() || this->has_green() || this->has_blue()); // Build key from flags: [rgb][cwww][ct][white] -#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) - - uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb); - - switch (key) { - case KEY(true, false, false, false): // white only - return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, - ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, true, false, false): // ct only - return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(true, true, false, false): // white + ct - return ColorModeMask( - {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, false, true, false): // cwww only - return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, false, false): // none - return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, - ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}) - .get_mask(); - case KEY(true, false, false, true): // rgb + white - return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, true, false, true): // rgb + ct - case KEY(true, true, false, true): // rgb + white + ct - return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, true, true): // rgb + cwww - return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, false, true): // rgb only - return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, - ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - default: - return 0; // conflicting flags - } - -#undef KEY + uint8_t key = has_white | (has_ct << 1) | (has_cwww << 2) | (has_rgb << 3); + return static_cast<color_mode_bitmask_t>(progmem_read_byte(&SUITABLE_COLOR_MODES[key])) << PACK_SHIFT; } LightCall &LightCall::set_effect(const char *effect, size_t len) { From 155447f541b18524ce8a9509e3614c1411c91e6e Mon Sep 17 00:00:00 2001 From: PolarGoose <35307286+PolarGoose@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:53:59 +0100 Subject: [PATCH 119/251] [dsmr] Fix issue with parsing lines like `1-0:0.2.0((ER11))` (#13780) --- .clang-tidy.hash | 2 +- esphome/components/dsmr/__init__.py | 2 +- esphome/components/dsmr/sensor.py | 8 -------- esphome/components/dsmr/text_sensor.py | 2 ++ platformio.ini | 2 +- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index ab354259e3..63ddbbac05 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3 +37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 386da3ce21..7d76856f28 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -66,7 +66,7 @@ async def to_code(config): cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("esphome/dsmr_parser", "1.0.0") + cg.add_library("esphome/dsmr_parser", "1.1.0") # Crypto cg.add_library("polargoose/Crypto-no-arduino", "0.4.0") diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 7d69f79530..863af42d1b 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -718,14 +718,6 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional("fw_core_version"): sensor.sensor_schema( - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("fw_module_version"): sensor.sensor_schema( - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 4c7455a38f..203c9c997e 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -26,7 +26,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(), + cv.Optional("fw_core_version"): text_sensor.text_sensor_schema(), cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(), + cv.Optional("fw_module_version"): text_sensor.text_sensor_schema(), cv.Optional("telegram"): text_sensor.text_sensor_schema().extend( {cv.Optional(CONF_INTERNAL, default=True): cv.boolean} ), diff --git a/platformio.ini b/platformio.ini index bb0de3c2b1..d198862a25 100644 --- a/platformio.ini +++ b/platformio.ini @@ -37,7 +37,7 @@ lib_deps_base = wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier - esphome/dsmr_parser@1.0.0 ; dsmr + esphome/dsmr_parser@1.1.0 ; dsmr polargoose/Crypto-no-arduino@0.4.0 ; dsmr https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps ; This is using the repository until a new release is published to PlatformIO From 9315da79bc016c5b45ba93ca85a2c257b89d559c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:03:16 -0500 Subject: [PATCH 120/251] [core] Add missing requests dependency to requirements.txt (#13803) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index bb118c5f9a..1771867535 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 +requests==2.32.5 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From 41cecbfb0f62db7e6dffff709e34141a06e59f7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 19:22:26 +0100 Subject: [PATCH 121/251] [template] Convert alarm sensor type to PROGMEM_STRING_TABLE and narrow enum to uint8_t (#13804) --- .../template_alarm_control_panel.cpp | 16 +++++----------- .../template_alarm_control_panel.h | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 1a5aef6b8d..09efe678ce 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -5,6 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::template_ { @@ -28,18 +29,11 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, this->sensor_data_.push_back(sd); }; +// Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS +PROGMEM_STRING_TABLE(AlarmSensorTypeStrings, "delayed", "instant", "delayed_follower", "instant_always"); + static const LogString *sensor_type_to_string(AlarmSensorType type) { - switch (type) { - case ALARM_SENSOR_TYPE_INSTANT: - return LOG_STR("instant"); - case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: - return LOG_STR("delayed_follower"); - case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: - return LOG_STR("instant_always"); - case ALARM_SENSOR_TYPE_DELAYED: - default: - return LOG_STR("delayed"); - } + return AlarmSensorTypeStrings::get_log_str(static_cast<uint8_t>(type), 0); } #endif diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index df3b64fb6e..4f32e99fd7 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -26,7 +26,7 @@ enum BinarySensorFlags : uint16_t { BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4, }; -enum AlarmSensorType : uint16_t { +enum AlarmSensorType : uint8_t { ALARM_SENSOR_TYPE_DELAYED = 0, ALARM_SENSOR_TYPE_INSTANT, ALARM_SENSOR_TYPE_DELAYED_FOLLOWER, From 86f91eed2f69a7cdb7789fb4927d560a7133e890 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Feb 2026 19:30:05 +0100 Subject: [PATCH 122/251] [mqtt] Move switch string tables to PROGMEM_STRING_TABLE (#13802) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../mqtt/mqtt_alarm_control_panel.cpp | 29 +---- esphome/components/mqtt/mqtt_client.cpp | 38 ++---- esphome/components/mqtt/mqtt_climate.cpp | 115 ++++-------------- esphome/components/mqtt/mqtt_component.cpp | 13 +- esphome/components/mqtt/mqtt_number.cpp | 17 ++- esphome/components/mqtt/mqtt_text.cpp | 14 +-- 6 files changed, 58 insertions(+), 168 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index dc8f75d8f5..a461a140ae 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -13,31 +13,12 @@ static const char *const TAG = "mqtt.alarm_control_panel"; using namespace esphome::alarm_control_panel; +// Alarm state MQTT strings indexed by AlarmControlPanelState enum (0-9) +PROGMEM_STRING_TABLE(AlarmMqttStateStrings, "disarmed", "armed_home", "armed_away", "armed_night", "armed_vacation", + "armed_custom_bypass", "pending", "arming", "disarming", "triggered", "unknown"); + static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { - switch (state) { - case ACP_STATE_DISARMED: - return ESPHOME_F("disarmed"); - case ACP_STATE_ARMED_HOME: - return ESPHOME_F("armed_home"); - case ACP_STATE_ARMED_AWAY: - return ESPHOME_F("armed_away"); - case ACP_STATE_ARMED_NIGHT: - return ESPHOME_F("armed_night"); - case ACP_STATE_ARMED_VACATION: - return ESPHOME_F("armed_vacation"); - case ACP_STATE_ARMED_CUSTOM_BYPASS: - return ESPHOME_F("armed_custom_bypass"); - case ACP_STATE_PENDING: - return ESPHOME_F("pending"); - case ACP_STATE_ARMING: - return ESPHOME_F("arming"); - case ACP_STATE_DISARMING: - return ESPHOME_F("disarming"); - case ACP_STATE_TRIGGERED: - return ESPHOME_F("triggered"); - default: - return ESPHOME_F("unknown"); - } + return AlarmMqttStateStrings::get_progmem_str(static_cast<uint8_t>(state), AlarmMqttStateStrings::LAST_INDEX); } MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index a284b162dd..d503461257 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -8,6 +8,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/version.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -27,6 +28,11 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8) +PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version", + "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized", + "Not Enough Space", "TLS Bad Fingerprint", "DNS Resolve Error", "Unknown"); + MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; char mac_addr[MAC_ADDRESS_BUFFER_SIZE]; @@ -348,36 +354,8 @@ void MQTTClientComponent::loop() { mqtt_backend_.loop(); if (this->disconnect_reason_.has_value()) { - const LogString *reason_s; - switch (*this->disconnect_reason_) { - case MQTTClientDisconnectReason::TCP_DISCONNECTED: - reason_s = LOG_STR("TCP disconnected"); - break; - case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: - reason_s = LOG_STR("Unacceptable Protocol Version"); - break; - case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: - reason_s = LOG_STR("Identifier Rejected"); - break; - case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: - reason_s = LOG_STR("Server Unavailable"); - break; - case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: - reason_s = LOG_STR("Malformed Credentials"); - break; - case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED: - reason_s = LOG_STR("Not Authorized"); - break; - case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: - reason_s = LOG_STR("Not Enough Space"); - break; - case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT: - reason_s = LOG_STR("TLS Bad Fingerprint"); - break; - default: - reason_s = LOG_STR("Unknown"); - break; - } + const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str( + static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX); if (!network::is_connected()) { reason_s = LOG_STR("WiFi disconnected"); } diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 673593ef84..158cfb5552 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -13,109 +13,44 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; +// Climate mode MQTT strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO +PROGMEM_STRING_TABLE(ClimateMqttModeStrings, "off", "heat_cool", "cool", "heat", "fan_only", "dry", "auto", "unknown"); + static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { - switch (mode) { - case CLIMATE_MODE_OFF: - return ESPHOME_F("off"); - case CLIMATE_MODE_HEAT_COOL: - return ESPHOME_F("heat_cool"); - case CLIMATE_MODE_AUTO: - return ESPHOME_F("auto"); - case CLIMATE_MODE_COOL: - return ESPHOME_F("cool"); - case CLIMATE_MODE_HEAT: - return ESPHOME_F("heat"); - case CLIMATE_MODE_FAN_ONLY: - return ESPHOME_F("fan_only"); - case CLIMATE_MODE_DRY: - return ESPHOME_F("dry"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), ClimateMqttModeStrings::LAST_INDEX); } +// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN +PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan", + "unknown"); + static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { - switch (action) { - case CLIMATE_ACTION_OFF: - return ESPHOME_F("off"); - case CLIMATE_ACTION_COOLING: - return ESPHOME_F("cooling"); - case CLIMATE_ACTION_HEATING: - return ESPHOME_F("heating"); - case CLIMATE_ACTION_IDLE: - return ESPHOME_F("idle"); - case CLIMATE_ACTION_DRYING: - return ESPHOME_F("drying"); - case CLIMATE_ACTION_FAN: - return ESPHOME_F("fan"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttActionStrings::get_progmem_str(static_cast<uint8_t>(action), ClimateMqttActionStrings::LAST_INDEX); } +// Climate fan mode MQTT strings indexed by ClimateFanMode enum (0-9) +PROGMEM_STRING_TABLE(ClimateMqttFanModeStrings, "on", "off", "auto", "low", "medium", "high", "middle", "focus", + "diffuse", "quiet", "unknown"); + static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) { - switch (fan_mode) { - case CLIMATE_FAN_ON: - return ESPHOME_F("on"); - case CLIMATE_FAN_OFF: - return ESPHOME_F("off"); - case CLIMATE_FAN_AUTO: - return ESPHOME_F("auto"); - case CLIMATE_FAN_LOW: - return ESPHOME_F("low"); - case CLIMATE_FAN_MEDIUM: - return ESPHOME_F("medium"); - case CLIMATE_FAN_HIGH: - return ESPHOME_F("high"); - case CLIMATE_FAN_MIDDLE: - return ESPHOME_F("middle"); - case CLIMATE_FAN_FOCUS: - return ESPHOME_F("focus"); - case CLIMATE_FAN_DIFFUSE: - return ESPHOME_F("diffuse"); - case CLIMATE_FAN_QUIET: - return ESPHOME_F("quiet"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttFanModeStrings::get_progmem_str(static_cast<uint8_t>(fan_mode), + ClimateMqttFanModeStrings::LAST_INDEX); } +// Climate swing mode MQTT strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL +PROGMEM_STRING_TABLE(ClimateMqttSwingModeStrings, "off", "both", "vertical", "horizontal", "unknown"); + static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) { - switch (swing_mode) { - case CLIMATE_SWING_OFF: - return ESPHOME_F("off"); - case CLIMATE_SWING_BOTH: - return ESPHOME_F("both"); - case CLIMATE_SWING_VERTICAL: - return ESPHOME_F("vertical"); - case CLIMATE_SWING_HORIZONTAL: - return ESPHOME_F("horizontal"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttSwingModeStrings::get_progmem_str(static_cast<uint8_t>(swing_mode), + ClimateMqttSwingModeStrings::LAST_INDEX); } +// Climate preset MQTT strings indexed by ClimatePreset enum (0-7) +PROGMEM_STRING_TABLE(ClimateMqttPresetStrings, "none", "home", "away", "boost", "comfort", "eco", "sleep", "activity", + "unknown"); + static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) { - switch (preset) { - case CLIMATE_PRESET_NONE: - return ESPHOME_F("none"); - case CLIMATE_PRESET_HOME: - return ESPHOME_F("home"); - case CLIMATE_PRESET_ECO: - return ESPHOME_F("eco"); - case CLIMATE_PRESET_AWAY: - return ESPHOME_F("away"); - case CLIMATE_PRESET_BOOST: - return ESPHOME_F("boost"); - case CLIMATE_PRESET_COMFORT: - return ESPHOME_F("comfort"); - case CLIMATE_PRESET_SLEEP: - return ESPHOME_F("sleep"); - case CLIMATE_PRESET_ACTIVITY: - return ESPHOME_F("activity"); - default: - return ESPHOME_F("unknown"); - } + return ClimateMqttPresetStrings::get_progmem_str(static_cast<uint8_t>(preset), ClimateMqttPresetStrings::LAST_INDEX); } void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index a77afd3f4e..e4b80de50a 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -14,6 +14,9 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt.component"; +// Entity category MQTT strings indexed by EntityCategory enum: NONE(0) is skipped, CONFIG(1), DIAGNOSTIC(2) +PROGMEM_STRING_TABLE(EntityCategoryMqttStrings, "", "config", "diagnostic"); + // Helper functions for building topic strings on stack inline char *append_str(char *p, const char *s, size_t len) { memcpy(p, s, len); @@ -213,13 +216,9 @@ bool MQTTComponent::send_discovery_() { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); - switch (entity_category) { - case ENTITY_CATEGORY_NONE: - break; - case ENTITY_CATEGORY_CONFIG: - case ENTITY_CATEGORY_DIAGNOSTIC: - root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic"; - break; + if (entity_category != ENTITY_CATEGORY_NONE) { + root[MQTT_ENTITY_CATEGORY] = EntityCategoryMqttStrings::get_progmem_str( + static_cast<uint8_t>(entity_category), static_cast<uint8_t>(ENTITY_CATEGORY_CONFIG)); } if (config.state_topic) { diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 7dc93eee0c..fdc909fcc9 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -1,5 +1,6 @@ #include "mqtt_number.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.number"; using namespace esphome::number; +// Number mode MQTT strings indexed by NumberMode enum: AUTO(0) is skipped, BOX(1), SLIDER(2) +PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider"); + MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {} void MQTTNumberComponent::setup() { @@ -48,15 +52,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; } - switch (this->number_->traits.get_mode()) { - case NUMBER_MODE_AUTO: - break; - case NUMBER_MODE_BOX: - root[MQTT_MODE] = "box"; - break; - case NUMBER_MODE_SLIDER: - root[MQTT_MODE] = "slider"; - break; + const auto mode = this->number_->traits.get_mode(); + if (mode != NUMBER_MODE_AUTO) { + root[MQTT_MODE] = + NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX)); } const auto device_class = this->number_->traits.get_device_class_ref(); if (!device_class.empty()) { diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index 16293c0603..200e420c9c 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -1,5 +1,6 @@ #include "mqtt_text.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.text"; using namespace esphome::text; +// Text mode MQTT strings indexed by TextMode enum (0-1): TEXT, PASSWORD +PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password"); + MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {} void MQTTTextComponent::setup() { @@ -34,14 +38,8 @@ const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - switch (this->text_->traits.get_mode()) { - case TEXT_MODE_TEXT: - root[MQTT_MODE] = "text"; - break; - case TEXT_MODE_PASSWORD: - root[MQTT_MODE] = "password"; - break; - } + root[MQTT_MODE] = TextMqttModeStrings::get_progmem_str(static_cast<uint8_t>(this->text_->traits.get_mode()), + static_cast<uint8_t>(TEXT_MODE_TEXT)); config.command_topic = true; } From eb7aa3420fcf0d649953a0ce6c8643c0a2357e31 Mon Sep 17 00:00:00 2001 From: tronikos <tronikos@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:23:42 -0800 Subject: [PATCH 123/251] Add target_temperature to the template water heater (#13661) Co-authored-by: J. Nick Koston <nick@koston.org> --- .../components/template/water_heater/__init__.py | 9 +++++++++ .../water_heater/template_water_heater.cpp | 14 +++++++++++++- .../template/water_heater/template_water_heater.h | 4 ++++ tests/components/template/common-base.yaml | 1 + .../fixtures/water_heater_template.yaml | 1 + tests/integration/test_water_heater_template.py | 3 +++ 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index bddd378b23..5f96155fbf 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -46,6 +46,7 @@ CONFIG_SCHEMA = ( RESTORE_MODES, upper=True ), cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, + cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda, cv.Optional(CONF_MODE): cv.returning_lambda, cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( water_heater.validate_water_heater_mode @@ -78,6 +79,14 @@ async def to_code(config: ConfigType) -> None: ) cg.add(var.set_current_temperature_lambda(template_)) + if CONF_TARGET_TEMPERATURE in config: + template_ = await cg.process_lambda( + config[CONF_TARGET_TEMPERATURE], + [], + return_type=cg.optional.template(cg.float_), + ) + cg.add(var.set_target_temperature_lambda(template_)) + if CONF_MODE in config: template_ = await cg.process_lambda( config[CONF_MODE], diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index f888edb1df..c354deee0e 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() { restore->perform(); } } - if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value()) + if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() && + !this->mode_f_.has_value()) this->disable_loop(); } @@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { } traits.set_supports_current_temperature(true); + if (this->target_temperature_f_.has_value()) { + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE); + } return traits; } @@ -42,6 +46,14 @@ void TemplateWaterHeater::loop() { } } + auto target_temp = this->target_temperature_f_.call(); + if (target_temp.has_value()) { + if (*target_temp != this->target_temperature_) { + this->target_temperature_ = *target_temp; + changed = true; + } + } + auto new_mode = this->mode_f_.call(); if (new_mode.has_value()) { if (*new_mode != this->mode_) { diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index f1cf00a115..22173209aa 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -20,6 +20,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { template<typename F> void set_current_temperature_lambda(F &&f) { this->current_temperature_f_.set(std::forward<F>(f)); } + template<typename F> void set_target_temperature_lambda(F &&f) { + this->target_temperature_f_.set(std::forward<F>(f)); + } template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } @@ -44,6 +47,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller Trigger<> set_trigger_; TemplateLambda<float> current_temperature_f_; + TemplateLambda<float> target_temperature_f_; TemplateLambda<water_heater::WaterHeaterMode> mode_f_; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; water_heater::WaterHeaterModeMask supported_modes_; diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index d1849efaf7..b8742f8c7b 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -412,6 +412,7 @@ water_heater: name: "Template Water Heater" optimistic: true current_temperature: !lambda "return 42.0f;" + target_temperature: !lambda "return 60.0f;" mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" supported_modes: - "OFF" diff --git a/tests/integration/fixtures/water_heater_template.yaml b/tests/integration/fixtures/water_heater_template.yaml index b54ebed789..1aaded1991 100644 --- a/tests/integration/fixtures/water_heater_template.yaml +++ b/tests/integration/fixtures/water_heater_template.yaml @@ -10,6 +10,7 @@ water_heater: name: Test Boiler optimistic: true current_temperature: !lambda "return 45.0f;" + target_temperature: !lambda "return 60.0f;" # Note: No mode lambda - we want optimistic mode changes to stick # A mode lambda would override mode changes in loop() supported_modes: diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index b5f1fb64c0..6b4a685d0d 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -85,6 +85,9 @@ async def test_water_heater_template( assert initial_state.current_temperature == 45.0, ( f"Expected current temp 45.0, got {initial_state.current_temperature}" ) + assert initial_state.target_temperature == 60.0, ( + f"Expected target temp 60.0, got {initial_state.target_temperature}" + ) # Test changing to GAS mode client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS) From 9de91539e6d1990593400db003ad05a7c55795d3 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:24:57 +0100 Subject: [PATCH 124/251] [epaper_spi] Add Waveshare 1.54-G (#13758) --- esphome/components/epaper_spi/colorconv.h | 67 ++++++ .../epaper_spi/epaper_spi_jd79660.cpp | 227 ++++++++++++++++++ .../epaper_spi/epaper_spi_jd79660.h | 145 +++++++++++ .../components/epaper_spi/models/jd79660.py | 86 +++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 16 ++ 5 files changed, 541 insertions(+) create mode 100644 esphome/components/epaper_spi/colorconv.h create mode 100644 esphome/components/epaper_spi/epaper_spi_jd79660.cpp create mode 100644 esphome/components/epaper_spi/epaper_spi_jd79660.h create mode 100644 esphome/components/epaper_spi/models/jd79660.py diff --git a/esphome/components/epaper_spi/colorconv.h b/esphome/components/epaper_spi/colorconv.h new file mode 100644 index 0000000000..a2ea28f4b6 --- /dev/null +++ b/esphome/components/epaper_spi/colorconv.h @@ -0,0 +1,67 @@ +#pragma once + +#include <cstdint> +#include <algorithm> +#include "esphome/core/color.h" + +/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys + * + * Focus in driver layer is on efficiency. + * For optimum output quality on RGB inputs consider offline color keying/dithering. + * Also see e.g. Image component. + */ + +namespace esphome::epaper_spi { + +/** Delta for when to regard as gray */ +static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50; + +/** Map RGB color to discrete BWYR hex 4 color key + * + * @tparam NATIVE_COLOR Type of native hardware color values + * @param color RGB color to convert from + * @param hw_black Native value for black + * @param hw_white Native value for white + * @param hw_yellow Native value for yellow + * @param hw_red Native value for red + * @return Converted native hardware color value + * @internal Constexpr. Does not depend on side effects ("pure"). + */ +template<typename NATIVE_COLOR> +constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_yellow, + NATIVE_COLOR hw_red) { + // --- Step 1: Check for Grayscale (Black or White) --- + // We define "grayscale" as a color where the min and max components + // are close to each other. + + const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) { + // It's a shade of gray. Map to BLACK or WHITE. + // We split the luminance at the halfway point (382 = (255*3)/2) + if ((static_cast<int>(color.r) + color.g + color.b) > 382) { + return hw_white; + } + return hw_black; + } + + // --- Step 2: Check for Primary/Secondary Colors --- + // If it's not gray, it's a color. We check which components are + // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. + const bool r_on = (color.r > 128); + const bool g_on = (color.g > 128); + const bool b_on = (color.b > 128); + + if (r_on) { + if (!b_on) { + return g_on ? hw_yellow : hw_red; + } + + // At least red+blue high (but not gray) -> White + return hw_white; + } else { + return (b_on && g_on) ? hw_white : hw_black; + } +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_jd79660.cpp b/esphome/components/epaper_spi/epaper_spi_jd79660.cpp new file mode 100644 index 0000000000..1cd1087c6b --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_jd79660.cpp @@ -0,0 +1,227 @@ +#include "epaper_spi_jd79660.h" +#include "colorconv.h" + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.jd79660"; + +/** Pixel color as 2bpp. Must match IC LUT values. */ +enum JD79660Color : uint8_t { + BLACK = 0b00, + WHITE = 0b01, + YELLOW = 0b10, + RED = 0b11, +}; + +/** Map RGB color to JD79660 BWYR hex color keys */ +static JD79660Color HOT color_to_hex(Color color) { + return color_to_bwyr(color, JD79660Color::BLACK, JD79660Color::WHITE, JD79660Color::YELLOW, JD79660Color::RED); +} + +void EPaperJD79660::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + EPaperBase::fill(color); + return; + } + + const auto pixel_color = color_to_hex(color); + + // We store 4 pixels per byte + this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6)); +} + +void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + const auto pixel_bits = color_to_hex(color); + const uint32_t pixel_position = x + y * this->get_width_internal(); + // We store 4 pixels per byte at LSB offsets 6, 4, 2, 0 + const uint32_t byte_position = pixel_position / 4; + const uint32_t bit_offset = 6 - ((pixel_position % 4) * 2); + const auto original = this->buffer_[byte_position]; + + this->buffer_[byte_position] = (original & (~(0b11 << bit_offset))) | // mask old 2bpp + (pixel_bits << bit_offset); // add new 2bpp +} + +bool EPaperJD79660::reset() { + // On entry state RESET set step, next state will be RESET_END + if (this->state_ == EPaperState::RESET) { + this->step_ = FSMState::RESET_STEP0_H; + } + + switch (this->step_) { + case FSMState::RESET_STEP0_H: + // Step #0: Reset H for some settle time. + + ESP_LOGVV(TAG, "reset #0"); + this->reset_pin_->digital_write(true); + + this->reset_duration_ = SLEEP_MS_RESET0; + this->step_ = FSMState::RESET_STEP1_L; + return false; // another loop: step #1 below + + case FSMState::RESET_STEP1_L: + // Step #1: Reset L pulse for slightly >1.5ms. + // This is actual reset trigger. + + ESP_LOGVV(TAG, "reset #1"); + + // As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window. + // So do not use FSM loop, and avoid other calls/logs during pulse below. + this->reset_pin_->digital_write(false); + delay(SLEEP_MS_RESET1); + this->reset_pin_->digital_write(true); + + this->reset_duration_ = SLEEP_MS_RESET2; + this->step_ = FSMState::RESET_STEP2_IDLECHECK; + return false; // another loop: step #2 below + + case FSMState::RESET_STEP2_IDLECHECK: + // Step #2: Basically finished. Check sanity, and move FSM to INITIALISE state + ESP_LOGVV(TAG, "reset #2"); + + if (!this->is_idle_()) { + // Expectation: Idle after reset + settle time. + // Improperly connected/unexpected hardware? + // Error path reproducable e.g. with disconnected VDD/... pins + // (optimally while busy_pin configured with local pulldown). + // -> Mark failed to avoid followup problems. + this->mark_failed(LOG_STR("Busy after reset")); + } + break; // End state loop below + + default: + // Unexpected step = bug? + this->mark_failed(); + } + + this->step_ = FSMState::INIT_STEP0_REGULARINIT; // reset for initialize state + return true; +} + +bool EPaperJD79660::initialise(bool partial) { + switch (this->step_) { + case FSMState::INIT_STEP0_REGULARINIT: + // Step #0: Regular init sequence + ESP_LOGVV(TAG, "init #0"); + if (!EPaperBase::initialise(partial)) { // Call parent impl + return false; // If parent should request another loop, do so + } + + // Fast init requested + supported? + if (partial && (this->fast_update_length_ > 0)) { + this->step_ = FSMState::INIT_STEP1_FASTINIT; + this->wait_for_idle_(true); // Must wait for idle before fastinit sequence in next loop + return false; // another loop: step #1 below + } + + break; // End state loop below + + case FSMState::INIT_STEP1_FASTINIT: + // Step #1: Fast init sequence + ESP_LOGVV(TAG, "init #1"); + this->write_fastinit_(); + break; // End state loop below + + default: + // Unexpected step = bug? + this->mark_failed(); + } + + this->step_ = FSMState::NONE; + return true; // Finished: State transition waits for idle +} + +bool EPaperJD79660::transfer_buffer_chunks_() { + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + const uint32_t start_time = App.get_loop_component_start_time(); + const auto buffer_length = this->buffer_length_; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; + + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } + } + } + + // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis()); + } + // Cleanup for next transfer + this->current_data_index_ = 0; + + // Finished with all buffer chunks + return true; +} + +void EPaperJD79660::write_fastinit_() { + // Undocumented register sequence in vendor register range. + // Related to Fast Init/Update. + // Should likely happen after regular init seq and power on, but before refresh. + // Might only work for some models with certain factory MTP. + // Please do not change without knowledge to avoid breakage. + + this->send_init_sequence_(this->fast_update_, this->fast_update_length_); +} + +bool EPaperJD79660::transfer_data() { + // For now always send full frame buffer in chunks. + // JD79660 might support partial window transfers. But sample code missing. + // And likely minimal impact, solely on SPI transfer time into RAM. + + if (this->current_data_index_ == 0) { + this->command(CMD_TRANSFER); + } + + return this->transfer_buffer_chunks_(); +} + +void EPaperJD79660::refresh_screen([[maybe_unused]] bool partial) { + ESP_LOGV(TAG, "Refresh"); + this->cmd_data(CMD_REFRESH, {(uint8_t) 0x00}); +} + +void EPaperJD79660::power_off() { + ESP_LOGV(TAG, "Power off"); + this->cmd_data(CMD_POWEROFF, {(uint8_t) 0x00}); +} + +void EPaperJD79660::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + // "Deepsleep between update": Ensure EPD sleep to avoid early hardware wearout! + this->cmd_data(CMD_DEEPSLEEP, {(uint8_t) 0xA5}); + + // Notes: + // - VDD: Some boards (Waveshare) with "clever reset logic" would allow switching off + // EPD VDD by pulling reset pin low for longer time. + // However, a) not all boards have this, b) reliable sequence timing is difficult, + // c) saving is not worth it after deepsleep command above. + // If needed: Better option is to drive VDD via MOSFET with separate enable pin. + // + // - Possible safe shutdown: + // EPaperBase::on_safe_shutdown() may also trigger deep_sleep() again. + // Regularly, in IDLE state, this does not make sense for this "deepsleep between update" model, + // but SPI sequence should simply be ignored by sleeping receiver. + // But if triggering during lengthy update, this quick SPI sleep sequence may have benefit. + // Optimally, EPDs should even be set all white for longer storage. + // But full sequence (>15s) not possible w/o app logic. +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_jd79660.h b/esphome/components/epaper_spi/epaper_spi_jd79660.h new file mode 100644 index 0000000000..4e488fe93e --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_jd79660.h @@ -0,0 +1,145 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * JD7966x IC driver implementation + * + * Currently tested with: + * - JD79660 (max res: 200x200) + * + * May also work for other JD7966x chipset family members with minimal adaptations. + * + * Capabilities: + * - HW frame buffer layout: + * 4 colors (gray0..3, commonly BWYR). Bytes consist of 4px/2bpp. + * Width must be rounded to multiple of 4. + * - Fast init/update (shorter wave forms): Yes. Controlled by CONF_FULL_UPDATE_EVERY. + * Needs undocumented fastinit sequence, based on likely vendor specific MTP content. + * - Partial transfer (transfer only changed window): No. Maybe possible by HW. + * - Partial refresh (refresh only changed window): No. Likely HW limit. + * + * @internal \c final saves few bytes by devirtualization. Remove \c final when subclassing. + */ +class EPaperJD79660 final : public EPaperBase { + public: + EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR), + fast_update_(fast_update), + fast_update_length_(fast_update_length) { + this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp) + this->buffer_length_ = this->row_width_ * height; + } + + void fill(Color color) override; + + protected: + /** Draw colored pixel into frame buffer */ + void draw_pixel_at(int x, int y, Color color) override; + + /** Reset (multistep sequence) + * @pre this->reset_pin_ != nullptr // cv.Required check + * @post Should be idle on successful reset. Can mark failures. + */ + bool reset() override; + + /** Initialise (multistep sequence) */ + bool initialise(bool partial) override; + + /** Buffer transfer */ + bool transfer_data() override; + + /** Power on: Already part of init sequence (likely needed there before transferring buffers). + * So nothing to do in FSM state. + */ + void power_on() override {} + + /** Refresh screen + * @param partial Ignored: Needed earlier in \a ::initialize + * @pre Must be idle. + * @post Should return to idle later after processing. + */ + void refresh_screen([[maybe_unused]] bool partial) override; + + /** Power off + * @pre Must be idle. + * @post Should return to idle later after processing. + * (latter will take long period like ~15-20s on actual refresh!) + */ + void power_off() override; + + /** Deepsleep: Must be used to avoid hardware wearout! + * @pre Must be idle. + * @post Will go busy, and not return idle till ::reset! + */ + void deep_sleep() override; + + /** Internal: Send fast init sequence via undocumented vendor registers + * @pre Must be directly after regular ::initialise sequence, before ::transfer_data + * @pre Must be idle. + * @post Should return to idle later after processing. + */ + void write_fastinit_(); + + /** Internal: Send raw buffer in chunks + * \retval true Finished + * \retval false Loop time elapsed. Need to call again next loop. + */ + bool transfer_buffer_chunks_(); + + /** @name IC commands @{ */ + static constexpr uint8_t CMD_POWEROFF = 0x02; + static constexpr uint8_t CMD_DEEPSLEEP = 0x07; + static constexpr uint8_t CMD_TRANSFER = 0x10; + static constexpr uint8_t CMD_REFRESH = 0x12; + /** @} */ + + /** State machine constants for \a step_ */ + enum class FSMState : uint8_t { + NONE = 0, //!< Initial/default value: Unused + + /* Reset state steps */ + RESET_STEP0_H, + RESET_STEP1_L, + RESET_STEP2_IDLECHECK, + + /* Init state steps */ + INIT_STEP0_REGULARINIT, + INIT_STEP1_FASTINIT, + }; + + /** Wait time (millisec) for first reset phase: High + * + * Wait via FSM loop. + */ + static constexpr uint16_t SLEEP_MS_RESET0 = 200; + + /** Wait time (millisec) for second reset phase: Low + * + * Holding Reset Low too long may trigger "clever reset" logic + * of e.g. Waveshare Rev2 boards: VDD is shut down via MOSFET, and IC + * will not report idle anymore! + * FSM loop may spuriously increase delay, e.g. >16ms. + * Therefore, sync wait below, as allowed (code rule "delays > 10ms not permitted"), + * yet only slightly exceeding known IC min req of >1.5ms. + */ + static constexpr uint16_t SLEEP_MS_RESET1 = 2; + + /** Wait time (millisec) for third reset phase: High + * + * Wait via FSM loop. + */ + static constexpr uint16_t SLEEP_MS_RESET2 = 200; + + // properties initialised in the constructor + const uint8_t *const fast_update_{}; + const uint16_t fast_update_length_{}; + + /** Counter for tracking substeps within FSM state */ + FSMState step_{FSMState::NONE}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/jd79660.py b/esphome/components/epaper_spi/models/jd79660.py new file mode 100644 index 0000000000..2d8830ebd2 --- /dev/null +++ b/esphome/components/epaper_spi/models/jd79660.py @@ -0,0 +1,86 @@ +import esphome.codegen as cg +from esphome.components.mipi import flatten_sequence +import esphome.config_validation as cv +from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN +from esphome.core import ID + +from ..display import CONF_INIT_SEQUENCE_ID +from . import EpaperModel + + +class JD79660(EpaperModel): + def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs): + super().__init__(name, class_name, **kwargs) + self.fast_update = fast_update + + def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required: + # Validate required pins, as C++ code will assume existence + if name in (CONF_RESET_PIN, CONF_BUSY_PIN): + return cv.Required(name) + + # Delegate to parent + return super().option(name, fallback) + + def get_constructor_args(self, config) -> tuple: + # Resembles init_sequence handling for fast_update config + if self.fast_update is None: + fast_update = cg.nullptr, 0 + else: + flat_fast_update = flatten_sequence(self.fast_update) + fast_update = ( + cg.static_const_array( + ID( + config[CONF_INIT_SEQUENCE_ID].id + "_fast_update", type=cg.uint8 + ), + flat_fast_update, + ), + len(flat_fast_update), + ) + return (*fast_update,) + + +jd79660 = JD79660( + "jd79660", + # Specified refresh times are ~20s (full) or ~15s (fast) due to BWRY. + # So disallow low update intervals (with safety margin), to avoid e.g. FSM update loops. + # Even less frequent intervals (min/h) highly recommended to optimize lifetime! + minimum_update_interval="30s", + # SPI rate: From spec comparisons, IC should allow SCL write cycles up to 10MHz rate. + # Existing code samples also prefer 10MHz. So justifies as default. + # Decrease value further in user config if needed (e.g. poor cabling). + data_rate="10MHz", + # No need to set optional reset_duration: + # Code requires multistep reset sequence with precise timings + # according to data sheet or samples. +) + +# Waveshare 1.54-G +# +# Device may have specific factory provisioned MTP content to facilitate vendor register features like fast init. +# Vendor specific init derived from vendor sample code +# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/1in54_e-Paper_G/ESP32/EPD_1in54g.cpp> +# Compatible MIT license, see esphome/LICENSE file. +# +# fmt: off +jd79660.extend( + "Waveshare-1.54in-G", + width=200, + height=200, + + initsequence=( + (0x4D, 0x78,), + (0x00, 0x0F, 0x29,), + (0x06, 0x0d, 0x12, 0x30, 0x20, 0x19, 0x2a, 0x22,), + (0x50, 0x37,), + (0x61, 200 // 256, 200 % 256, 200 // 256, 200 % 256,), # RES: 200x200 fixed + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), + fast_update=( + (0xE0, 0x02,), + (0xE6, 0x5D,), + (0xA5, 0x00,), + ), +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 621a819c3c..333ab567cd 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -25,6 +25,22 @@ display: lambda: |- it.circle(64, 64, 50, Color::BLACK); + - platform: epaper_spi + spi_id: spi_bus + model: waveshare-1.54in-G + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + - platform: epaper_spi spi_id: spi_bus model: waveshare-2.13in-v3 From a43e3e59483a239467d1db8201522db25291371d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Feb 2026 22:19:20 +0100 Subject: [PATCH 125/251] [dashboard] Close WebSocket after process exit to prevent zombie connections (#13834) --- esphome/dashboard/web_server.py | 1 + tests/dashboard/test_web_server.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index f94d8eea22..da50279864 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -317,6 +317,7 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl # Check if the proc was not forcibly closed _LOGGER.info("Process exited with return code %s", returncode) self.write_message({"event": "exit", "code": returncode}) + self.close() def on_close(self) -> None: # Check if proc exists (if 'start' has been run) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 10ca6061e6..7642876ee5 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -29,7 +29,7 @@ from esphome.dashboard.entries import ( bool_to_entry_state, ) from esphome.dashboard.models import build_importable_device_dict -from esphome.dashboard.web_server import DashboardSubscriber +from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket from esphome.zeroconf import DiscoveredImport from .common import get_fixture_path @@ -1654,3 +1654,25 @@ async def test_websocket_check_origin_multiple_trusted_domains( assert data["event"] == "initial_state" finally: ws.close() + + +def test_proc_on_exit_calls_close() -> None: + """Test _proc_on_exit sends exit event and closes the WebSocket.""" + handler = Mock(spec=EsphomeCommandWebSocket) + handler._is_closed = False + + EsphomeCommandWebSocket._proc_on_exit(handler, 0) + + handler.write_message.assert_called_once_with({"event": "exit", "code": 0}) + handler.close.assert_called_once() + + +def test_proc_on_exit_skips_when_already_closed() -> None: + """Test _proc_on_exit does nothing when WebSocket is already closed.""" + handler = Mock(spec=EsphomeCommandWebSocket) + handler._is_closed = True + + EsphomeCommandWebSocket._proc_on_exit(handler, 0) + + handler.write_message.assert_not_called() + handler.close.assert_not_called() From 7b40e8afcb5c60ec15436da1e5b4588bd028df81 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:21:37 +0100 Subject: [PATCH 126/251] [epaper_spi] Declare leaf classes final (#13776) --- esphome/components/epaper_spi/epaper_spi_spectra_e6.h | 2 +- esphome/components/epaper_spi/epaper_waveshare.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index b8dbf0b0c5..9c251068af 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -4,7 +4,7 @@ namespace esphome::epaper_spi { -class EPaperSpectraE6 : public EPaperBase { +class EPaperSpectraE6 final : public EPaperBase { public: EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, size_t init_sequence_length) diff --git a/esphome/components/epaper_spi/epaper_waveshare.h b/esphome/components/epaper_spi/epaper_waveshare.h index 15fb575ca0..d3ad313d92 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.h +++ b/esphome/components/epaper_spi/epaper_waveshare.h @@ -6,7 +6,7 @@ namespace esphome::epaper_spi { /** * An epaper display that needs LUTs to be sent to it. */ -class EpaperWaveshare : public EPaperMono { +class EpaperWaveshare final : public EPaperMono { public: EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut, From 41fedaedb3aea2a93621f7715e5d6588414eb696 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Feb 2026 08:26:47 -0600 Subject: [PATCH 127/251] [udp] Eliminate per-loop heap allocation using std::span (#13838) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/codegen.py | 1 + .../packet_transport/packet_transport.cpp | 4 +- .../packet_transport/packet_transport.h | 5 ++- esphome/components/udp/__init__.py | 16 ++++++-- .../udp/packet_transport/udp_transport.cpp | 2 +- esphome/components/udp/udp_component.cpp | 8 ++-- esphome/components/udp/udp_component.h | 6 ++- esphome/cpp_types.py | 1 + tests/integration/test_udp.py | 39 ++++++++++++++++--- 9 files changed, 62 insertions(+), 20 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 4a2a5975c6..c5283f4967 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -87,6 +87,7 @@ from esphome.cpp_types import ( # noqa: F401 size_t, std_ns, std_shared_ptr, + std_span, std_string, std_string_ref, std_vector, diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index cefe9a604e..365a5f2ec7 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) { /** * Process a received packet */ -void PacketTransport::process_(const std::vector<uint8_t> &data) { +void PacketTransport::process_(std::span<const uint8_t> data) { auto ping_key_seen = !this->ping_pong_enable_; - PacketDecoder decoder((data.data()), data.size()); + PacketDecoder decoder(data.data(), data.size()); char namebuf[256]{}; uint8_t byte; FuData rdata{}; diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 86ec564fce..57f40874b5 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -9,8 +9,9 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -#include <vector> #include <map> +#include <span> +#include <vector> /** * Providing packet encoding functions for exchanging data with a remote host. @@ -113,7 +114,7 @@ class PacketTransport : public PollingComponent { virtual bool should_send() { return true; } // to be called by child classes when a data packet is received. - void process_(const std::vector<uint8_t> &data); + void process_(std::span<const uint8_t> data); void send_data_(bool all); void flush_(); void add_data_(uint8_t key, const char *id, float data); diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 8252e35023..bfaa5f2516 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -13,7 +13,7 @@ from esphome.components.packet_transport import ( import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID -from esphome.cpp_generator import literal +from esphome.cpp_generator import MockObj CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -23,8 +23,12 @@ MULTI_CONF = True udp_ns = cg.esphome_ns.namespace("udp") UDPComponent = udp_ns.class_("UDPComponent", cg.Component) UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action) -trigger_args = cg.std_vector.template(cg.uint8) trigger_argname = "data" +# Listener callback type (non-owning span from UDP component) +listener_args = cg.std_span.template(cg.uint8.operator("const")) +listener_argtype = [(listener_args, trigger_argname)] +# Automation/trigger type (owned vector, safe for deferred actions like delay) +trigger_args = cg.std_vector.template(cg.uint8) trigger_argtype = [(trigger_args, trigger_argname)] CONF_ADDRESSES = "addresses" @@ -118,7 +122,13 @@ async def to_code(config): trigger_id, trigger_argtype, on_receive ) trigger_lambda = await cg.process_lambda( - trigger.trigger(literal(trigger_argname)), trigger_argtype + trigger.trigger( + cg.std_vector.template(cg.uint8)( + MockObj(trigger_argname).begin(), + MockObj(trigger_argname).end(), + ) + ), + listener_argtype, ) cg.add(var.add_listener(trigger_lambda)) cg.add(var.set_should_listen()) diff --git a/esphome/components/udp/packet_transport/udp_transport.cpp b/esphome/components/udp/packet_transport/udp_transport.cpp index f3e33573a5..b5e73af777 100644 --- a/esphome/components/udp/packet_transport/udp_transport.cpp +++ b/esphome/components/udp/packet_transport/udp_transport.cpp @@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); } void UDPTransport::setup() { PacketTransport::setup(); if (!this->providers_.empty() || this->is_encrypted_()) { - this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); }); + this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); }); } } diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 947a59dfa9..c144212ecf 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -103,8 +103,8 @@ void UDPComponent::setup() { } void UDPComponent::loop() { - auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE); if (this->should_listen_) { + std::array<uint8_t, MAX_PACKET_SIZE> buf; for (;;) { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) auto len = this->listen_socket_->read(buf.data(), buf.size()); @@ -116,9 +116,9 @@ void UDPComponent::loop() { #endif if (len <= 0) break; - buf.resize(len); - ESP_LOGV(TAG, "Received packet of length %zu", len); - this->packet_listeners_.call(buf); + size_t packet_len = static_cast<size_t>(len); + ESP_LOGV(TAG, "Received packet of length %zu", packet_len); + this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len)); } } } diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index 9967e4dbbb..7fd6308065 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -10,7 +10,9 @@ #ifdef USE_SOCKET_IMPL_LWIP_TCP #include <WiFiUdp.h> #endif +#include <array> #include <initializer_list> +#include <span> #include <vector> namespace esphome::udp { @@ -26,7 +28,7 @@ class UDPComponent : public Component { void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; } void set_should_broadcast() { this->should_broadcast_ = true; } void set_should_listen() { this->should_listen_ = true; } - void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) { + void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) { this->packet_listeners_.add(std::move(listener)); } void setup() override; @@ -41,7 +43,7 @@ class UDPComponent : public Component { uint16_t broadcast_port_{}; bool should_broadcast_{}; bool should_listen_{}; - CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{}; + CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{}; #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr; diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 7001c38857..6d255bc0be 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -12,6 +12,7 @@ std_shared_ptr = std_ns.class_("shared_ptr") std_string = std_ns.class_("string") std_string_ref = std_ns.namespace("string &") std_vector = std_ns.class_("vector") +std_span = std_ns.class_("span") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") diff --git a/tests/integration/test_udp.py b/tests/integration/test_udp.py index 74c7ef60e3..2187d13814 100644 --- a/tests/integration/test_udp.py +++ b/tests/integration/test_udp.py @@ -93,23 +93,34 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]] sock.close() +def _get_free_udp_port() -> int: + """Get a free UDP port by binding to port 0 and releasing.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + sock.close() + return port + + @pytest.mark.asyncio async def test_udp_send_receive( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test UDP component can send messages with multiple addresses configured.""" - # Track log lines to verify dump_config output + """Test UDP component can send and receive messages.""" log_lines: list[str] = [] + receive_event = asyncio.Event() def on_log_line(line: str) -> None: log_lines.append(line) + if "Received UDP:" in line: + receive_event.set() - async with udp_listener() as (udp_port, receiver): - # Replace placeholders in the config - config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1)) - config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port)) + async with udp_listener() as (broadcast_port, receiver): + listen_port = _get_free_udp_port() + config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port)) + config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port)) async with ( run_compiled(config, line_callback=on_log_line), @@ -169,3 +180,19 @@ async def test_udp_send_receive( assert "Address: 127.0.0.2" in log_text, ( f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}" ) + + # Test receiving a UDP packet (exercises on_receive with std::span) + test_payload = b"TEST_RECEIVE_UDP" + send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + send_sock.sendto(test_payload, ("127.0.0.1", listen_port)) + finally: + send_sock.close() + + try: + await asyncio.wait_for(receive_event.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"on_receive did not fire. Expected 'Received UDP:' in logs. " + f"Last log lines: {log_lines[-20:]}" + ) From 28b9487b25043dcbecb12068d617bc1909acb22f Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Feb 2026 18:52:05 +0100 Subject: [PATCH 128/251] [nrf52,logger] fix printk (#13874) --- esphome/components/logger/logger_zephyr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index ef1702c5c1..1fc0acd573 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) { #ifdef CONFIG_PRINTK // Requires the debug component and an active SWD connection. // It is used for pyocd rtt -t nrf52840 - k_str_out(const_cast<char *>(msg), len); + printk("%.*s", static_cast<int>(len), msg); #endif if (this->uart_dev_ == nullptr) { return; From 756f1c6b7e8162752af7ca8191e2eff71a122e3a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:53:43 +1100 Subject: [PATCH 129/251] [lvgl] Fix crash with unconfigured `top_layer` (#13846) --- esphome/components/lvgl/schemas.py | 1 + tests/components/lvgl/test.host.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 45d933c00e..2aeeedbd10 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -436,6 +436,7 @@ def container_schema(widget_type: WidgetType, extras=None): schema = schema.extend(widget_type.schema) def validator(value): + value = value or {} return append_layout_schema(schema, value)(value) return validator diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 00a8cd8c01..f84156c9d8 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -20,6 +20,8 @@ lvgl: - id: lvgl_0 default_font: space16 displays: sdl0 + top_layer: + - id: lvgl_1 displays: sdl1 on_idle: From 140ec0639ca3f0e8cac0c839d5e93f67cbee2621 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:24:45 -0600 Subject: [PATCH 130/251] [api] Elide empty message construction in protobuf dispatch (#13871) --- esphome/components/api/api_connection.cpp | 17 ++-- esphome/components/api/api_connection.h | 24 +++-- esphome/components/api/api_pb2_service.cpp | 106 ++++++++------------- esphome/components/api/api_pb2_service.h | 62 ++++++------ script/api_protobuf/api_protobuf.py | 53 ++++++----- 5 files changed, 117 insertions(+), 145 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2aa5956f24..efc3d210b4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -283,7 +283,7 @@ void APIConnection::loop() { #endif } -bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { +bool APIConnection::send_disconnect_response() { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop @@ -292,7 +292,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { DisconnectResponse resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); } -void APIConnection::on_disconnect_response(const DisconnectResponse &value) { +void APIConnection::on_disconnect_response() { this->helper_->close(); this->flags_.remove = true; } @@ -1095,7 +1095,7 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); } -void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { +void APIConnection::unsubscribe_bluetooth_le_advertisements() { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { @@ -1121,8 +1121,7 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); } -bool APIConnection::send_subscribe_bluetooth_connections_free_response( - const SubscribeBluetoothConnectionsFreeRequest &msg) { +bool APIConnection::send_subscribe_bluetooth_connections_free_response() { bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this); return true; } @@ -1491,12 +1490,12 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -bool APIConnection::send_ping_response(const PingRequest &msg) { +bool APIConnection::send_ping_response() { PingResponse resp; return this->send_message(resp, PingResponse::MESSAGE_TYPE); } -bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { +bool APIConnection::send_device_info_response() { DeviceInfoResponse resp{}; resp.name = StringRef(App.get_name()); resp.friendly_name = StringRef(App.get_friendly_name()); @@ -1746,9 +1745,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { - state_subs_at_ = 0; -} +void APIConnection::subscribe_home_assistant_states() { state_subs_at_ = 0; } #endif bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { if (this->flags_.remove) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 40e4fd61c1..935393b2da 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -127,7 +127,7 @@ class APIConnection final : public APIServerConnection { #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; - void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; + void unsubscribe_bluetooth_le_advertisements() override; void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; @@ -136,7 +136,7 @@ class APIConnection final : public APIServerConnection { void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; - bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override; + bool send_subscribe_bluetooth_connections_free_response() override; void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; #endif @@ -187,8 +187,8 @@ class APIConnection final : public APIServerConnection { void update_command(const UpdateCommandRequest &msg) override; #endif - void on_disconnect_response(const DisconnectResponse &value) override; - void on_ping_response(const PingResponse &value) override { + void on_disconnect_response() override; + void on_ping_response() override { // we initiated ping this->flags_.sent_ping = false; } @@ -199,11 +199,11 @@ class APIConnection final : public APIServerConnection { void on_get_time_response(const GetTimeResponse &value) override; #endif bool send_hello_response(const HelloRequest &msg) override; - bool send_disconnect_response(const DisconnectRequest &msg) override; - bool send_ping_response(const PingRequest &msg) override; - bool send_device_info_response(const DeviceInfoRequest &msg) override; - void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } - void subscribe_states(const SubscribeStatesRequest &msg) override { + bool send_disconnect_response() override; + bool send_ping_response() override; + bool send_device_info_response() override; + void list_entities() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } + void subscribe_states() override { this->flags_.state_subscription = true; // Start initial state iterator only if no iterator is active // If list_entities is running, we'll start initial_state when it completes @@ -217,12 +217,10 @@ class APIConnection final : public APIServerConnection { App.schedule_dump_config(); } #ifdef USE_API_HOMEASSISTANT_SERVICES - void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { - this->flags_.service_call_subscription = true; - } + void subscribe_homeassistant_services() override { this->flags_.service_call_subscription = true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES - void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; + void subscribe_home_assistant_states() override; #endif #ifdef USE_API_USER_DEFINED_ACTIONS void execute_service(const ExecuteServiceRequest &msg) override; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index af0a2d0ca2..df66b6eb83 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -15,6 +15,9 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name, const DumpBuffer dump_buf; ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf)); } +void APIServerConnectionBase::log_receive_message_(const LogString *name) { + ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name)); +} #endif void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { @@ -29,66 +32,52 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } case DisconnectRequest::MESSAGE_TYPE: { - DisconnectRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_disconnect_request"), msg); + this->log_receive_message_(LOG_STR("on_disconnect_request")); #endif - this->on_disconnect_request(msg); + this->on_disconnect_request(); break; } case DisconnectResponse::MESSAGE_TYPE: { - DisconnectResponse msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_disconnect_response"), msg); + this->log_receive_message_(LOG_STR("on_disconnect_response")); #endif - this->on_disconnect_response(msg); + this->on_disconnect_response(); break; } case PingRequest::MESSAGE_TYPE: { - PingRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_ping_request"), msg); + this->log_receive_message_(LOG_STR("on_ping_request")); #endif - this->on_ping_request(msg); + this->on_ping_request(); break; } case PingResponse::MESSAGE_TYPE: { - PingResponse msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_ping_response"), msg); + this->log_receive_message_(LOG_STR("on_ping_response")); #endif - this->on_ping_response(msg); + this->on_ping_response(); break; } case DeviceInfoRequest::MESSAGE_TYPE: { - DeviceInfoRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_device_info_request"), msg); + this->log_receive_message_(LOG_STR("on_device_info_request")); #endif - this->on_device_info_request(msg); + this->on_device_info_request(); break; } case ListEntitiesRequest::MESSAGE_TYPE: { - ListEntitiesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_list_entities_request"), msg); + this->log_receive_message_(LOG_STR("on_list_entities_request")); #endif - this->on_list_entities_request(msg); + this->on_list_entities_request(); break; } case SubscribeStatesRequest::MESSAGE_TYPE: { - SubscribeStatesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_states_request")); #endif - this->on_subscribe_states_request(msg); + this->on_subscribe_states_request(); break; } case SubscribeLogsRequest::MESSAGE_TYPE: { @@ -146,12 +135,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #endif #ifdef USE_API_HOMEASSISTANT_SERVICES case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { - SubscribeHomeassistantServicesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request")); #endif - this->on_subscribe_homeassistant_services_request(msg); + this->on_subscribe_homeassistant_services_request(); break; } #endif @@ -166,12 +153,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #ifdef USE_API_HOMEASSISTANT_STATES case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { - SubscribeHomeAssistantStatesRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request")); #endif - this->on_subscribe_home_assistant_states_request(msg); + this->on_subscribe_home_assistant_states_request(); break; } #endif @@ -375,23 +360,19 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #endif #ifdef USE_BLUETOOTH_PROXY case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { - SubscribeBluetoothConnectionsFreeRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request")); #endif - this->on_subscribe_bluetooth_connections_free_request(msg); + this->on_subscribe_bluetooth_connections_free_request(); break; } #endif #ifdef USE_BLUETOOTH_PROXY case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { - UnsubscribeBluetoothLEAdvertisementsRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg); + this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request")); #endif - this->on_unsubscribe_bluetooth_le_advertisements_request(msg); + this->on_unsubscribe_bluetooth_le_advertisements_request(); break; } #endif @@ -647,36 +628,29 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) { this->on_fatal_error(); } } -void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { - if (!this->send_disconnect_response(msg)) { +void APIServerConnection::on_disconnect_request() { + if (!this->send_disconnect_response()) { this->on_fatal_error(); } } -void APIServerConnection::on_ping_request(const PingRequest &msg) { - if (!this->send_ping_response(msg)) { +void APIServerConnection::on_ping_request() { + if (!this->send_ping_response()) { this->on_fatal_error(); } } -void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { - if (!this->send_device_info_response(msg)) { +void APIServerConnection::on_device_info_request() { + if (!this->send_device_info_response()) { this->on_fatal_error(); } } -void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); } -void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { - this->subscribe_states(msg); -} +void APIServerConnection::on_list_entities_request() { this->list_entities(); } +void APIServerConnection::on_subscribe_states_request() { this->subscribe_states(); } void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); } #ifdef USE_API_HOMEASSISTANT_SERVICES -void APIServerConnection::on_subscribe_homeassistant_services_request( - const SubscribeHomeassistantServicesRequest &msg) { - this->subscribe_homeassistant_services(msg); -} +void APIServerConnection::on_subscribe_homeassistant_services_request() { this->subscribe_homeassistant_services(); } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { - this->subscribe_home_assistant_states(msg); -} +void APIServerConnection::on_subscribe_home_assistant_states_request() { this->subscribe_home_assistant_states(); } #endif #ifdef USE_API_USER_DEFINED_ACTIONS void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } @@ -793,17 +767,15 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo } #endif #ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_subscribe_bluetooth_connections_free_request( - const SubscribeBluetoothConnectionsFreeRequest &msg) { - if (!this->send_subscribe_bluetooth_connections_free_response(msg)) { +void APIServerConnection::on_subscribe_bluetooth_connections_free_request() { + if (!this->send_subscribe_bluetooth_connections_free_response()) { this->on_fatal_error(); } } #endif #ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { - this->unsubscribe_bluetooth_le_advertisements(msg); +void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request() { + this->unsubscribe_bluetooth_le_advertisements(); } #endif #ifdef USE_BLUETOOTH_PROXY diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e2bc1609ed..b8c9e4da6f 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -14,6 +14,7 @@ class APIServerConnectionBase : public ProtoService { protected: void log_send_message_(const char *name, const char *dump); void log_receive_message_(const LogString *name, const ProtoMessage &msg); + void log_receive_message_(const LogString *name); public: #endif @@ -28,15 +29,15 @@ class APIServerConnectionBase : public ProtoService { virtual void on_hello_request(const HelloRequest &value){}; - virtual void on_disconnect_request(const DisconnectRequest &value){}; - virtual void on_disconnect_response(const DisconnectResponse &value){}; - virtual void on_ping_request(const PingRequest &value){}; - virtual void on_ping_response(const PingResponse &value){}; - virtual void on_device_info_request(const DeviceInfoRequest &value){}; + virtual void on_disconnect_request(){}; + virtual void on_disconnect_response(){}; + virtual void on_ping_request(){}; + virtual void on_ping_response(){}; + virtual void on_device_info_request(){}; - virtual void on_list_entities_request(const ListEntitiesRequest &value){}; + virtual void on_list_entities_request(){}; - virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){}; + virtual void on_subscribe_states_request(){}; #ifdef USE_COVER virtual void on_cover_command_request(const CoverCommandRequest &value){}; @@ -61,14 +62,14 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; + virtual void on_subscribe_homeassistant_services_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; + virtual void on_subscribe_home_assistant_states_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES @@ -147,12 +148,11 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){}; + virtual void on_subscribe_bluetooth_connections_free_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &value){}; + virtual void on_unsubscribe_bluetooth_le_advertisements_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY @@ -231,17 +231,17 @@ class APIServerConnectionBase : public ProtoService { class APIServerConnection : public APIServerConnectionBase { public: virtual bool send_hello_response(const HelloRequest &msg) = 0; - virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; - virtual bool send_ping_response(const PingRequest &msg) = 0; - virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; - virtual void list_entities(const ListEntitiesRequest &msg) = 0; - virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; + virtual bool send_disconnect_response() = 0; + virtual bool send_ping_response() = 0; + virtual bool send_device_info_response() = 0; + virtual void list_entities() = 0; + virtual void subscribe_states() = 0; virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; + virtual void subscribe_homeassistant_services() = 0; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; + virtual void subscribe_home_assistant_states() = 0; #endif #ifdef USE_API_USER_DEFINED_ACTIONS virtual void execute_service(const ExecuteServiceRequest &msg) = 0; @@ -331,11 +331,10 @@ class APIServerConnection : public APIServerConnectionBase { virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; #endif #ifdef USE_BLUETOOTH_PROXY - virtual bool send_subscribe_bluetooth_connections_free_response( - const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; + virtual bool send_subscribe_bluetooth_connections_free_response() = 0; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0; + virtual void unsubscribe_bluetooth_le_advertisements() = 0; #endif #ifdef USE_BLUETOOTH_PROXY virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0; @@ -363,17 +362,17 @@ class APIServerConnection : public APIServerConnectionBase { #endif protected: void on_hello_request(const HelloRequest &msg) override; - void on_disconnect_request(const DisconnectRequest &msg) override; - void on_ping_request(const PingRequest &msg) override; - void on_device_info_request(const DeviceInfoRequest &msg) override; - void on_list_entities_request(const ListEntitiesRequest &msg) override; - void on_subscribe_states_request(const SubscribeStatesRequest &msg) override; + void on_disconnect_request() override; + void on_ping_request() override; + void on_device_info_request() override; + void on_list_entities_request() override; + void on_subscribe_states_request() override; void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; #ifdef USE_API_HOMEASSISTANT_SERVICES - void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; + void on_subscribe_homeassistant_services_request() override; #endif #ifdef USE_API_HOMEASSISTANT_STATES - void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; + void on_subscribe_home_assistant_states_request() override; #endif #ifdef USE_API_USER_DEFINED_ACTIONS void on_execute_service_request(const ExecuteServiceRequest &msg) override; @@ -463,11 +462,10 @@ class APIServerConnection : public APIServerConnectionBase { void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; #endif #ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &msg) override; + void on_subscribe_bluetooth_connections_free_request() override; #endif #ifdef USE_BLUETOOTH_PROXY - void on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; + void on_unsubscribe_bluetooth_le_advertisements_request() override; #endif #ifdef USE_BLUETOOTH_PROXY void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4021a062ca..5fbc1137a8 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2270,10 +2270,13 @@ SOURCE_NAMES = { SOURCE_CLIENT: "SOURCE_CLIENT", } -RECEIVE_CASES: dict[int, tuple[str, str | None]] = {} +RECEIVE_CASES: dict[int, tuple[str, str | None, str]] = {} ifdefs: dict[str, str] = {} +# Track messages with no fields (empty messages) for parameter elision +EMPTY_MESSAGES: set[str] = set() + def get_opt( desc: descriptor.DescriptorProto, @@ -2504,26 +2507,26 @@ def build_service_message_type( # Only add ifdef when we're actually generating content if ifdef is not None: hout += f"#ifdef {ifdef}\n" - # Generate receive + # Generate receive handler and switch case func = f"on_{snake}" - hout += f"virtual void {func}(const {mt.name} &value){{}};\n" - case = "" - case += f"{mt.name} msg;\n" - # Check if this message has any fields (excluding deprecated ones) has_fields = any(not field.options.deprecated for field in mt.field) - if has_fields: - # Normal case: decode the message + is_empty = not has_fields + if is_empty: + EMPTY_MESSAGES.add(mt.name) + hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" + case = "" + if not is_empty: + case += f"{mt.name} msg;\n" case += "msg.decode(msg_data, msg_size);\n" - else: - # Empty message optimization: skip decode since there are no fields - case += "// Empty message: no decode needed\n" if log: case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n' + if is_empty: + case += f'this->log_receive_message_(LOG_STR("{func}"));\n' + else: + case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n' case += "#endif\n" - case += f"this->{func}(msg);\n" + case += f"this->{func}({'msg' if not is_empty else ''});\n" case += "break;" - # Store the message name and ifdef with the case for later use RECEIVE_CASES[id_] = (case, ifdef, mt.name) # Only close ifdef if we opened it @@ -2839,6 +2842,7 @@ static const char *const TAG = "api.service"; hpp += ( " void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n" ) + hpp += " void log_receive_message_(const LogString *name);\n" hpp += " public:\n" hpp += "#endif\n\n" @@ -2862,6 +2866,9 @@ static const char *const TAG = "api.service"; cpp += " DumpBuffer dump_buf;\n" cpp += ' ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));\n' cpp += "}\n" + cpp += f"void {class_name}::log_receive_message_(const LogString *name) {{\n" + cpp += ' ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name));\n' + cpp += "}\n" cpp += "#endif\n\n" for mt in file.message_type: @@ -2929,22 +2936,22 @@ static const char *const TAG = "api.service"; hpp_protected += f"#ifdef {ifdef}\n" cpp += f"#ifdef {ifdef}\n" - hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" + is_empty = inp in EMPTY_MESSAGES + param = "" if is_empty else f"const {inp} &msg" + arg = "" if is_empty else "msg" - # For non-void methods, generate a send_ method instead of return-by-value + hpp_protected += f" void {on_func}({param}) override;\n" if is_void: - hpp += f" virtual void {func}(const {inp} &msg) = 0;\n" + hpp += f" virtual void {func}({param}) = 0;\n" else: - hpp += f" virtual bool send_{func}_response(const {inp} &msg) = 0;\n" + hpp += f" virtual bool send_{func}_response({param}) = 0;\n" - cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" - - # No authentication check here - it's done in read_message + cpp += f"void {class_name}::{on_func}({param}) {{\n" body = "" if is_void: - body += f"this->{func}(msg);\n" + body += f"this->{func}({arg});\n" else: - body += f"if (!this->send_{func}_response(msg)) {{\n" + body += f"if (!this->send_{func}_response({arg})) {{\n" body += " this->on_fatal_error();\n" body += "}\n" From eb6a6f8d0d6a408138d0dd2fd2143a8af9b2ee8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:05 -0600 Subject: [PATCH 131/251] [web_server_idf] Remove unused host() method (#13869) --- esphome/components/web_server_idf/web_server_idf.cpp | 2 -- esphome/components/web_server_idf/web_server_idf.h | 1 - 2 files changed, 3 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 9860810452..39e6b7a790 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -258,8 +258,6 @@ StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) co return StringRef(buffer.data(), decoded_len); } -std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } - void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 817f47da79..1760544963 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -121,7 +121,6 @@ class AsyncWebServerRequest { char buffer[URL_BUF_SIZE]; return std::string(this->url_to(buffer)); } - std::string host() const; // NOLINTNEXTLINE(readability-identifier-naming) size_t contentLength() const { return this->req_->content_len; } From 6ee185c58aa047dde7e933c4d5a1b47c42f6572a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:23 -0600 Subject: [PATCH 132/251] [dashboard] Use resolve/relative_to for download path validation (#13867) --- esphome/dashboard/web_server.py | 17 ++++-- tests/dashboard/test_web_server.py | 93 +++++++++++++++++++++++++----- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index da50279864..00974bf460 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1054,17 +1054,26 @@ class DownloadBinaryRequestHandler(BaseHandler): # fallback to type=, but prioritize file= file_name = self.get_argument("type", None) file_name = self.get_argument("file", file_name) - if file_name is None: + if file_name is None or not file_name.strip(): self.send_error(400) return - file_name = file_name.replace("..", "").lstrip("/") # get requested download name, or build it based on filename download_name = self.get_argument( "download", f"{storage_json.name}-{file_name}", ) - path = storage_json.firmware_bin_path.parent.joinpath(file_name) + if storage_json.firmware_bin_path is None: + self.send_error(404) + return + + base_dir = storage_json.firmware_bin_path.parent.resolve() + path = base_dir.joinpath(file_name).resolve() + try: + path.relative_to(base_dir) + except ValueError: + self.send_error(403) + return if not path.is_file(): args = ["esphome", "idedata", settings.rel_path(configuration)] @@ -1078,7 +1087,7 @@ class DownloadBinaryRequestHandler(BaseHandler): found = False for image in idedata.extra_flash_images: - if image.path.endswith(file_name): + if image.path.as_posix().endswith(file_name): path = image.path download_name = file_name found = True diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 7642876ee5..9ea7a5164b 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -8,6 +8,7 @@ import gzip import json import os from pathlib import Path +import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -421,7 +422,7 @@ async def test_download_binary_handler_idedata_fallback( # Mock idedata response mock_image = Mock() - mock_image.path = str(bootloader_file) + mock_image.path = bootloader_file mock_idedata_instance = Mock() mock_idedata_instance.extra_flash_images = [mock_image] mock_idedata.return_value = mock_idedata_instance @@ -528,14 +529,22 @@ async def test_download_binary_handler_subdirectory_file_url_encoded( @pytest.mark.asyncio @pytest.mark.usefixtures("mock_ext_storage_path") @pytest.mark.parametrize( - "attack_path", + ("attack_path", "expected_code"), [ - pytest.param("../../../secrets.yaml", id="basic_traversal"), - pytest.param("..%2F..%2F..%2Fsecrets.yaml", id="url_encoded"), - pytest.param("zephyr/../../../secrets.yaml", id="traversal_with_prefix"), - pytest.param("/etc/passwd", id="absolute_path"), - pytest.param("//etc/passwd", id="double_slash_absolute"), - pytest.param("....//secrets.yaml", id="multiple_dots"), + pytest.param("../../../secrets.yaml", 403, id="basic_traversal"), + pytest.param("..%2F..%2F..%2Fsecrets.yaml", 403, id="url_encoded"), + pytest.param("zephyr/../../../secrets.yaml", 403, id="traversal_with_prefix"), + pytest.param("/etc/passwd", 403, id="absolute_path"), + pytest.param("//etc/passwd", 403, id="double_slash_absolute"), + pytest.param( + "....//secrets.yaml", + # On Windows, Path.resolve() treats "..." and "...." as parent + # traversal (like ".."), so the path escapes base_dir -> 403. + # On Unix, "...." is a literal directory name that stays inside + # base_dir but doesn't exist -> 404. + 403 if sys.platform == "win32" else 404, + id="multiple_dots", + ), ], ) async def test_download_binary_handler_path_traversal_protection( @@ -543,11 +552,14 @@ async def test_download_binary_handler_path_traversal_protection( tmp_path: Path, mock_storage_json: MagicMock, attack_path: str, + expected_code: int, ) -> None: """Test that DownloadBinaryRequestHandler prevents path traversal attacks. - Verifies that attempts to use '..' in file paths are sanitized to prevent - accessing files outside the build directory. Tests multiple attack vectors. + Verifies that attempts to escape the build directory via '..' are rejected + using resolve()/relative_to() validation. Tests multiple attack vectors. + Real traversals that escape the base directory get 403. Paths like '....' + that resolve inside the base directory but don't exist get 404. """ # Create build structure build_dir = get_build_path(tmp_path, "test") @@ -565,14 +577,67 @@ async def test_download_binary_handler_path_traversal_protection( mock_storage.firmware_bin_path = firmware_file mock_storage_json.load.return_value = mock_storage - # Attempt path traversal attack - should be blocked - with pytest.raises(HTTPClientError) as exc_info: + # Mock async_run_system_command so paths that pass validation but don't exist + # return 404 deterministically without spawning a real subprocess. + with ( + patch( + "esphome.dashboard.web_server.async_run_system_command", + new_callable=AsyncMock, + return_value=(2, "", ""), + ), + pytest.raises(HTTPClientError) as exc_info, + ): await dashboard.fetch( f"/download.bin?configuration=test.yaml&file={attack_path}", method="GET", ) - # Should get 404 (file not found after sanitization) or 500 (idedata fails) - assert exc_info.value.code in (404, 500) + assert exc_info.value.code == expected_code + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_no_firmware_bin_path( + dashboard: DashboardTestHelper, + mock_storage_json: MagicMock, +) -> None: + """Test that download returns 404 when firmware_bin_path is None. + + This covers configs created by StorageJSON.from_wizard() where no + firmware has been compiled yet. + """ + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = None + mock_storage_json.load.return_value = mock_storage + + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=firmware.bin", + method="GET", + ) + assert exc_info.value.code == 404 + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +@pytest.mark.parametrize("file_value", ["", "%20%20", "%20"]) +async def test_download_binary_handler_empty_file_name( + dashboard: DashboardTestHelper, + mock_storage_json: MagicMock, + file_value: str, +) -> None: + """Test that download returns 400 for empty or whitespace-only file names.""" + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = Path("/fake/firmware.bin") + mock_storage_json.load.return_value = mock_storage + + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + f"/download.bin?configuration=test.yaml&file={file_value}", + method="GET", + ) + assert exc_info.value.code == 400 @pytest.mark.asyncio From 5370687001745b2dfd590426eb3008158469a56b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:41 -0600 Subject: [PATCH 133/251] [wizard] Use secrets module for fallback AP password generation (#13864) --- esphome/wizard.py | 3 +-- tests/unit_tests/test_wizard.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/esphome/wizard.py b/esphome/wizard.py index 4b74847996..f83342cc6a 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,6 +1,5 @@ import base64 from pathlib import Path -import random import secrets import string from typing import Literal, NotRequired, TypedDict, Unpack @@ -130,7 +129,7 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: if len(ap_name) > 32: ap_name = ap_name_base kwargs["fallback_name"] = ap_name - kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) + kwargs["fallback_psk"] = "".join(secrets.choice(letters) for _ in range(12)) base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index eb44c1c20f..0ce89230d8 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Any -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest from pytest import MonkeyPatch @@ -632,3 +632,14 @@ def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch): # rpipico doesn't support WiFi, so no api_encryption_key or ota_password assert "api_encryption_key" not in call_kwargs assert "ota_password" not in call_kwargs + + +def test_fallback_psk_uses_secrets_choice( + default_config: dict[str, Any], +) -> None: + """Test that fallback PSK is generated using secrets.choice.""" + with patch("esphome.wizard.secrets.choice", return_value="X") as mock_choice: + config = wz.wizard_file(**default_config) + + assert 'password: "XXXXXXXXXXXX"' in config + assert mock_choice.call_count == 12 From e24528c842f6cbab9b71a07d6d738006fd5dd509 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:25:59 -0600 Subject: [PATCH 134/251] [analyze-memory] Attribute CSWTCH symbols from SDK archives (#13850) --- esphome/analyze_memory/__init__.py | 156 +++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 42 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index d8c941e76f..d8abc8bafb 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -397,47 +397,38 @@ class MemoryAnalyzer: return pioenvs_dir return None - def _scan_cswtch_in_objects( - self, obj_dir: Path - ) -> dict[str, list[tuple[str, int]]]: - """Scan object files for CSWTCH symbols using a single nm invocation. + @staticmethod + def _parse_nm_cswtch_output( + output: str, + base_dir: Path | None, + cswtch_map: dict[str, list[tuple[str, int]]], + ) -> None: + """Parse nm output for CSWTCH symbols and add to cswtch_map. - Uses ``nm --print-file-name -S`` on all ``.o`` files at once. - Output format: ``/path/to/file.o:address size type name`` + Handles both ``.o`` files and ``.a`` archives. + + nm output formats:: + + .o files: /path/file.o:hex_addr hex_size type name + .a files: /path/lib.a:member.o:hex_addr hex_size type name + + For ``.o`` files, paths are made relative to *base_dir* when possible. + For ``.a`` archives (detected by ``:`` in the file portion), paths are + formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``). Args: - obj_dir: Directory containing object files (.pioenvs/<env>/) - - Returns: - Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples. + output: Raw stdout from ``nm --print-file-name -S``. + base_dir: Base directory for computing relative paths of ``.o`` files. + Pass ``None`` when scanning archives outside the build tree. + cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list. """ - cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) - - if not self.nm_path: - return cswtch_map - - # Find all .o files recursively, sorted for deterministic output - obj_files = sorted(obj_dir.rglob("*.o")) - if not obj_files: - return cswtch_map - - _LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files)) - - # Single nm call with --print-file-name for all object files - result = run_tool( - [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files], - timeout=30, - ) - if result is None or result.returncode != 0: - return cswtch_map - - for line in result.stdout.splitlines(): + for line in output.splitlines(): if "CSWTCH$" not in line: continue # Split on last ":" that precedes a hex address. - # nm --print-file-name format: filepath:hex_addr hex_size type name - # We split from the right: find the last colon followed by hex digits. + # For .o: "filepath.o" : "hex_addr hex_size type name" + # For .a: "filepath.a:member.o" : "hex_addr hex_size type name" parts_after_colon = line.rsplit(":", 1) if len(parts_after_colon) != 2: continue @@ -457,16 +448,89 @@ class MemoryAnalyzer: except ValueError: continue - # Get relative path from obj_dir for readability - try: - rel_path = str(Path(file_path).relative_to(obj_dir)) - except ValueError: + # Determine readable source path + # Use ".a:" to detect archive format (not bare ":" which matches + # Windows drive letters like "C:\...\file.o"). + if ".a:" in file_path: + # Archive format: "archive.a:member.o" → "archive_stem/member.o" + archive_part, member = file_path.rsplit(":", 1) + archive_name = Path(archive_part).stem + rel_path = f"{archive_name}/{member}" + elif base_dir is not None: + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + rel_path = file_path + else: rel_path = file_path key = f"{sym_name}:{size}" cswtch_map[key].append((rel_path, size)) - return cswtch_map + def _run_nm_cswtch_scan( + self, + files: list[Path], + base_dir: Path | None, + cswtch_map: dict[str, list[tuple[str, int]]], + ) -> None: + """Run nm on *files* and add any CSWTCH symbols to *cswtch_map*. + + Args: + files: Object (``.o``) or archive (``.a``) files to scan. + base_dir: Base directory for relative path computation (see + :meth:`_parse_nm_cswtch_output`). + cswtch_map: Dict to populate with results. + """ + if not self.nm_path or not files: + return + + _LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files)) + + result = run_tool( + [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files], + timeout=30, + ) + if result is None or result.returncode != 0: + _LOGGER.debug( + "nm failed or timed out scanning %d files for CSWTCH symbols", + len(files), + ) + return + + self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map) + + def _scan_cswtch_in_sdk_archives( + self, cswtch_map: dict[str, list[tuple[str, int]]] + ) -> None: + """Scan SDK library archives (.a) for CSWTCH symbols. + + Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source, + so their CSWTCH symbols only exist inside ``.a`` archives. Results are + merged into *cswtch_map* for keys not already found in ``.o`` files. + + The same source file (e.g. ``lwip-esp.o``) often appears in multiple + library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.), + so results are deduplicated by member name. + """ + sdk_dirs = self._find_sdk_library_dirs() + if not sdk_dirs: + return + + sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a")) + + sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + self._run_nm_cswtch_scan(sdk_archives, None, sdk_map) + + # Merge SDK results, deduplicating by member name. + for key, sources in sdk_map.items(): + if key in cswtch_map: + continue + seen: dict[str, tuple[str, int]] = {} + for path, sz in sources: + member = Path(path).name + if member not in seen: + seen[member] = (path, sz) + cswtch_map[key] = list(seen.values()) def _source_file_to_component(self, source_file: str) -> str: """Map a source object file path to its component name. @@ -505,17 +569,25 @@ class MemoryAnalyzer: CSWTCH symbols are compiler-generated lookup tables for switch statements. They are local symbols, so the same name can appear in different object files. - This method scans .o files to attribute them to their source components. + This method scans .o files and SDK archives to attribute them to their + source components. """ obj_dir = self._find_object_files_dir() if obj_dir is None: _LOGGER.debug("No object files directory found, skipping CSWTCH analysis") return - # Scan object files for CSWTCH symbols - cswtch_map = self._scan_cswtch_in_objects(obj_dir) + # Scan build-dir object files for CSWTCH symbols + cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map) + + # Also scan SDK library archives (.a) for CSWTCH symbols. + # Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source + # so their symbols only exist inside .a archives, not as loose .o files. + self._scan_cswtch_in_sdk_archives(cswtch_map) + if not cswtch_map: - _LOGGER.debug("No CSWTCH symbols found in object files") + _LOGGER.debug("No CSWTCH symbols found in object files or SDK archives") return # Collect CSWTCH symbols from the ELF (already parsed in sections) From 46f8302d8ff4aa8acf9751d0df948f86e4b61aa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:26:15 -0600 Subject: [PATCH 135/251] [mqtt] Use stack buffer for discovery topic to avoid heap allocation (#13812) --- esphome/components/mqtt/mqtt_component.cpp | 21 +++++++++++---------- esphome/components/mqtt/mqtt_component.h | 9 +++++++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index e4b80de50a..8cf393e2df 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -34,10 +34,7 @@ inline char *append_char(char *p, char c) { // MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h. // ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h. // This ensures the stack buffers below are always large enough. -static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) -// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null -static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + - ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; +// MQTT_DISCOVERY_PREFIX_MAX_LEN and MQTT_DISCOVERY_TOPIC_MAX_LEN are defined in mqtt_component.h // Function implementation of LOG_MQTT_COMPONENT macro to reduce code size void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) { @@ -54,15 +51,15 @@ void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } -std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { +StringRef MQTTComponent::get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf, + const MQTTDiscoveryInfo &discovery_info) const { char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1]; str_sanitize_to(sanitized_name, App.get_name().c_str()); const char *comp_type = this->component_type(); char object_id_buf[OBJECT_ID_MAX_LEN]; StringRef object_id = this->get_default_object_id_to_(object_id_buf); - char buf[DISCOVERY_TOPIC_MAX_LEN]; - char *p = buf; + char *p = buf.data(); p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size()); p = append_char(p, '/'); @@ -72,8 +69,9 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove p = append_char(p, '/'); p = append_str(p, object_id.c_str(), object_id.size()); p = append_str(p, "/config", 7); + *p = '\0'; - return std::string(buf, p - buf); + return StringRef(buf.data(), p - buf.data()); } StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix, @@ -182,16 +180,19 @@ bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); + char discovery_topic_buf[MQTT_DISCOVERY_TOPIC_MAX_LEN]; + StringRef discovery_topic = this->get_discovery_topic_to_(discovery_topic_buf, discovery_info); + if (discovery_info.clean) { ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str()); - return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); + return global_mqtt_client->publish(discovery_topic.c_str(), "", 0, this->qos_, true); } ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( - this->get_discovery_topic_(discovery_info), + discovery_topic.c_str(), [this](JsonObject root) { SendDiscoveryConfig config; config.state_topic = true; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2cec6fda7e..76375fb106 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -32,6 +32,10 @@ static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: // Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN = MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1; +static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) +// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null +static constexpr size_t MQTT_DISCOVERY_TOPIC_MAX_LEN = MQTT_DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + + 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; class MQTTComponent; // Forward declaration void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic); @@ -263,8 +267,9 @@ class MQTTComponent : public Component { void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0); protected: - /// Helper method to get the discovery topic for this component. - std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const; + /// Helper method to get the discovery topic for this component into a buffer. + StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf, + const MQTTDiscoveryInfo &discovery_info) const; /** Get this components state/command/... topic into a buffer. * From c3c0c40524105ad3165581b2c8241dc1990201cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:26:29 -0600 Subject: [PATCH 136/251] [mqtt] Return friendly_name_() by const reference to avoid string copies (#13810) --- esphome/components/mqtt/mqtt_component.cpp | 6 +++--- esphome/components/mqtt/mqtt_component.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 8cf393e2df..09570106df 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -205,7 +205,7 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : ""; + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : StringRef(); if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; @@ -249,7 +249,7 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32, - fnv1_hash(this->friendly_name_())); + fnv1_hash(this->friendly_name_().c_str())); // Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678") // MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43 char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11]; @@ -415,7 +415,7 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden -std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const { return this->get_entity()->get_object_id_to(buf); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 76375fb106..eb98158647 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -293,7 +293,7 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - std::string friendly_name_() const; + const StringRef &friendly_name_() const; /// Get the icon field of this component as StringRef StringRef get_icon_ref_() const; From 422f413680690b257429a4c4f8b51a700bee40e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:26:44 -0600 Subject: [PATCH 137/251] [lps22] Replace set_retry with set_interval to avoid heap allocation (#13841) --- esphome/components/lps22/lps22.cpp | 14 ++++++++++---- esphome/components/lps22/lps22.h | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp index 526286ba72..7fc5774b08 100644 --- a/esphome/components/lps22/lps22.cpp +++ b/esphome/components/lps22/lps22.cpp @@ -38,22 +38,29 @@ void LPS22Component::dump_config() { LOG_UPDATE_INTERVAL(this); } +static constexpr uint32_t INTERVAL_READ = 0; + void LPS22Component::update() { uint8_t value = 0x00; this->read_register(CTRL_REG2, &value, 1); value |= CTRL_REG2_ONE_SHOT_MASK; this->write_register(CTRL_REG2, &value, 1); - this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); }); + this->read_attempts_remaining_ = READ_ATTEMPTS; + this->set_interval(INTERVAL_READ, READ_INTERVAL, [this]() { this->try_read_(); }); } -RetryResult LPS22Component::try_read_() { +void LPS22Component::try_read_() { uint8_t value = 0x00; this->read_register(STATUS, &value, 1); const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK; if ((value & expected_status_mask) != expected_status_mask) { ESP_LOGD(TAG, "STATUS not ready: %x", value); - return RetryResult::RETRY; + if (--this->read_attempts_remaining_ == 0) { + this->cancel_interval(INTERVAL_READ); + } + return; } + this->cancel_interval(INTERVAL_READ); if (this->temperature_sensor_ != nullptr) { uint8_t t_buf[2]{0}; @@ -68,7 +75,6 @@ RetryResult LPS22Component::try_read_() { uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]); this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb)); } - return RetryResult::DONE; } } // namespace lps22 diff --git a/esphome/components/lps22/lps22.h b/esphome/components/lps22/lps22.h index 549ea524ea..95ee4ad442 100644 --- a/esphome/components/lps22/lps22.h +++ b/esphome/components/lps22/lps22.h @@ -17,10 +17,11 @@ class LPS22Component : public sensor::Sensor, public PollingComponent, public i2 void dump_config() override; protected: + void try_read_(); + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; - - RetryResult try_read_(); + uint8_t read_attempts_remaining_{0}; }; } // namespace lps22 From bed01da345c6c652ef464d800aa2c8152c9681a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 03:45:40 -0600 Subject: [PATCH 138/251] [api] Guard varint parsing against overlong encodings (#13870) --- esphome/components/api/proto.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 92978f765f..41ea0043f9 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -112,8 +112,12 @@ class ProtoVarInt { uint64_t result = buffer[0] & 0x7F; uint8_t bitpos = 7; + // A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings + // to avoid undefined behavior from shifting uint64_t by >= 64 bits. + uint32_t max_len = std::min(len, uint32_t(10)); + // Start from the second byte since we've already processed the first - for (uint32_t i = 1; i < len; i++) { + for (uint32_t i = 1; i < max_len; i++) { uint8_t val = buffer[i]; result |= uint64_t(val & 0x7F) << uint64_t(bitpos); bitpos += 7; From fb9328372028c941d7a1c1bc446efbf157c835d0 Mon Sep 17 00:00:00 2001 From: tronikos <tronikos@users.noreply.github.com> Date: Mon, 9 Feb 2026 01:52:49 -0800 Subject: [PATCH 139/251] [water_heater] Add state masking to distinguish explicit commands from no-change (#13879) --- .../components/water_heater/water_heater.cpp | 18 ++++++++++++------ esphome/components/water_heater/water_heater.h | 4 ++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index e6d1562352..9d7ae0cbc0 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -65,6 +65,7 @@ WaterHeaterCall &WaterHeaterCall::set_away(bool away) { } else { this->state_ &= ~WATER_HEATER_STATE_AWAY; } + this->state_mask_ |= WATER_HEATER_STATE_AWAY; return *this; } @@ -74,6 +75,7 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) { } else { this->state_ &= ~WATER_HEATER_STATE_ON; } + this->state_mask_ |= WATER_HEATER_STATE_ON; return *this; } @@ -92,11 +94,11 @@ void WaterHeaterCall::perform() { if (!std::isnan(this->target_temperature_high_)) { ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); } - if (this->state_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: YES"); + if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { + ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); } - if (this->state_ & WATER_HEATER_STATE_ON) { - ESP_LOGD(TAG, " On: YES"); + if (this->state_mask_ & WATER_HEATER_STATE_ON) { + ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } this->parent_->control(*this); } @@ -137,13 +139,17 @@ void WaterHeaterCall::validate_() { this->target_temperature_high_ = NAN; } } - if ((this->state_ & WATER_HEATER_STATE_AWAY) && !traits.get_supports_away_mode()) { - ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str()); + if (!traits.get_supports_away_mode()) { + if (this->state_ & WATER_HEATER_STATE_AWAY) { + ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str()); + } this->state_ &= ~WATER_HEATER_STATE_AWAY; + this->state_mask_ &= ~WATER_HEATER_STATE_AWAY; } // If ON/OFF not supported, device is always on - clear the flag silently if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { this->state_ &= ~WATER_HEATER_STATE_ON; + this->state_mask_ &= ~WATER_HEATER_STATE_ON; } } diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 7bd05ba7f5..93fcf5f401 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -91,6 +91,8 @@ class WaterHeaterCall { float get_target_temperature_high() const { return this->target_temperature_high_; } /// Get state flags value uint32_t get_state() const { return this->state_; } + /// Get mask of state flags that are being changed + uint32_t get_state_mask() const { return this->state_mask_; } protected: void validate_(); @@ -100,6 +102,7 @@ class WaterHeaterCall { float target_temperature_low_{NAN}; float target_temperature_high_{NAN}; uint32_t state_{0}; + uint32_t state_mask_{0}; }; struct WaterHeaterCallInternal : public WaterHeaterCall { @@ -111,6 +114,7 @@ struct WaterHeaterCallInternal : public WaterHeaterCall { this->target_temperature_low_ = restore.target_temperature_low_; this->target_temperature_high_ = restore.target_temperature_high_; this->state_ = restore.state_; + this->state_mask_ = restore.state_mask_; return *this; } }; From 790ac620ab10a228ac54ae0caedf7669650ff9e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 06:42:12 -0600 Subject: [PATCH 140/251] [web_server_idf] Use C++17 nested namespace style (#13856) --- esphome/components/web_server_idf/multipart.cpp | 6 ++---- esphome/components/web_server_idf/multipart.h | 6 ++---- esphome/components/web_server_idf/utils.cpp | 6 ++---- esphome/components/web_server_idf/utils.h | 6 ++---- esphome/components/web_server_idf/web_server_idf.cpp | 6 ++---- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 2092a41a8e..52dafeb997 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -6,8 +6,7 @@ #include <cstring> #include "multipart_parser.h" -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { static const char *const TAG = "multipart"; @@ -249,6 +248,5 @@ std::string str_trim(const std::string &str) { return str.substr(start, end - start + 1); } -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index 8fbe90c4a0..9008be6459 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -10,8 +10,7 @@ #include <string> #include <utility> -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { // Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads class MultipartReader { @@ -81,6 +80,5 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st // Trim whitespace from both ends of a string std::string str_trim(const std::string &str); -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index d3c3c3dc55..a1a2793b4a 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -8,8 +8,7 @@ #include "utils.h" -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { static const char *const TAG = "web_server_idf_utils"; @@ -119,6 +118,5 @@ const char *stristr(const char *haystack, const char *needle) { return nullptr; } -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index ae58f82398..bb0610dac0 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -5,8 +5,7 @@ #include <string> #include "esphome/core/helpers.h" -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { /// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space) /// Returns the new length of the decoded string @@ -29,6 +28,5 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n); // Case-insensitive string search (like strstr but case-insensitive) const char *stristr(const char *haystack, const char *needle); -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 39e6b7a790..ac7f99bef7 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -30,8 +30,7 @@ #include <cerrno> #include <sys/socket.h> -namespace esphome { -namespace web_server_idf { +namespace esphome::web_server_idf { #ifndef HTTPD_409 #define HTTPD_409 "409 Conflict" @@ -895,7 +894,6 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } #endif // USE_WEBSERVER_OTA -} // namespace web_server_idf -} // namespace esphome +} // namespace esphome::web_server_idf #endif // !defined(USE_ESP32) From 22c77866d89da64ab96c830c716225d700b33906 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 06:42:26 -0600 Subject: [PATCH 141/251] [e131] Remove unnecessary heap allocation from packet receive loop (#13852) --- esphome/components/e131/e131.cpp | 7 ++----- esphome/components/e131/e131.h | 2 +- esphome/components/e131/e131_packet.cpp | 6 +++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index f11e7f4fe3..4187857901 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -55,7 +55,6 @@ void E131Component::setup() { } void E131Component::loop() { - std::vector<uint8_t> payload; E131Packet packet; int universe = 0; uint8_t buf[1460]; @@ -64,11 +63,9 @@ void E131Component::loop() { if (len == -1) { return; } - payload.resize(len); - memmove(&payload[0], buf, len); - if (!this->packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); return; } diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 831138a545..d4b272eae2 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -38,7 +38,7 @@ class E131Component : public esphome::Component { void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } protected: - bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet); + bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); void join_(int universe); diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index e663a3d0fc..ed081e5758 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -116,11 +116,11 @@ void E131Component::leave_(int universe) { ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); } -bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) { - if (data.size() < E131_MIN_PACKET_SIZE) +bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet) { + if (len < E131_MIN_PACKET_SIZE) return false; - auto *sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]); + auto *sbuff = reinterpret_cast<const E131RawPacket *>(data); if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) return false; From 4ef238eb7b563a4a0aec21b4dd492a64543824aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:26:03 -0600 Subject: [PATCH 142/251] [analyze-memory] Attribute third-party library symbols via nm scanning (#13878) --- esphome/analyze_memory/__init__.py | 366 ++++++++++++++++++++++++++++- esphome/analyze_memory/cli.py | 14 +- 2 files changed, 374 insertions(+), 6 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index d8abc8bafb..bf1bcbfa05 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -43,6 +43,7 @@ _READELF_SECTION_PATTERN = re.compile( # Component category prefixes _COMPONENT_PREFIX_ESPHOME = "[esphome]" _COMPONENT_PREFIX_EXTERNAL = "[external]" +_COMPONENT_PREFIX_LIB = "[lib]" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" @@ -56,6 +57,16 @@ SymbolInfoType = tuple[str, int, str] # RAM sections - symbols in these sections consume RAM RAM_SECTIONS = frozenset([".data", ".bss"]) +# nm symbol types for global/weak defined symbols (used for library symbol mapping) +# Only global (uppercase) and weak symbols are safe to use - local symbols (lowercase) +# can have name collisions across compilation units +_NM_DEFINED_GLOBAL_TYPES = frozenset({"T", "D", "B", "R", "W", "V"}) + +# Pattern matching compiler-generated local names that can collide across compilation +# units (e.g., packet$19, buf$20, flag$5261). These are unsafe for name-based lookup. +# Does NOT match mangled C++ names with optimization suffixes (e.g., func$isra$0). +_COMPILER_LOCAL_PATTERN = re.compile(r"^[a-zA-Z_]\w*\$\d+$") + @dataclass class MemorySection: @@ -179,11 +190,19 @@ class MemoryAnalyzer: self._sdk_symbols: list[SDKSymbol] = [] # CSWTCH symbols: list of (name, size, source_file, component) self._cswtch_symbols: list[tuple[str, int, str, str]] = [] + # Library symbol mapping: symbol_name -> library_name + self._lib_symbol_map: dict[str, str] = {} + # Library dir to name mapping: "lib641" -> "espsoftwareserial", + # "espressif__mdns" -> "mdns" + self._lib_hash_to_name: dict[str, str] = {} + # Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns" + self._heuristic_to_lib: dict[str, str] = {} def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" self._parse_sections() self._parse_symbols() + self._scan_libraries() self._categorize_symbols() self._analyze_cswtch_symbols() self._analyze_sdk_libraries() @@ -328,15 +347,19 @@ class MemoryAnalyzer: # If no component match found, it's core return _COMPONENT_CORE + # Check library symbol map (more accurate than heuristic patterns) + if lib_name := self._lib_symbol_map.get(symbol_name): + return f"{_COMPONENT_PREFIX_LIB}{lib_name}" + # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): if any(pattern in symbol_name for pattern in patterns): - return component + return self._heuristic_to_lib.get(component, component) # Check against demangled patterns for component, patterns in DEMANGLED_PATTERNS.items(): if any(pattern in demangled for pattern in patterns): - return component + return self._heuristic_to_lib.get(component, component) # Special cases that need more complex logic @@ -384,6 +407,327 @@ class MemoryAnalyzer: return "Other Core" + def _discover_pio_libraries( + self, + libraries: dict[str, list[Path]], + hash_to_name: dict[str, str], + ) -> None: + """Discover PlatformIO third-party libraries from the build directory. + + Scans ``lib<hex>/`` directories under ``.pioenvs/<env>/`` to find + library names and their ``.a`` archive or ``.o`` file paths. + + Args: + libraries: Dict to populate with library name -> file path list mappings. + Prefers ``.a`` archives when available, falls back to ``.o`` files + (e.g., pioarduino ESP32 Arduino builds only produce ``.o`` files). + hash_to_name: Dict to populate with dir name -> library name mappings + for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``). + """ + build_dir = self.elf_path.parent + + for entry in build_dir.iterdir(): + if not entry.is_dir() or not entry.name.startswith("lib"): + continue + # Validate that the suffix after "lib" is a hex hash + hex_part = entry.name[3:] + if not hex_part: + continue + try: + int(hex_part, 16) + except ValueError: + continue + + # Each lib<hex>/ directory contains a subdirectory named after the library + for lib_subdir in entry.iterdir(): + if not lib_subdir.is_dir(): + continue + lib_name = lib_subdir.name.lower() + + # Prefer .a archive (lib<LibraryName>.a), fall back to .o files + # e.g., lib72a/ESPAsyncTCP/... has lib72a/libESPAsyncTCP.a + archive = entry / f"lib{lib_subdir.name}.a" + if archive.exists(): + file_paths = [archive] + elif archives := list(entry.glob("*.a")): + # Case-insensitive fallback + file_paths = [archives[0]] + else: + # No .a archive (e.g., pioarduino CMake builds) - use .o files + file_paths = sorted(lib_subdir.rglob("*.o")) + + if file_paths: + libraries[lib_name] = file_paths + hash_to_name[entry.name] = lib_name + _LOGGER.debug( + "Discovered PlatformIO library: %s -> %s", + lib_subdir.name, + file_paths[0], + ) + + def _discover_idf_managed_components( + self, + libraries: dict[str, list[Path]], + hash_to_name: dict[str, str], + ) -> None: + """Discover ESP-IDF managed component libraries from the build directory. + + ESP-IDF managed components (from the IDF component registry) use a + ``<vendor>__<name>`` naming convention. Source files live under + ``managed_components/<vendor>__<name>/`` and the compiled archives are at + ``esp-idf/<vendor>__<name>/lib<vendor>__<name>.a``. + + Args: + libraries: Dict to populate with library name -> file path list mappings. + hash_to_name: Dict to populate with dir name -> library name mappings + for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``). + """ + build_dir = self.elf_path.parent + + managed_dir = build_dir / "managed_components" + if not managed_dir.is_dir(): + return + + espidf_dir = build_dir / "esp-idf" + + for entry in managed_dir.iterdir(): + if not entry.is_dir() or "__" not in entry.name: + continue + + # Extract the short name: espressif__mdns -> mdns + full_name = entry.name # e.g., espressif__mdns + short_name = full_name.split("__", 1)[1].lower() + + # Find the .a archive under esp-idf/<vendor>__<name>/ + archive = espidf_dir / full_name / f"lib{full_name}.a" + if archive.exists(): + libraries[short_name] = [archive] + hash_to_name[full_name] = short_name + _LOGGER.debug( + "Discovered IDF managed component: %s -> %s", + short_name, + archive, + ) + + def _build_library_symbol_map( + self, libraries: dict[str, list[Path]] + ) -> dict[str, str]: + """Build a symbol-to-library mapping from library archives or object files. + + Runs ``nm --defined-only`` on each ``.a`` or ``.o`` file to collect + global and weak defined symbols. + + Args: + libraries: Dictionary mapping library name to list of file paths + (``.a`` archives or ``.o`` object files). + + Returns: + Dictionary mapping symbol name to library name. + """ + symbol_map: dict[str, str] = {} + + if not self.nm_path: + return symbol_map + + for lib_name, file_paths in libraries.items(): + result = run_tool( + [self.nm_path, "--defined-only", *(str(p) for p in file_paths)], + timeout=10, + ) + if result is None or result.returncode != 0: + continue + + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) < 3: + continue + + sym_type = parts[-2] + sym_name = parts[-1] + + # Include global defined symbols (uppercase) and weak symbols (W/V) + if sym_type in _NM_DEFINED_GLOBAL_TYPES: + symbol_map[sym_name] = lib_name + + return symbol_map + + @staticmethod + def _build_heuristic_to_lib_mapping( + library_names: set[str], + ) -> dict[str, str]: + """Build mapping from heuristic pattern categories to discovered libraries. + + Heuristic categories like ``mdns_lib``, ``web_server_lib``, ``async_tcp`` + exist as approximations for library attribution. When we discover the + actual library, symbols matching those heuristics should be redirected + to the ``[lib]`` category instead. + + The mapping is built by checking if the normalized category name + (stripped of ``_lib`` suffix and underscores) appears as a substring + of any discovered library name. + + Examples:: + + mdns_lib -> mdns -> in "mdns" or "esp8266mdns" -> [lib]mdns + web_server_lib -> webserver -> in "espasyncwebserver" -> [lib]espasyncwebserver + async_tcp -> asynctcp -> in "espasynctcp" -> [lib]espasynctcp + + Args: + library_names: Set of discovered library names (lowercase). + + Returns: + Dictionary mapping heuristic category to ``[lib]<name>`` string. + """ + mapping: dict[str, str] = {} + all_categories = set(SYMBOL_PATTERNS) | set(DEMANGLED_PATTERNS) + + for category in all_categories: + base = category.removesuffix("_lib").replace("_", "") + # Collect all libraries whose name contains the base string + candidates = [lib_name for lib_name in library_names if base in lib_name] + if not candidates: + continue + + # Choose a deterministic "best" match: + # 1. Prefer exact name matches over substring matches. + # 2. Among non-exact matches, prefer the shortest library name. + # 3. Break remaining ties lexicographically. + best_lib = min( + candidates, + key=lambda lib_name, _base=base: ( + lib_name != _base, + len(lib_name), + lib_name, + ), + ) + mapping[category] = f"{_COMPONENT_PREFIX_LIB}{best_lib}" + + if mapping: + _LOGGER.debug( + "Heuristic-to-library redirects: %s", + ", ".join(f"{k} -> {v}" for k, v in sorted(mapping.items())), + ) + + return mapping + + def _parse_map_file(self) -> dict[str, str] | None: + """Parse linker map file to build authoritative symbol-to-library mapping. + + The linker map file contains the definitive source attribution for every + symbol, including local/static ones that ``nm`` cannot safely export. + + Map file format (GNU ld):: + + .text._mdns_service_task + 0x400e9fdc 0x65c .pioenvs/env/esp-idf/espressif__mdns/libespressif__mdns.a(mdns.c.o) + + Each section entry has a ``.section.symbol_name`` line followed by an + indented line with address, size, and source path. + + Returns: + Symbol-to-library dict, or ``None`` if no usable map file exists. + """ + map_path = self.elf_path.with_suffix(".map") + if not map_path.exists() or map_path.stat().st_size < 10000: + return None + + _LOGGER.info("Parsing linker map file: %s", map_path.name) + + try: + map_text = map_path.read_text(encoding="utf-8", errors="replace") + except OSError as err: + _LOGGER.warning("Failed to read map file: %s", err) + return None + + symbol_map: dict[str, str] = {} + current_symbol: str | None = None + section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.") + + for line in map_text.splitlines(): + # Match section.symbol line: " .text.symbol_name" + # Single space indent, starts with dot + if len(line) > 2 and line[0] == " " and line[1] == ".": + stripped = line.strip() + for prefix in section_prefixes: + if stripped.startswith(prefix): + current_symbol = stripped[len(prefix) :] + break + else: + current_symbol = None + continue + + # Match source attribution line: " 0xADDR 0xSIZE source_path" + if current_symbol is None: + continue + + fields = line.split() + # Skip compiler-generated local names (e.g., packet$19, buf$20) + # that can collide across compilation units + if ( + len(fields) >= 3 + and fields[0].startswith("0x") + and fields[1].startswith("0x") + and not _COMPILER_LOCAL_PATTERN.match(current_symbol) + ): + source_path = fields[2] + # Check if source path contains a known library directory + for dir_key, lib_name in self._lib_hash_to_name.items(): + if dir_key in source_path: + symbol_map[current_symbol] = lib_name + break + + current_symbol = None + + return symbol_map or None + + def _scan_libraries(self) -> None: + """Discover third-party libraries and build symbol mapping. + + Scans both PlatformIO ``lib<hex>/`` directories (Arduino builds) and + ESP-IDF ``managed_components/`` (IDF builds) to find library archives. + + Uses the linker map file for authoritative symbol attribution when + available, falling back to ``nm`` scanning with heuristic redirects. + """ + libraries: dict[str, list[Path]] = {} + self._discover_pio_libraries(libraries, self._lib_hash_to_name) + self._discover_idf_managed_components(libraries, self._lib_hash_to_name) + + if not libraries: + _LOGGER.debug("No third-party libraries found") + return + + _LOGGER.info( + "Scanning %d libraries: %s", + len(libraries), + ", ".join(sorted(libraries)), + ) + + # Heuristic redirect catches local symbols (e.g., mdns_task_buffer$14) + # that can't be safely added to the symbol map due to name collisions + self._heuristic_to_lib = self._build_heuristic_to_lib_mapping( + set(libraries.keys()) + ) + + # Try linker map file first (authoritative, includes local symbols) + map_symbols = self._parse_map_file() + if map_symbols is not None: + self._lib_symbol_map = map_symbols + _LOGGER.info( + "Built library symbol map from linker map: %d symbols", + len(self._lib_symbol_map), + ) + return + + # Fall back to nm scanning (global symbols only) + self._lib_symbol_map = self._build_library_symbol_map(libraries) + + _LOGGER.info( + "Built library symbol map from nm: %d symbols from %d libraries", + len(self._lib_symbol_map), + len(libraries), + ) + def _find_object_files_dir(self) -> Path | None: """Find the directory containing object files for this build. @@ -559,9 +903,21 @@ class MemoryAnalyzer: if "esphome" in parts and "components" not in parts: return _COMPONENT_CORE - # Framework/library files - return the first path component - # e.g., lib65b/ESPAsyncTCP/... -> lib65b - # FrameworkArduino/... -> FrameworkArduino + # Framework/library files - check for PlatformIO library hash dirs + # e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp + if parts and parts[0] in self._lib_hash_to_name: + return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[0]]}" + + # ESP-IDF managed components: managed_components/espressif__mdns/... -> [lib]mdns + if ( + len(parts) >= 2 + and parts[0] == "managed_components" + and parts[1] in self._lib_hash_to_name + ): + return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}" + + # Other framework/library files - return the first path component + # e.g., FrameworkArduino/... -> FrameworkArduino return parts[0] if parts else source_file def _analyze_cswtch_symbols(self) -> None: diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index bb0eb7723e..dbc19c6b89 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -14,6 +14,7 @@ from . import ( _COMPONENT_CORE, _COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL, + _COMPONENT_PREFIX_LIB, RAM_SECTIONS, MemoryAnalyzer, ) @@ -407,6 +408,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for name, mem in components if name.startswith(_COMPONENT_PREFIX_EXTERNAL) ] + library_components = [ + (name, mem) + for name, mem in components + if name.startswith(_COMPONENT_PREFIX_LIB) + ] top_esphome_components = sorted( esphome_components, key=lambda x: x[1].flash_total, reverse=True @@ -417,6 +423,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): external_components, key=lambda x: x[1].flash_total, reverse=True ) + # Include all library components + top_library_components = sorted( + library_components, key=lambda x: x[1].flash_total, reverse=True + ) + # Check if API component exists and ensure it's included api_component = None for name, mem in components: @@ -435,10 +446,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): if name in system_components_to_include ] - # Combine all components to analyze: top ESPHome + all external + API if not already included + system components + # Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components components_to_analyze = ( list(top_esphome_components) + list(top_external_components) + + list(top_library_components) + system_components ) if api_component and api_component not in components_to_analyze: From 1c60efa4b6591f4b1fad1f76811faec142ab3d3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:30:49 -0600 Subject: [PATCH 143/251] [ota] Use secrets module for OTA authentication cnonce (#13863) --- esphome/espota2.py | 6 ++-- tests/unit_tests/test_espota2.py | 47 +++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/esphome/espota2.py b/esphome/espota2.py index 2d90251b38..c342eb4463 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -6,7 +6,7 @@ import hashlib import io import logging from pathlib import Path -import random +import secrets import socket import sys import time @@ -300,8 +300,8 @@ def perform_ota( nonce = nonce_bytes.decode() _LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce) - # Generate cnonce - cnonce = hash_func(str(random.random()).encode()).hexdigest() + # Generate cnonce matching the hash algorithm's digest size + cnonce = secrets.token_hex(nonce_size // 2) _LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce) send_check(sock, cnonce, "auth cnonce") diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index 1885b769f1..20ba4b1f76 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -18,8 +18,8 @@ from esphome import espota2 from esphome.core import EsphomeError # Test constants -MOCK_RANDOM_VALUE = 0.123456 -MOCK_RANDOM_BYTES = b"0.123456" +MOCK_MD5_CNONCE = "a" * 32 # Mock 32-char hex string from secrets.token_hex(16) +MOCK_SHA256_CNONCE = "b" * 64 # Mock 64-char hex string from secrets.token_hex(32) MOCK_MD5_NONCE = b"12345678901234567890123456789012" # 32 char nonce for MD5 MOCK_SHA256_NONCE = b"1234567890123456789012345678901234567890123456789012345678901234" # 64 char nonce for SHA256 @@ -55,10 +55,18 @@ def mock_time() -> Generator[None]: @pytest.fixture -def mock_random() -> Generator[Mock]: - """Mock random for predictable test values.""" - with patch("random.random", return_value=MOCK_RANDOM_VALUE) as mock_rand: - yield mock_rand +def mock_token_hex() -> Generator[Mock]: + """Mock secrets.token_hex for predictable test values.""" + + def _token_hex(nbytes: int) -> str: + if nbytes == 16: + return MOCK_MD5_CNONCE + if nbytes == 32: + return MOCK_SHA256_CNONCE + raise ValueError(f"Unexpected nbytes for token_hex mock: {nbytes}") + + with patch("esphome.espota2.secrets.token_hex", side_effect=_token_hex) as mock: + yield mock @pytest.fixture @@ -236,7 +244,7 @@ def test_send_check_socket_error(mock_socket: Mock) -> None: @pytest.mark.usefixtures("mock_time") def test_perform_ota_successful_md5_auth( - mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock + mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock ) -> None: """Test successful OTA with MD5 authentication.""" # Setup socket responses for recv calls @@ -272,8 +280,11 @@ def test_perform_ota_successful_md5_auth( ) ) - # Verify cnonce was sent (MD5 of random.random()) - cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest() + # Verify token_hex was called with MD5 digest size + mock_token_hex.assert_called_once_with(16) + + # Verify cnonce was sent + cnonce = MOCK_MD5_CNONCE assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode()) # Verify auth result was computed correctly @@ -366,7 +377,7 @@ def test_perform_ota_auth_without_password(mock_socket: Mock) -> None: @pytest.mark.usefixtures("mock_time") def test_perform_ota_md5_auth_wrong_password( - mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock + mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock ) -> None: """Test OTA fails when MD5 authentication is rejected due to wrong password.""" # Setup socket responses for recv calls @@ -390,7 +401,7 @@ def test_perform_ota_md5_auth_wrong_password( @pytest.mark.usefixtures("mock_time") def test_perform_ota_sha256_auth_wrong_password( - mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock + mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock ) -> None: """Test OTA fails when SHA256 authentication is rejected due to wrong password.""" # Setup socket responses for recv calls @@ -603,7 +614,7 @@ def test_progress_bar(capsys: CaptureFixture[str]) -> None: # Tests for SHA256 authentication @pytest.mark.usefixtures("mock_time") def test_perform_ota_successful_sha256_auth( - mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock + mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock ) -> None: """Test successful OTA with SHA256 authentication.""" # Setup socket responses for recv calls @@ -639,8 +650,11 @@ def test_perform_ota_successful_sha256_auth( ) ) - # Verify cnonce was sent (SHA256 of random.random()) - cnonce = hashlib.sha256(MOCK_RANDOM_BYTES).hexdigest() + # Verify token_hex was called with SHA256 digest size + mock_token_hex.assert_called_once_with(32) + + # Verify cnonce was sent + cnonce = MOCK_SHA256_CNONCE assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode()) # Verify auth result was computed correctly with SHA256 @@ -654,7 +668,7 @@ def test_perform_ota_successful_sha256_auth( @pytest.mark.usefixtures("mock_time") def test_perform_ota_sha256_fallback_to_md5( - mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock + mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock ) -> None: """Test SHA256-capable client falls back to MD5 for compatibility.""" # This test verifies the temporary backward compatibility @@ -692,7 +706,8 @@ def test_perform_ota_sha256_fallback_to_md5( ) # But authentication was done with MD5 - cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest() + mock_token_hex.assert_called_once_with(16) + cnonce = MOCK_MD5_CNONCE expected_hash = hashlib.md5() expected_hash.update(b"testpass") expected_hash.update(MOCK_MD5_NONCE) From 8b8acb3b2722c8c605dc84a7af9719f7b287ea9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:31:06 -0600 Subject: [PATCH 144/251] [dashboard] Use constant-time comparison for username check (#13865) --- esphome/dashboard/settings.py | 15 +++++--- tests/dashboard/test_settings.py | 66 +++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 6035b4a1d6..3b22180b1d 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -32,7 +32,7 @@ class DashboardSettings: def __init__(self) -> None: """Initialize the dashboard settings.""" self.config_dir: Path = None - self.password_hash: str = "" + self.password_hash: bytes = b"" self.username: str = "" self.using_password: bool = False self.on_ha_addon: bool = False @@ -84,11 +84,14 @@ class DashboardSettings: def check_password(self, username: str, password: str) -> bool: if not self.using_auth: return True - if username != self.username: - return False - - # Compare password in constant running time (to prevent timing attacks) - return hmac.compare_digest(self.password_hash, password_hash(password)) + # Compare in constant running time (to prevent timing attacks) + username_matches = hmac.compare_digest( + username.encode("utf-8"), self.username.encode("utf-8") + ) + password_matches = hmac.compare_digest( + self.password_hash, password_hash(password) + ) + return username_matches and password_matches def rel_path(self, *args: Any) -> Path: """Return a path relative to the ESPHome config folder.""" diff --git a/tests/dashboard/test_settings.py b/tests/dashboard/test_settings.py index 91a8ec70c3..55776ac7c4 100644 --- a/tests/dashboard/test_settings.py +++ b/tests/dashboard/test_settings.py @@ -1,4 +1,4 @@ -"""Tests for dashboard settings Path-related functionality.""" +"""Tests for DashboardSettings (path resolution and authentication).""" from __future__ import annotations @@ -10,6 +10,7 @@ import pytest from esphome.core import CORE from esphome.dashboard.settings import DashboardSettings +from esphome.dashboard.util.password import password_hash @pytest.fixture @@ -221,3 +222,66 @@ def test_config_path_parent_resolves_to_config_dir(tmp_path: Path) -> None: # Verify that CORE.config_path itself uses the sentinel file assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml" assert not CORE.config_path.exists() # Sentinel file doesn't actually exist + + +@pytest.fixture +def auth_settings(dashboard_settings: DashboardSettings) -> DashboardSettings: + """Create DashboardSettings with auth configured, based on dashboard_settings.""" + dashboard_settings.username = "admin" + dashboard_settings.using_password = True + dashboard_settings.password_hash = password_hash("correctpassword") + return dashboard_settings + + +def test_check_password_correct_credentials(auth_settings: DashboardSettings) -> None: + """Test check_password returns True for correct username and password.""" + assert auth_settings.check_password("admin", "correctpassword") is True + + +def test_check_password_wrong_password(auth_settings: DashboardSettings) -> None: + """Test check_password returns False for wrong password.""" + assert auth_settings.check_password("admin", "wrongpassword") is False + + +def test_check_password_wrong_username(auth_settings: DashboardSettings) -> None: + """Test check_password returns False for wrong username.""" + assert auth_settings.check_password("notadmin", "correctpassword") is False + + +def test_check_password_both_wrong(auth_settings: DashboardSettings) -> None: + """Test check_password returns False when both are wrong.""" + assert auth_settings.check_password("notadmin", "wrongpassword") is False + + +def test_check_password_no_auth(dashboard_settings: DashboardSettings) -> None: + """Test check_password returns True when auth is not configured.""" + assert dashboard_settings.check_password("anyone", "anything") is True + + +def test_check_password_non_ascii_username( + dashboard_settings: DashboardSettings, +) -> None: + """Test check_password handles non-ASCII usernames without TypeError.""" + dashboard_settings.username = "\u00e9l\u00e8ve" + dashboard_settings.using_password = True + dashboard_settings.password_hash = password_hash("pass") + assert dashboard_settings.check_password("\u00e9l\u00e8ve", "pass") is True + assert dashboard_settings.check_password("\u00e9l\u00e8ve", "wrong") is False + assert dashboard_settings.check_password("other", "pass") is False + + +def test_check_password_ha_addon_no_password( + dashboard_settings: DashboardSettings, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test check_password doesn't crash in HA add-on mode without a password. + + In HA add-on mode, using_ha_addon_auth can be True while using_password + is False, leaving password_hash as b"". This must not raise TypeError + in hmac.compare_digest. + """ + monkeypatch.delenv("DISABLE_HA_AUTHENTICATION", raising=False) + dashboard_settings.on_ha_addon = True + dashboard_settings.using_password = False + # password_hash stays as default b"" + assert dashboard_settings.check_password("anyone", "anything") is False From 248fc06dacbb4e5cfe861bcda003d2fdb2f7d60b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:44:20 -0600 Subject: [PATCH 145/251] [scheduler] Eliminate heap allocation in full_cleanup_removed_items_ (#13837) --- esphome/core/scheduler.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 047bf4ef17..6797640f54 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -390,20 +390,19 @@ void Scheduler::full_cleanup_removed_items_() { // 4. No operations inside can block or take other locks, so no deadlock risk LockGuard guard{this->lock_}; - std::vector<std::unique_ptr<SchedulerItem>> valid_items; - - // Move all non-removed items to valid_items, recycle removed ones - for (auto &item : this->items_) { - if (!is_item_removed_(item.get())) { - valid_items.push_back(std::move(item)); + // Compact in-place: move valid items forward, recycle removed ones + size_t write = 0; + for (size_t read = 0; read < this->items_.size(); ++read) { + if (!is_item_removed_(this->items_[read].get())) { + if (write != read) { + this->items_[write] = std::move(this->items_[read]); + } + ++write; } else { - // Recycle removed items - this->recycle_item_main_loop_(std::move(item)); + this->recycle_item_main_loop_(std::move(this->items_[read])); } } - - // Replace items_ with the filtered list - this->items_ = std::move(valid_items); + this->items_.erase(this->items_.begin() + write, this->items_.end()); // Rebuild the heap structure since items are no longer in heap order std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->to_remove_ = 0; From c812ac8b29aff567a219a263000494fb4b04b5f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:44:35 -0600 Subject: [PATCH 146/251] [ms8607] Replace set_retry with set_timeout chain to avoid heap allocation (#13842) --- esphome/components/ms8607/ms8607.cpp | 86 ++++++++++++++-------------- esphome/components/ms8607/ms8607.h | 4 ++ 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index 215131eb8e..88a3e6d7dc 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -72,53 +72,55 @@ void MS8607Component::setup() { // I do not know why the device sometimes NACKs the reset command, but // try 3 times in case it's a transitory issue on this boot - this->set_retry( - "reset", 5, 3, - [this](const uint8_t remaining_setup_attempts) { - ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, - this->humidity_device_->get_address()); - // I believe sending the reset command to both addresses is preferable to - // skipping humidity if PT fails for some reason. - // However, only consider the reset successful if they both ACK - bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0); - bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0); + // Backoff: executes at now, +5ms, +30ms + this->reset_attempts_remaining_ = 3; + this->reset_interval_ = 5; + this->try_reset_(); +} - if (!(pt_successful && h_successful)) { - ESP_LOGE(TAG, "Resetting I2C devices failed"); - if (!pt_successful && !h_successful) { - this->error_code_ = ErrorCode::PTH_RESET_FAILED; - } else if (!pt_successful) { - this->error_code_ = ErrorCode::PT_RESET_FAILED; - } else { - this->error_code_ = ErrorCode::H_RESET_FAILED; - } +void MS8607Component::try_reset_() { + ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, this->humidity_device_->get_address()); + // I believe sending the reset command to both addresses is preferable to + // skipping humidity if PT fails for some reason. + // However, only consider the reset successful if they both ACK + bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0); + bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0); - if (remaining_setup_attempts > 0) { - this->status_set_error(); - } else { - this->mark_failed(); - } - return RetryResult::RETRY; - } + if (!(pt_successful && h_successful)) { + ESP_LOGE(TAG, "Resetting I2C devices failed"); + if (!pt_successful && !h_successful) { + this->error_code_ = ErrorCode::PTH_RESET_FAILED; + } else if (!pt_successful) { + this->error_code_ = ErrorCode::PT_RESET_FAILED; + } else { + this->error_code_ = ErrorCode::H_RESET_FAILED; + } - this->setup_status_ = SetupStatus::NEEDS_PROM_READ; - this->error_code_ = ErrorCode::NONE; - this->status_clear_error(); + if (--this->reset_attempts_remaining_ > 0) { + uint32_t delay = this->reset_interval_; + this->reset_interval_ *= 5; + this->set_timeout("reset", delay, [this]() { this->try_reset_(); }); + this->status_set_error(); + } else { + this->mark_failed(); + } + return; + } - // 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library - this->set_timeout("prom-read", 15, [this]() { - if (this->read_calibration_values_from_prom_()) { - this->setup_status_ = SetupStatus::SUCCESSFUL; - this->status_clear_error(); - } else { - this->mark_failed(); - return; - } - }); + this->setup_status_ = SetupStatus::NEEDS_PROM_READ; + this->error_code_ = ErrorCode::NONE; + this->status_clear_error(); - return RetryResult::DONE; - }, - 5.0f); // executes at now, +5ms, +25ms + // 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library + this->set_timeout("prom-read", 15, [this]() { + if (this->read_calibration_values_from_prom_()) { + this->setup_status_ = SetupStatus::SUCCESSFUL; + this->status_clear_error(); + } else { + this->mark_failed(); + return; + } + }); } void MS8607Component::update() { diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index 67ce2817fa..ceb3dd22c8 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -44,6 +44,8 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; } protected: + /// Attempt to reset both I2C devices, retrying with backoff on failure + void try_reset_(); /** Read and store the Pressure & Temperature calibration settings from the PROM. Intended to be called during setup(), this will set the `failure_reason_` @@ -102,6 +104,8 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { enum class SetupStatus; /// Current step in the multi-step & possibly delayed setup() process SetupStatus setup_status_; + uint32_t reset_interval_{5}; + uint8_t reset_attempts_remaining_{0}; }; } // namespace ms8607 From 938a11595dc221cc86586bb778d332d7c60a4753 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:44:50 -0600 Subject: [PATCH 147/251] [speaker] Replace set_retry with set_interval to avoid heap allocation (#13843) --- .../media_player/speaker_media_player.cpp | 42 +++++++++---------- .../media_player/speaker_media_player.h | 5 +++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 94f555c26e..fdf6bf66cd 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -103,6 +103,20 @@ void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type, } } +void SpeakerMediaPlayer::stop_and_unpause_media_() { + this->media_pipeline_->stop(); + this->unpause_media_remaining_ = 3; + this->set_interval("unpause_med", 50, [this]() { + if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { + this->cancel_interval("unpause_med"); + this->media_pipeline_->set_pause_state(false); + this->is_paused_ = false; + } else if (--this->unpause_media_remaining_ == 0) { + this->cancel_interval("unpause_med"); + } + }); +} + void SpeakerMediaPlayer::watch_media_commands_() { if (!this->is_ready()) { return; @@ -144,15 +158,7 @@ void SpeakerMediaPlayer::watch_media_commands_() { if (this->is_paused_) { // If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a // short segment of the paused file before starting the new one. - this->media_pipeline_->stop(); - this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) { - if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { - this->media_pipeline_->set_pause_state(false); - this->is_paused_ = false; - return RetryResult::DONE; - } - return RetryResult::RETRY; - }); + this->stop_and_unpause_media_(); } else { // Not paused, just directly start the file if (media_command.file.has_value()) { @@ -197,27 +203,21 @@ void SpeakerMediaPlayer::watch_media_commands_() { this->cancel_timeout("next_ann"); this->announcement_playlist_.clear(); this->announcement_pipeline_->stop(); - this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) { + this->unpause_announcement_remaining_ = 3; + this->set_interval("unpause_ann", 50, [this]() { if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) { + this->cancel_interval("unpause_ann"); this->announcement_pipeline_->set_pause_state(false); - return RetryResult::DONE; + } else if (--this->unpause_announcement_remaining_ == 0) { + this->cancel_interval("unpause_ann"); } - return RetryResult::RETRY; }); } } else { if (this->media_pipeline_ != nullptr) { this->cancel_timeout("next_media"); this->media_playlist_.clear(); - this->media_pipeline_->stop(); - this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) { - if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { - this->media_pipeline_->set_pause_state(false); - this->is_paused_ = false; - return RetryResult::DONE; - } - return RetryResult::RETRY; - }); + this->stop_and_unpause_media_(); } } diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 722f98ceea..6796fc9c00 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -112,6 +112,9 @@ class SpeakerMediaPlayer : public Component, /// media pipelines are defined. inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); } + /// Stops the media pipeline and polls until stopped to unpause it, avoiding an audible glitch. + void stop_and_unpause_media_(); + // Processes commands from media_control_command_queue_. void watch_media_commands_(); @@ -141,6 +144,8 @@ class SpeakerMediaPlayer : public Component, bool is_paused_{false}; bool is_muted_{false}; + uint8_t unpause_media_remaining_{0}; + uint8_t unpause_announcement_remaining_{0}; // The amount to change the volume on volume up/down commands float volume_increment_; From 66af9980983f4f6fd09e0c7e672231ddd3edfe45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:45:03 -0600 Subject: [PATCH 148/251] [dashboard] Handle malformed Basic Auth headers gracefully (#13866) --- esphome/dashboard/web_server.py | 7 ++- tests/dashboard/test_web_server.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 00974bf460..92cab929ef 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -120,8 +120,11 @@ def is_authenticated(handler: BaseHandler) -> bool: if auth_header := handler.request.headers.get("Authorization"): assert isinstance(auth_header, str) if auth_header.startswith("Basic "): - auth_decoded = base64.b64decode(auth_header[6:]).decode() - username, password = auth_decoded.split(":", 1) + try: + auth_decoded = base64.b64decode(auth_header[6:]).decode() + username, password = auth_decoded.split(":", 1) + except (binascii.Error, ValueError, UnicodeDecodeError): + return False return settings.check_password(username, password) return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 9ea7a5164b..daff384515 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -2,6 +2,7 @@ from __future__ import annotations from argparse import Namespace import asyncio +import base64 from collections.abc import Generator from contextlib import asynccontextmanager import gzip @@ -1741,3 +1742,85 @@ def test_proc_on_exit_skips_when_already_closed() -> None: handler.write_message.assert_not_called() handler.close.assert_not_called() + + +def _make_auth_handler(auth_header: str | None = None) -> Mock: + """Create a mock handler with the given Authorization header.""" + handler = Mock() + handler.request = Mock() + if auth_header is not None: + handler.request.headers = {"Authorization": auth_header} + else: + handler.request.headers = {} + handler.get_secure_cookie = Mock(return_value=None) + return handler + + +@pytest.fixture +def mock_auth_settings(mock_dashboard_settings: MagicMock) -> MagicMock: + """Fixture to configure mock dashboard settings with auth enabled.""" + mock_dashboard_settings.using_auth = True + mock_dashboard_settings.on_ha_addon = False + return mock_dashboard_settings + + +@pytest.mark.usefixtures("mock_auth_settings") +def test_is_authenticated_malformed_base64() -> None: + """Test that invalid base64 in Authorization header returns False.""" + handler = _make_auth_handler("Basic !!!not-valid-base64!!!") + assert web_server.is_authenticated(handler) is False + + +@pytest.mark.usefixtures("mock_auth_settings") +def test_is_authenticated_bad_base64_padding() -> None: + """Test that incorrect base64 padding (binascii.Error) returns False.""" + handler = _make_auth_handler("Basic abc") + assert web_server.is_authenticated(handler) is False + + +@pytest.mark.usefixtures("mock_auth_settings") +def test_is_authenticated_invalid_utf8() -> None: + """Test that base64 decoding to invalid UTF-8 returns False.""" + # \xff\xfe is invalid UTF-8 + bad_payload = base64.b64encode(b"\xff\xfe").decode("ascii") + handler = _make_auth_handler(f"Basic {bad_payload}") + assert web_server.is_authenticated(handler) is False + + +@pytest.mark.usefixtures("mock_auth_settings") +def test_is_authenticated_no_colon() -> None: + """Test that base64 payload without ':' separator returns False.""" + no_colon = base64.b64encode(b"nocolonhere").decode("ascii") + handler = _make_auth_handler(f"Basic {no_colon}") + assert web_server.is_authenticated(handler) is False + + +def test_is_authenticated_valid_credentials( + mock_auth_settings: MagicMock, +) -> None: + """Test that valid Basic auth credentials are checked.""" + creds = base64.b64encode(b"admin:secret").decode("ascii") + mock_auth_settings.check_password.return_value = True + handler = _make_auth_handler(f"Basic {creds}") + assert web_server.is_authenticated(handler) is True + mock_auth_settings.check_password.assert_called_once_with("admin", "secret") + + +def test_is_authenticated_wrong_credentials( + mock_auth_settings: MagicMock, +) -> None: + """Test that valid Basic auth with wrong credentials returns False.""" + creds = base64.b64encode(b"admin:wrong").decode("ascii") + mock_auth_settings.check_password.return_value = False + handler = _make_auth_handler(f"Basic {creds}") + assert web_server.is_authenticated(handler) is False + + +def test_is_authenticated_no_auth_configured( + mock_dashboard_settings: MagicMock, +) -> None: + """Test that requests pass when auth is not configured.""" + mock_dashboard_settings.using_auth = False + mock_dashboard_settings.on_ha_addon = False + handler = _make_auth_handler() + assert web_server.is_authenticated(handler) is True From be4e573cc4af420e8f34af8d9c667e11a9662349 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:45:18 -0600 Subject: [PATCH 149/251] [esp32_hosted] Replace set_retry with set_interval to avoid heap allocation (#13844) --- .../update/esp32_hosted_update.cpp | 20 +++++++++++++------ .../esp32_hosted/update/esp32_hosted_update.h | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index a7d5f7e3d5..c8e2e879d4 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -27,6 +27,11 @@ static const char *const TAG = "esp32_hosted.update"; // Older coprocessor firmware versions have a 1500-byte limit per RPC call constexpr size_t CHUNK_SIZE = 1500; +#ifdef USE_ESP32_HOSTED_HTTP_UPDATE +// Interval/timeout IDs (uint32_t to avoid string comparison) +constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; +#endif + // Compile-time version string from esp_hosted_host_fw_ver.h macros #define STRINGIFY_(x) #x #define STRINGIFY(x) STRINGIFY_(x) @@ -127,15 +132,18 @@ void Esp32HostedUpdate::setup() { this->status_clear_error(); this->publish_state(); #else - // HTTP mode: retry initial check every 10s until network is ready (max 6 attempts) + // HTTP mode: check every 10s until network is ready (max 6 attempts) // Only if update interval is > 1 minute to avoid redundant checks if (this->get_update_interval() > 60000) { - this->set_retry("initial_check", 10000, 6, [this](uint8_t) { - if (!network::is_connected()) { - return RetryResult::RETRY; + this->initial_check_remaining_ = 6; + this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() { + bool connected = network::is_connected(); + if (--this->initial_check_remaining_ == 0 || connected) { + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); + if (connected) { + this->check(); + } } - this->check(); - return RetryResult::DONE; }); } #endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.h b/esphome/components/esp32_hosted/update/esp32_hosted_update.h index 7c9645c12a..005e6a6f21 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.h +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.h @@ -44,6 +44,7 @@ class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent { // HTTP mode helpers bool fetch_manifest_(); bool stream_firmware_to_coprocessor_(); + uint8_t initial_check_remaining_{0}; #else // Embedded mode members const uint8_t *firmware_data_{nullptr}; From 3cde3dacebf4bc39d36fcd37abc342e198d0500f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 08:45:33 -0600 Subject: [PATCH 150/251] [api] Collapse APIServerConnection intermediary layer (#13872) --- esphome/components/api/api_connection.cpp | 122 ++++++---- esphome/components/api/api_connection.h | 109 +++++---- esphome/components/api/api_pb2_service.cpp | 194 --------------- esphome/components/api/api_pb2_service.h | 261 --------------------- script/api_protobuf/api_protobuf.py | 40 ---- 5 files changed, 141 insertions(+), 585 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index efc3d210b4..ddc24a7e2c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -283,7 +283,7 @@ void APIConnection::loop() { #endif } -bool APIConnection::send_disconnect_response() { +bool APIConnection::send_disconnect_response_() { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop @@ -406,7 +406,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.device_class = cover->get_device_class_ref(); return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::cover_command(const CoverCommandRequest &msg) { +void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) if (msg.has_position) call.set_position(msg.position); @@ -449,7 +449,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supported_preset_modes = &traits.supported_preset_modes(); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::fan_command(const FanCommandRequest &msg) { +void APIConnection::on_fan_command_request(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) if (msg.has_state) call.set_state(msg.state); @@ -517,7 +517,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c msg.effects = &effects_list; return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::light_command(const LightCommandRequest &msg) { +void APIConnection::on_light_command_request(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) if (msg.has_state) call.set_state(msg.state); @@ -594,7 +594,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * msg.device_class = a_switch->get_device_class_ref(); return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::switch_command(const SwitchCommandRequest &msg) { +void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) if (msg.state) { @@ -692,7 +692,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_swing_modes = &traits.get_supported_swing_modes(); return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::climate_command(const ClimateCommandRequest &msg) { +void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) if (msg.has_mode) call.set_mode(static_cast<climate::ClimateMode>(msg.mode)); @@ -742,7 +742,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.step = number->traits.get_step(); return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::number_command(const NumberCommandRequest &msg) { +void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) call.set_value(msg.state); call.perform(); @@ -767,7 +767,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co ListEntitiesDateResponse msg; return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::date_command(const DateCommandRequest &msg) { +void APIConnection::on_date_command_request(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) call.set_date(msg.year, msg.month, msg.day); call.perform(); @@ -792,7 +792,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co ListEntitiesTimeResponse msg; return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::time_command(const TimeCommandRequest &msg) { +void APIConnection::on_time_command_request(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) call.set_time(msg.hour, msg.minute, msg.second); call.perform(); @@ -819,7 +819,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection ListEntitiesDateTimeResponse msg; return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { +void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) call.set_datetime(msg.epoch_seconds); call.perform(); @@ -848,7 +848,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.pattern = text->traits.get_pattern_ref(); return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::text_command(const TextCommandRequest &msg) { +void APIConnection::on_text_command_request(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) call.set_value(msg.state); call.perform(); @@ -874,7 +874,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection * msg.options = &select->traits.get_options(); return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::select_command(const SelectCommandRequest &msg) { +void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) call.set_option(msg.state.c_str(), msg.state.size()); call.perform(); @@ -888,7 +888,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * msg.device_class = button->get_device_class_ref(); return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); } -void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { +void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) button->press(); } @@ -914,7 +914,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co msg.requires_code = a_lock->traits.get_requires_code(); return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::lock_command(const LockCommandRequest &msg) { +void APIConnection::on_lock_command_request(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) switch (msg.command) { @@ -952,7 +952,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.supports_stop = traits.get_supports_stop(); return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::valve_command(const ValveCommandRequest &msg) { +void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) if (msg.has_position) call.set_position(msg.position); @@ -996,7 +996,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { +void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) if (msg.has_command) { call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command)); @@ -1063,7 +1063,7 @@ uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection * ListEntitiesCameraResponse msg; return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::camera_image(const CameraImageRequest &msg) { +void APIConnection::on_camera_image_request(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) return; @@ -1092,41 +1092,47 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { #endif #ifdef USE_BLUETOOTH_PROXY -void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { +void APIConnection::on_subscribe_bluetooth_le_advertisements_request( + const SubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); } -void APIConnection::unsubscribe_bluetooth_le_advertisements() { +void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } -void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { +void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); } -void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) { +void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg); } -void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) { +void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg); } -void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) { +void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg); } -void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) { +void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg); } -void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) { +void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg); } -void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) { +void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); } -bool APIConnection::send_subscribe_bluetooth_connections_free_response() { +bool APIConnection::send_subscribe_bluetooth_connections_free_response_() { bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this); return true; } +void APIConnection::on_subscribe_bluetooth_connections_free_request() { + if (!this->send_subscribe_bluetooth_connections_free_response_()) { + this->on_fatal_error(); + } +} -void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { +void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); } @@ -1138,7 +1144,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const { voice_assistant::global_voice_assistant->get_api_connection() == this; } -void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { +void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { if (voice_assistant::global_voice_assistant != nullptr) { voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); } @@ -1184,7 +1190,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno } } -bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) { +bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); @@ -1221,8 +1227,13 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA resp.max_active_wake_words = config.max_active_wake_words; return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); } +void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { + if (!this->send_voice_assistant_get_configuration_response_(msg)) { + this->on_fatal_error(); + } +} -void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { +void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); } @@ -1230,11 +1241,11 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon #endif #ifdef USE_ZWAVE_PROXY -void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) { +void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len); } -void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) { +void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); } #endif @@ -1262,7 +1273,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { +void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) switch (msg.command) { case enums::ALARM_CONTROL_PANEL_DISARM: @@ -1322,7 +1333,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) { +void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater) if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE) call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode)); @@ -1364,7 +1375,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c #endif #ifdef USE_IR_RF -void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) { +void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) { // TODO: When RF is implemented, add a field to the message to distinguish IR vs RF // and dispatch to the appropriate entity type based on that field. #ifdef USE_INFRARED @@ -1418,7 +1429,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection * msg.device_class = update->get_device_class_ref(); return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::update_command(const UpdateCommandRequest &msg) { +void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) switch (msg.command) { @@ -1469,7 +1480,7 @@ void APIConnection::complete_authentication_() { #endif } -bool APIConnection::send_hello_response(const HelloRequest &msg) { +bool APIConnection::send_hello_response_(const HelloRequest &msg) { // Copy client name with truncation if needed (set_client_name handles truncation) this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size()); this->client_api_version_major_ = msg.api_version_major; @@ -1490,12 +1501,12 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -bool APIConnection::send_ping_response() { +bool APIConnection::send_ping_response_() { PingResponse resp; return this->send_message(resp, PingResponse::MESSAGE_TYPE); } -bool APIConnection::send_device_info_response() { +bool APIConnection::send_device_info_response_() { DeviceInfoResponse resp{}; resp.name = StringRef(App.get_name()); resp.friendly_name = StringRef(App.get_friendly_name()); @@ -1618,6 +1629,26 @@ bool APIConnection::send_device_info_response() { return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); } +void APIConnection::on_hello_request(const HelloRequest &msg) { + if (!this->send_hello_response_(msg)) { + this->on_fatal_error(); + } +} +void APIConnection::on_disconnect_request() { + if (!this->send_disconnect_response_()) { + this->on_fatal_error(); + } +} +void APIConnection::on_ping_request() { + if (!this->send_ping_response_()) { + this->on_fatal_error(); + } +} +void APIConnection::on_device_info_request() { + if (!this->send_device_info_response_()) { + this->on_fatal_error(); + } +} #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { @@ -1656,7 +1687,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes } #endif #ifdef USE_API_USER_DEFINED_ACTIONS -void APIConnection::execute_service(const ExecuteServiceRequest &msg) { +void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { bool found = false; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES // Register the call and get a unique server-generated action_call_id @@ -1722,7 +1753,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe }; #endif #ifdef USE_API_NOISE -bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { +bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; resp.success = false; @@ -1743,9 +1774,14 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); } +void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { + if (!this->send_noise_encryption_set_key_response_(msg)) { + this->on_fatal_error(); + } +} #endif #ifdef USE_API_HOMEASSISTANT_STATES -void APIConnection::subscribe_home_assistant_states() { state_subs_at_ = 0; } +void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; } #endif bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { if (this->flags_.remove) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 935393b2da..ae7f864568 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -47,72 +47,72 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void cover_command(const CoverCommandRequest &msg) override; + void on_cover_command_request(const CoverCommandRequest &msg) override; #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void fan_command(const FanCommandRequest &msg) override; + void on_fan_command_request(const FanCommandRequest &msg) override; #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void light_command(const LightCommandRequest &msg) override; + void on_light_command_request(const LightCommandRequest &msg) override; #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void switch_command(const SwitchCommandRequest &msg) override; + void on_switch_command_request(const SwitchCommandRequest &msg) override; #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_CAMERA void set_camera_state(std::shared_ptr<camera::CameraImage> image); - void camera_image(const CameraImageRequest &msg) override; + void on_camera_image_request(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void climate_command(const ClimateCommandRequest &msg) override; + void on_climate_command_request(const ClimateCommandRequest &msg) override; #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void number_command(const NumberCommandRequest &msg) override; + void on_number_command_request(const NumberCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void date_command(const DateCommandRequest &msg) override; + void on_date_command_request(const DateCommandRequest &msg) override; #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void time_command(const TimeCommandRequest &msg) override; + void on_time_command_request(const TimeCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void datetime_command(const DateTimeCommandRequest &msg) override; + void on_date_time_command_request(const DateTimeCommandRequest &msg) override; #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void text_command(const TextCommandRequest &msg) override; + void on_text_command_request(const TextCommandRequest &msg) override; #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void select_command(const SelectCommandRequest &msg) override; + void on_select_command_request(const SelectCommandRequest &msg) override; #endif #ifdef USE_BUTTON - void button_command(const ButtonCommandRequest &msg) override; + void on_button_command_request(const ButtonCommandRequest &msg) override; #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void lock_command(const LockCommandRequest &msg) override; + void on_lock_command_request(const LockCommandRequest &msg) override; #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void valve_command(const ValveCommandRequest &msg) override; + void on_valve_command_request(const ValveCommandRequest &msg) override; #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void media_player_command(const MediaPlayerCommandRequest &msg) override; + void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); #ifdef USE_API_HOMEASSISTANT_SERVICES @@ -126,18 +126,18 @@ class APIConnection final : public APIServerConnection { #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY - void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; - void unsubscribe_bluetooth_le_advertisements() override; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; + void on_unsubscribe_bluetooth_le_advertisements_request() override; - void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; - void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; - void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override; - void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override; - void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; - void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; - void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; - bool send_subscribe_bluetooth_connections_free_response() override; - void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; + void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override; + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override; + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override; + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override; + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override; + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override; + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; + void on_subscribe_bluetooth_connections_free_request() override; + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; #endif #ifdef USE_HOMEASSISTANT_TIME @@ -148,33 +148,33 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_VOICE_ASSISTANT - void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; - bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override; - void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; #endif #ifdef USE_ZWAVE_PROXY - void zwave_proxy_frame(const ZWaveProxyFrame &msg) override; - void zwave_proxy_request(const ZWaveProxyRequest &msg) override; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override; + void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; #endif #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; #endif #ifdef USE_WATER_HEATER bool send_water_heater_state(water_heater::WaterHeater *water_heater); - void water_heater_command(const WaterHeaterCommandRequest &msg) override; + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; #endif #ifdef USE_IR_RF - void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override; void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif @@ -184,7 +184,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void update_command(const UpdateCommandRequest &msg) override; + void on_update_command_request(const UpdateCommandRequest &msg) override; #endif void on_disconnect_response() override; @@ -198,12 +198,12 @@ class APIConnection final : public APIServerConnection { #ifdef USE_HOMEASSISTANT_TIME void on_get_time_response(const GetTimeResponse &value) override; #endif - bool send_hello_response(const HelloRequest &msg) override; - bool send_disconnect_response() override; - bool send_ping_response() override; - bool send_device_info_response() override; - void list_entities() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } - void subscribe_states() override { + void on_hello_request(const HelloRequest &msg) override; + void on_disconnect_request() override; + void on_ping_request() override; + void on_device_info_request() override; + void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } + void on_subscribe_states_request() override { this->flags_.state_subscription = true; // Start initial state iterator only if no iterator is active // If list_entities is running, we'll start initial_state when it completes @@ -211,19 +211,19 @@ class APIConnection final : public APIServerConnection { this->begin_iterator_(ActiveIterator::INITIAL_STATE); } } - void subscribe_logs(const SubscribeLogsRequest &msg) override { + void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); } #ifdef USE_API_HOMEASSISTANT_SERVICES - void subscribe_homeassistant_services() override { this->flags_.service_call_subscription = true; } + void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES - void subscribe_home_assistant_states() override; + void on_subscribe_home_assistant_states_request() override; #endif #ifdef USE_API_USER_DEFINED_ACTIONS - void execute_service(const ExecuteServiceRequest &msg) override; + void on_execute_service_request(const ExecuteServiceRequest &msg) override; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON @@ -233,7 +233,7 @@ class APIConnection final : public APIServerConnection { #endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_NOISE - bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; #endif bool is_authenticated() override { @@ -283,6 +283,21 @@ class APIConnection final : public APIServerConnection { // Helper function to handle authentication completion void complete_authentication_(); + // Pattern B helpers: send response and return success/failure + bool send_hello_response_(const HelloRequest &msg); + bool send_disconnect_response_(); + bool send_ping_response_(); + bool send_device_info_response_(); +#ifdef USE_API_NOISE + bool send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_subscribe_bluetooth_connections_free_response_(); +#endif +#ifdef USE_VOICE_ASSISTANT + bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg); +#endif + #ifdef USE_CAMERA void try_send_camera_image_(); #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index df66b6eb83..1c04eacc82 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -623,200 +623,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } } -void APIServerConnection::on_hello_request(const HelloRequest &msg) { - if (!this->send_hello_response(msg)) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_disconnect_request() { - if (!this->send_disconnect_response()) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_ping_request() { - if (!this->send_ping_response()) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_device_info_request() { - if (!this->send_device_info_response()) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_list_entities_request() { this->list_entities(); } -void APIServerConnection::on_subscribe_states_request() { this->subscribe_states(); } -void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); } -#ifdef USE_API_HOMEASSISTANT_SERVICES -void APIServerConnection::on_subscribe_homeassistant_services_request() { this->subscribe_homeassistant_services(); } -#endif -#ifdef USE_API_HOMEASSISTANT_STATES -void APIServerConnection::on_subscribe_home_assistant_states_request() { this->subscribe_home_assistant_states(); } -#endif -#ifdef USE_API_USER_DEFINED_ACTIONS -void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } -#endif -#ifdef USE_API_NOISE -void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { - if (!this->send_noise_encryption_set_key_response(msg)) { - this->on_fatal_error(); - } -} -#endif -#ifdef USE_BUTTON -void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); } -#endif -#ifdef USE_CAMERA -void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); } -#endif -#ifdef USE_CLIMATE -void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); } -#endif -#ifdef USE_COVER -void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); } -#endif -#ifdef USE_DATETIME_DATE -void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); } -#endif -#ifdef USE_DATETIME_DATETIME -void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { - this->datetime_command(msg); -} -#endif -#ifdef USE_FAN -void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); } -#endif -#ifdef USE_LIGHT -void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); } -#endif -#ifdef USE_LOCK -void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); } -#endif -#ifdef USE_MEDIA_PLAYER -void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { - this->media_player_command(msg); -} -#endif -#ifdef USE_NUMBER -void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); } -#endif -#ifdef USE_SELECT -void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); } -#endif -#ifdef USE_SIREN -void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); } -#endif -#ifdef USE_SWITCH -void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); } -#endif -#ifdef USE_TEXT -void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); } -#endif -#ifdef USE_DATETIME_TIME -void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); } -#endif -#ifdef USE_UPDATE -void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); } -#endif -#ifdef USE_VALVE -void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); } -#endif -#ifdef USE_WATER_HEATER -void APIServerConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { - this->water_heater_command(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( - const SubscribeBluetoothLEAdvertisementsRequest &msg) { - this->subscribe_bluetooth_le_advertisements(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { - this->bluetooth_device_request(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { - this->bluetooth_gatt_get_services(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { - this->bluetooth_gatt_read(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { - this->bluetooth_gatt_write(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { - this->bluetooth_gatt_read_descriptor(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { - this->bluetooth_gatt_write_descriptor(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { - this->bluetooth_gatt_notify(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_subscribe_bluetooth_connections_free_request() { - if (!this->send_subscribe_bluetooth_connections_free_response()) { - this->on_fatal_error(); - } -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request() { - this->unsubscribe_bluetooth_le_advertisements(); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { - this->bluetooth_scanner_set_mode(msg); -} -#endif -#ifdef USE_VOICE_ASSISTANT -void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { - this->subscribe_voice_assistant(msg); -} -#endif -#ifdef USE_VOICE_ASSISTANT -void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { - if (!this->send_voice_assistant_get_configuration_response(msg)) { - this->on_fatal_error(); - } -} -#endif -#ifdef USE_VOICE_ASSISTANT -void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - this->voice_assistant_set_configuration(msg); -} -#endif -#ifdef USE_ALARM_CONTROL_PANEL -void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { - this->alarm_control_panel_command(msg); -} -#endif -#ifdef USE_ZWAVE_PROXY -void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); } -#endif -#ifdef USE_ZWAVE_PROXY -void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } -#endif -#ifdef USE_IR_RF -void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) { - this->infrared_rf_transmit_raw_timings(msg); -} -#endif - void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements for messages switch (msg_type) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index b8c9e4da6f..4dc6ce27d0 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -229,268 +229,7 @@ class APIServerConnectionBase : public ProtoService { }; class APIServerConnection : public APIServerConnectionBase { - public: - virtual bool send_hello_response(const HelloRequest &msg) = 0; - virtual bool send_disconnect_response() = 0; - virtual bool send_ping_response() = 0; - virtual bool send_device_info_response() = 0; - virtual void list_entities() = 0; - virtual void subscribe_states() = 0; - virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; -#ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void subscribe_homeassistant_services() = 0; -#endif -#ifdef USE_API_HOMEASSISTANT_STATES - virtual void subscribe_home_assistant_states() = 0; -#endif -#ifdef USE_API_USER_DEFINED_ACTIONS - virtual void execute_service(const ExecuteServiceRequest &msg) = 0; -#endif -#ifdef USE_API_NOISE - virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0; -#endif -#ifdef USE_BUTTON - virtual void button_command(const ButtonCommandRequest &msg) = 0; -#endif -#ifdef USE_CAMERA - virtual void camera_image(const CameraImageRequest &msg) = 0; -#endif -#ifdef USE_CLIMATE - virtual void climate_command(const ClimateCommandRequest &msg) = 0; -#endif -#ifdef USE_COVER - virtual void cover_command(const CoverCommandRequest &msg) = 0; -#endif -#ifdef USE_DATETIME_DATE - virtual void date_command(const DateCommandRequest &msg) = 0; -#endif -#ifdef USE_DATETIME_DATETIME - virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; -#endif -#ifdef USE_FAN - virtual void fan_command(const FanCommandRequest &msg) = 0; -#endif -#ifdef USE_LIGHT - virtual void light_command(const LightCommandRequest &msg) = 0; -#endif -#ifdef USE_LOCK - virtual void lock_command(const LockCommandRequest &msg) = 0; -#endif -#ifdef USE_MEDIA_PLAYER - virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; -#endif -#ifdef USE_NUMBER - virtual void number_command(const NumberCommandRequest &msg) = 0; -#endif -#ifdef USE_SELECT - virtual void select_command(const SelectCommandRequest &msg) = 0; -#endif -#ifdef USE_SIREN - virtual void siren_command(const SirenCommandRequest &msg) = 0; -#endif -#ifdef USE_SWITCH - virtual void switch_command(const SwitchCommandRequest &msg) = 0; -#endif -#ifdef USE_TEXT - virtual void text_command(const TextCommandRequest &msg) = 0; -#endif -#ifdef USE_DATETIME_TIME - virtual void time_command(const TimeCommandRequest &msg) = 0; -#endif -#ifdef USE_UPDATE - virtual void update_command(const UpdateCommandRequest &msg) = 0; -#endif -#ifdef USE_VALVE - virtual void valve_command(const ValveCommandRequest &msg) = 0; -#endif -#ifdef USE_WATER_HEATER - virtual void water_heater_command(const WaterHeaterCommandRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_device_request(const BluetoothDeviceRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual bool send_subscribe_bluetooth_connections_free_response() = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void unsubscribe_bluetooth_le_advertisements() = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0; -#endif -#ifdef USE_VOICE_ASSISTANT - virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; -#endif -#ifdef USE_VOICE_ASSISTANT - virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0; -#endif -#ifdef USE_VOICE_ASSISTANT - virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; -#endif -#ifdef USE_ZWAVE_PROXY - virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0; -#endif -#ifdef USE_ZWAVE_PROXY - virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0; -#endif -#ifdef USE_IR_RF - virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0; -#endif protected: - void on_hello_request(const HelloRequest &msg) override; - void on_disconnect_request() override; - void on_ping_request() override; - void on_device_info_request() override; - void on_list_entities_request() override; - void on_subscribe_states_request() override; - void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; -#ifdef USE_API_HOMEASSISTANT_SERVICES - void on_subscribe_homeassistant_services_request() override; -#endif -#ifdef USE_API_HOMEASSISTANT_STATES - void on_subscribe_home_assistant_states_request() override; -#endif -#ifdef USE_API_USER_DEFINED_ACTIONS - void on_execute_service_request(const ExecuteServiceRequest &msg) override; -#endif -#ifdef USE_API_NOISE - void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; -#endif -#ifdef USE_BUTTON - void on_button_command_request(const ButtonCommandRequest &msg) override; -#endif -#ifdef USE_CAMERA - void on_camera_image_request(const CameraImageRequest &msg) override; -#endif -#ifdef USE_CLIMATE - void on_climate_command_request(const ClimateCommandRequest &msg) override; -#endif -#ifdef USE_COVER - void on_cover_command_request(const CoverCommandRequest &msg) override; -#endif -#ifdef USE_DATETIME_DATE - void on_date_command_request(const DateCommandRequest &msg) override; -#endif -#ifdef USE_DATETIME_DATETIME - void on_date_time_command_request(const DateTimeCommandRequest &msg) override; -#endif -#ifdef USE_FAN - void on_fan_command_request(const FanCommandRequest &msg) override; -#endif -#ifdef USE_LIGHT - void on_light_command_request(const LightCommandRequest &msg) override; -#endif -#ifdef USE_LOCK - void on_lock_command_request(const LockCommandRequest &msg) override; -#endif -#ifdef USE_MEDIA_PLAYER - void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; -#endif -#ifdef USE_NUMBER - void on_number_command_request(const NumberCommandRequest &msg) override; -#endif -#ifdef USE_SELECT - void on_select_command_request(const SelectCommandRequest &msg) override; -#endif -#ifdef USE_SIREN - void on_siren_command_request(const SirenCommandRequest &msg) override; -#endif -#ifdef USE_SWITCH - void on_switch_command_request(const SwitchCommandRequest &msg) override; -#endif -#ifdef USE_TEXT - void on_text_command_request(const TextCommandRequest &msg) override; -#endif -#ifdef USE_DATETIME_TIME - void on_time_command_request(const TimeCommandRequest &msg) override; -#endif -#ifdef USE_UPDATE - void on_update_command_request(const UpdateCommandRequest &msg) override; -#endif -#ifdef USE_VALVE - void on_valve_command_request(const ValveCommandRequest &msg) override; -#endif -#ifdef USE_WATER_HEATER - void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_connections_free_request() override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_unsubscribe_bluetooth_le_advertisements_request() override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; -#endif -#ifdef USE_VOICE_ASSISTANT - void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; -#endif -#ifdef USE_VOICE_ASSISTANT - void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; -#endif -#ifdef USE_VOICE_ASSISTANT - void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; -#endif -#ifdef USE_ZWAVE_PROXY - void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override; -#endif -#ifdef USE_ZWAVE_PROXY - void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; -#endif -#ifdef USE_IR_RF - void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override; -#endif void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 5fbc1137a8..8673996a25 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2906,7 +2906,6 @@ static const char *const TAG = "api.service"; class_name = "APIServerConnection" hpp += "\n" hpp += f"class {class_name} : public {class_name}Base {{\n" - hpp += " public:\n" hpp_protected = "" cpp += "\n" @@ -2914,14 +2913,8 @@ static const char *const TAG = "api.service"; message_auth_map: dict[str, bool] = {} message_conn_map: dict[str, bool] = {} - m = serv.method[0] for m in serv.method: - func = m.name inp = m.input_type[1:] - ret = m.output_type[1:] - is_void = ret == "void" - snake = camel_to_snake(inp) - on_func = f"on_{snake}" needs_conn = get_opt(m, pb.needs_setup_connection, True) needs_auth = get_opt(m, pb.needs_authentication, True) @@ -2929,39 +2922,6 @@ static const char *const TAG = "api.service"; message_auth_map[inp] = needs_auth message_conn_map[inp] = needs_conn - ifdef = message_ifdef_map.get(inp, ifdefs.get(inp)) - - if ifdef is not None: - hpp += f"#ifdef {ifdef}\n" - hpp_protected += f"#ifdef {ifdef}\n" - cpp += f"#ifdef {ifdef}\n" - - is_empty = inp in EMPTY_MESSAGES - param = "" if is_empty else f"const {inp} &msg" - arg = "" if is_empty else "msg" - - hpp_protected += f" void {on_func}({param}) override;\n" - if is_void: - hpp += f" virtual void {func}({param}) = 0;\n" - else: - hpp += f" virtual bool send_{func}_response({param}) = 0;\n" - - cpp += f"void {class_name}::{on_func}({param}) {{\n" - body = "" - if is_void: - body += f"this->{func}({arg});\n" - else: - body += f"if (!this->send_{func}_response({arg})) {{\n" - body += " this->on_fatal_error();\n" - body += "}\n" - - cpp += indent(body) + "\n" + "}\n" - - if ifdef is not None: - hpp += "#endif\n" - hpp_protected += "#endif\n" - cpp += "#endif\n" - # Generate optimized read_message with authentication checking # Categorize messages by their authentication requirements no_conn_ids: set[int] = set() From c28c97fbaf15c4ce353317e873054fb00a1d7a6d Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 9 Feb 2026 09:19:00 -0600 Subject: [PATCH 151/251] [mixer] Refactor for stability and to support Sendspin (#12253) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/mixer/speaker/__init__.py | 10 +- esphome/components/mixer/speaker/automation.h | 4 +- .../mixer/speaker/mixer_speaker.cpp | 438 +++++++++++++----- .../components/mixer/speaker/mixer_speaker.h | 53 ++- 4 files changed, 364 insertions(+), 141 deletions(-) diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index c4069851af..a3025d7121 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import audio, esp32, speaker +from esphome.components import audio, esp32, socket, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -61,7 +61,7 @@ def _set_stream_limits(config): def _validate_source_speaker(config): fconf = fv.full_config.get() - # Get ID for the output speaker and add it to the source speakrs config to easily inherit properties + # Get ID for the output speaker and add it to the source speakers config to easily inherit properties path = fconf.get_path_for_id(config[CONF_ID])[:-3] path.append(CONF_OUTPUT_SPEAKER) output_speaker_id = fconf.get_config_for_path(path) @@ -111,6 +111,9 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): + # Enable wake_loop_threadsafe for immediate command processing from other tasks + socket.require_wake_loop_threadsafe() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -127,6 +130,9 @@ async def to_code(config): "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True ) + # Initialize FixedVector with exact count of source speakers + cg.add(var.init_source_speakers(len(config[CONF_SOURCE_SPEAKERS]))) + for speaker_config in config[CONF_SOURCE_SPEAKERS]: source_speaker = cg.new_Pvariable(speaker_config[CONF_ID]) diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index 2fb2f49373..4fa3853583 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -8,8 +8,8 @@ namespace esphome { namespace mixer_speaker { template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> { - TEMPLATABLE_VALUE(uint8_t, decibel_reduction) - TEMPLATABLE_VALUE(uint32_t, duration) + TEMPLATABLE_VALUE(uint8_t, decibel_reduction); + TEMPLATABLE_VALUE(uint32_t, duration); void play(const Ts &...x) override { this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...)); } diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 043b629cf1..100acbebc3 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -2,11 +2,13 @@ #ifdef USE_ESP32 +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include <algorithm> +#include <array> #include <cstring> namespace esphome { @@ -14,6 +16,7 @@ namespace mixer_speaker { static const UBaseType_t MIXER_TASK_PRIORITY = 10; +static const uint32_t STOPPING_TIMEOUT_MS = 5000; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; static const uint32_t TASK_DELAY_MS = 25; @@ -27,21 +30,53 @@ static const char *const TAG = "speaker_mixer"; // Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB // dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) // float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15) -static const std::vector<int16_t> DECIBEL_REDUCTION_TABLE = { +static const std::array<int16_t, 51> DECIBEL_REDUCTION_TABLE = { 32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183, 4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731, 651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103}; -enum MixerEventGroupBits : uint32_t { - COMMAND_STOP = (1 << 0), // stops the mixer task - STATE_STARTING = (1 << 10), - STATE_RUNNING = (1 << 11), - STATE_STOPPING = (1 << 12), - STATE_STOPPED = (1 << 13), - ERR_ESP_NO_MEM = (1 << 19), - ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +// Event bits for SourceSpeaker command processing +enum SourceSpeakerEventBits : uint32_t { + SOURCE_SPEAKER_COMMAND_START = (1 << 0), + SOURCE_SPEAKER_COMMAND_STOP = (1 << 1), + SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2), }; +// Event bits for mixer task control and state +enum MixerTaskEventBits : uint32_t { + MIXER_TASK_COMMAND_START = (1 << 0), + MIXER_TASK_COMMAND_STOP = (1 << 1), + MIXER_TASK_STATE_STARTING = (1 << 10), + MIXER_TASK_STATE_RUNNING = (1 << 11), + MIXER_TASK_STATE_STOPPING = (1 << 12), + MIXER_TASK_STATE_STOPPED = (1 << 13), + MIXER_TASK_ERR_ESP_NO_MEM = (1 << 19), + MIXER_TASK_ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +}; + +static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) { + uint32_t current = var.load(std::memory_order_acquire); + uint32_t subtracted = 0; + if (current > 0) { + uint32_t new_value; + do { + subtracted = std::min(amount, current); + new_value = current - subtracted; + } while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire)); + } + return subtracted; +} + +static bool create_event_group(EventGroupHandle_t &event_group, Component *component) { + event_group = xEventGroupCreate(); + if (event_group == nullptr) { + ESP_LOGE(TAG, "Failed to create event group"); + component->mark_failed(); + return false; + } + return true; +} + void SourceSpeaker::dump_config() { ESP_LOGCONFIG(TAG, "Mixer Source Speaker\n" @@ -55,22 +90,70 @@ void SourceSpeaker::dump_config() { } void SourceSpeaker::setup() { - this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) { - // The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames - uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_); - this->pending_playback_frames_ -= speakers_playback_frames; + if (!create_event_group(this->event_group_, this)) { + return; + } - if (speakers_playback_frames > 0) { - this->audio_output_callback_(speakers_playback_frames, write_timestamp); + // Start with loop disabled since we begin in STATE_STOPPED with no pending commands + this->disable_loop(); + + this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) { + // First, drain the playback delay (frames in pipeline before this source started contributing) + uint32_t delay_to_drain = atomic_subtract_clamped(this->playback_delay_frames_, new_frames); + uint32_t remaining_frames = new_frames - delay_to_drain; + + // Then, count towards this source's pending playback frames + if (remaining_frames > 0) { + uint32_t speakers_playback_frames = atomic_subtract_clamped(this->pending_playback_frames_, remaining_frames); + if (speakers_playback_frames > 0) { + this->audio_output_callback_(speakers_playback_frames, write_timestamp); + } } }); } void SourceSpeaker::loop() { + uint32_t event_bits = xEventGroupGetBits(this->event_group_); + + // Process commands with priority: STOP > FINISH > START + // This ensures stop commands take precedence over conflicting start commands + if (event_bits & SOURCE_SPEAKER_COMMAND_STOP) { + if (this->state_ == speaker::STATE_RUNNING) { + // Clear both STOP and START bits - stop takes precedence + xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START); + this->enter_stopping_state_(); + } else if (this->state_ == speaker::STATE_STOPPED) { + // Already stopped, just clear the command bits + xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START); + } + // Leave bits set if transitioning states (STARTING/STOPPING) - will be processed once state allows + } else if (event_bits & SOURCE_SPEAKER_COMMAND_FINISH) { + if (this->state_ == speaker::STATE_RUNNING) { + xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH); + this->stop_gracefully_ = true; + } else if (this->state_ == speaker::STATE_STOPPED) { + // Already stopped, just clear the command bit + xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH); + } + // Leave bit set if transitioning states - will be processed once state allows + } else if (event_bits & SOURCE_SPEAKER_COMMAND_START) { + if (this->state_ == speaker::STATE_STOPPED) { + xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START); + this->state_ = speaker::STATE_STARTING; + } else if (this->state_ == speaker::STATE_RUNNING) { + // Already running, just clear the command bit + xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START); + } + // Leave bit set if transitioning states - will be processed once state allows + } + // Process state machine switch (this->state_) { case speaker::STATE_STARTING: { esp_err_t err = this->start_(); if (err == ESP_OK) { + this->pending_playback_frames_.store(0, std::memory_order_release); // reset pending playback frames + this->playback_delay_frames_.store(0, std::memory_order_release); // reset playback delay + this->has_contributed_.store(false, std::memory_order_release); // reset contribution tracking this->state_ = speaker::STATE_RUNNING; this->stop_gracefully_ = false; this->last_seen_data_ms_ = millis(); @@ -78,41 +161,62 @@ void SourceSpeaker::loop() { } else { switch (err) { case ESP_ERR_NO_MEM: - this->status_set_error(LOG_STR("Failed to start mixer: not enough memory")); + this->status_set_error(LOG_STR("Not enough memory")); break; case ESP_ERR_NOT_SUPPORTED: - this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample")); + this->status_set_error(LOG_STR("Unsupported bit depth")); break; case ESP_ERR_INVALID_ARG: - this->status_set_error( - LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream.")); + this->status_set_error(LOG_STR("Incompatible audio streams")); break; case ESP_ERR_INVALID_STATE: - this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start")); + this->status_set_error(LOG_STR("Task failed")); break; default: - this->status_set_error(LOG_STR("Failed to start mixer")); + this->status_set_error(LOG_STR("Failed")); break; } - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); } break; } case speaker::STATE_RUNNING: - if (!this->transfer_buffer_->has_buffered_data()) { + if (!this->transfer_buffer_->has_buffered_data() && + (this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) { + // No audio data in buffer waiting to get mixed and no frames are pending playback if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) || this->stop_gracefully_) { - this->state_ = speaker::STATE_STOPPING; + // Timeout exceeded or graceful stop requested + this->enter_stopping_state_(); } } break; - case speaker::STATE_STOPPING: - this->stop_(); - this->stop_gracefully_ = false; - this->state_ = speaker::STATE_STOPPED; + case speaker::STATE_STOPPING: { + if ((this->parent_->get_output_speaker()->get_pause_state()) || + ((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) { + // If parent speaker is paused or if the stopping timeout is exceeded, force stop the output speaker + this->parent_->get_output_speaker()->stop(); + } + + if (this->parent_->get_output_speaker()->is_stopped() || + (this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) { + // Output speaker is stopped OR all pending playback frames have played + this->pending_playback_frames_.store(0, std::memory_order_release); + this->stop_gracefully_ = false; + + this->state_ = speaker::STATE_STOPPED; + } break; + } case speaker::STATE_STOPPED: + // Re-check event bits for any new commands that may have arrived + event_bits = xEventGroupGetBits(this->event_group_); + if (!(event_bits & + (SOURCE_SPEAKER_COMMAND_START | SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_FINISH))) { + // No pending commands, disable loop to save CPU cycles + this->disable_loop(); + } break; } } @@ -122,17 +226,34 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_ this->start(); } size_t bytes_written = 0; - if (this->ring_buffer_.use_count() == 1) { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + if (temp_ring_buffer.use_count() > 0) { + // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); if (bytes_written > 0) { this->last_seen_data_ms_ = millis(); } + } else { + // Delay to avoid repeatedly hammering while waiting for the speaker to start + vTaskDelay(ticks_to_wait); } return bytes_written; } -void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; } +void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { + this->enable_loop_soon_any_context(); + uint32_t event_bits = xEventGroupGetBits(this->event_group_); + if (!(event_bits & command_bit)) { + xEventGroupSetBits(this->event_group_, command_bit); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + if (wake_loop) { + App.wake_loop_threadsafe(); + } +#endif + } +} + +void SourceSpeaker::start() { this->send_command_(SOURCE_SPEAKER_COMMAND_START, true); } esp_err_t SourceSpeaker::start_() { const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_); @@ -143,35 +264,26 @@ esp_err_t SourceSpeaker::start_() { if (this->transfer_buffer_ == nullptr) { return ESP_ERR_NO_MEM; } - std::shared_ptr<RingBuffer> temp_ring_buffer; - if (!this->ring_buffer_.use_count()) { + std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + if (!temp_ring_buffer) { temp_ring_buffer = RingBuffer::create(ring_buffer_size); this->ring_buffer_ = temp_ring_buffer; } - if (!this->ring_buffer_.use_count()) { + if (!temp_ring_buffer) { return ESP_ERR_NO_MEM; } else { this->transfer_buffer_->set_source(temp_ring_buffer); } } - this->pending_playback_frames_ = 0; // reset return this->parent_->start(this->audio_stream_info_); } -void SourceSpeaker::stop() { - if (this->state_ != speaker::STATE_STOPPED) { - this->state_ = speaker::STATE_STOPPING; - } -} +void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); } -void SourceSpeaker::stop_() { - this->transfer_buffer_.reset(); // deallocates the transfer buffer -} - -void SourceSpeaker::finish() { this->stop_gracefully_ = true; } +void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); } bool SourceSpeaker::has_buffered_data() const { return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data()); @@ -191,19 +303,16 @@ void SourceSpeaker::set_volume(float volume) { float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); } -size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) { - if (!this->transfer_buffer_.use_count()) { - return 0; - } - +size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer, + TickType_t ticks_to_wait) { // Store current offset, as these samples are already ducked - const size_t current_length = this->transfer_buffer_->available(); + const size_t current_length = transfer_buffer->available(); - size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait); + size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait); uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read); if (samples_to_duck > 0) { - int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length); + int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length); duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_, &this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_, @@ -215,10 +324,13 @@ size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) { void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) { if (this->target_ducking_db_reduction_ != decibel_reduction) { + // Start transition from the previous target (which becomes the new current level) this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_; this->target_ducking_db_reduction_ = decibel_reduction; + // Calculate the number of intermediate dB steps for the transition timing. + // Subtract 1 because the first step is taken immediately after this calculation. uint8_t total_ducking_steps = 0; if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) { // The dB reduction level is increasing (which results in quieter audio) @@ -234,7 +346,7 @@ void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) this->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps; this->ducking_transition_samples_remaining_ = - this->samples_per_ducking_step_ * total_ducking_steps; // Adjust for integer division rounding + this->samples_per_ducking_step_ * total_ducking_steps; // adjust for integer division rounding this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_; } else { @@ -293,6 +405,12 @@ void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_t } } +void SourceSpeaker::enter_stopping_state_() { + this->state_ = speaker::STATE_STOPPING; + this->stopping_start_ms_ = millis(); + this->transfer_buffer_.reset(); +} + void MixerSpeaker::dump_config() { ESP_LOGCONFIG(TAG, "Speaker Mixer:\n" @@ -301,42 +419,74 @@ void MixerSpeaker::dump_config() { } void MixerSpeaker::setup() { - this->event_group_ = xEventGroupCreate(); - - if (this->event_group_ == nullptr) { - ESP_LOGE(TAG, "Failed to create event group"); - this->mark_failed(); + if (!create_event_group(this->event_group_, this)) { return; } + + // Register callback to track frames in the output pipeline + this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) { + atomic_subtract_clamped(this->frames_in_pipeline_, new_frames); + }); + + // Start with loop disabled since no task is running and no commands are pending + this->disable_loop(); } void MixerSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - if (event_group_bits & MixerEventGroupBits::STATE_STARTING) { - ESP_LOGD(TAG, "Starting speaker mixer"); - xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING); + // Handle pending start request + if (event_group_bits & MIXER_TASK_COMMAND_START) { + // Only start the task if it's fully stopped and cleaned up + if (!this->status_has_error() && (this->task_handle_ == nullptr) && (this->task_stack_buffer_ == nullptr)) { + esp_err_t err = this->start_task_(); + switch (err) { + case ESP_OK: + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START); + break; + case ESP_ERR_NO_MEM: + ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); + this->status_momentary_error("memory-failure", 1000); + return; + case ESP_ERR_INVALID_STATE: + ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); + this->status_momentary_error("task-failure", 1000); + return; + default: + ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); + this->status_momentary_error("failure", 1000); + return; + } + } } - if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer")); - xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM); + + if (event_group_bits & MIXER_TASK_STATE_STARTING) { + ESP_LOGD(TAG, "Starting"); + xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING); } - if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) { - ESP_LOGD(TAG, "Started speaker mixer"); + if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) { + this->status_set_error(LOG_STR("Not enough memory")); + xEventGroupClearBits(this->event_group_, MIXER_TASK_ERR_ESP_NO_MEM); + } + if (event_group_bits & MIXER_TASK_STATE_RUNNING) { + ESP_LOGV(TAG, "Started"); this->status_clear_error(); - xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING); + xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING); } - if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) { - ESP_LOGD(TAG, "Stopping speaker mixer"); - xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING); + if (event_group_bits & MIXER_TASK_STATE_STOPPING) { + ESP_LOGV(TAG, "Stopping"); + xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING); } - if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) { + if (event_group_bits & MIXER_TASK_STATE_STOPPED) { if (this->delete_task_() == ESP_OK) { - xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS); + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); } } if (this->task_handle_ != nullptr) { + // If the mixer task is running, check if all source speakers are stopped + bool all_stopped = true; for (auto &speaker : this->source_speakers_) { @@ -344,7 +494,15 @@ void MixerSpeaker::loop() { } if (all_stopped) { - this->stop(); + // Send stop command signal to the mixer task since no source speakers are active + xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + } + } else if (this->task_stack_buffer_ == nullptr) { + // Task is fully stopped and cleaned up, check if we can disable loop + event_group_bits = xEventGroupGetBits(this->event_group_); + if (event_group_bits == 0) { + // No pending events, disable loop to save CPU cycles + this->disable_loop(); } } } @@ -366,7 +524,18 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { } } - return this->start_task_(); + this->enable_loop_soon_any_context(); // ensure loop processes command + + uint32_t event_bits = xEventGroupGetBits(this->event_group_); + if (!(event_bits & MIXER_TASK_COMMAND_START)) { + // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency + xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + } + + return ESP_OK; } esp_err_t MixerSpeaker::start_task_() { @@ -397,28 +566,31 @@ esp_err_t MixerSpeaker::start_task_() { } esp_err_t MixerSpeaker::delete_task_() { - if (!this->task_created_) { + if (this->task_handle_ != nullptr) { + // Delete the task + vTaskDelete(this->task_handle_); this->task_handle_ = nullptr; - - if (this->task_stack_buffer_ != nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } - - this->task_stack_buffer_ = nullptr; - } - - return ESP_OK; } - return ESP_ERR_INVALID_STATE; -} + if ((this->task_handle_ == nullptr) && (this->task_stack_buffer_ != nullptr)) { + // Deallocate the task stack buffer + if (this->task_stack_in_psram_) { + RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); + stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); + } else { + RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL); + stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); + } -void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); } + this->task_stack_buffer_ = nullptr; + } + + if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) { + return ESP_ERR_INVALID_STATE; + } + + return ESP_OK; +} void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info, int16_t *output_buffer, audio::AudioStreamInfo output_stream_info, @@ -472,32 +644,34 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio } void MixerSpeaker::audio_mixer_task(void *params) { - MixerSpeaker *this_mixer = (MixerSpeaker *) params; + MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params); - xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING); - - this_mixer->task_created_ = true; + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING); std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create( this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); if (output_transfer_buffer == nullptr) { - xEventGroupSetBits(this_mixer->event_group_, - MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM); - this_mixer->task_created_ = false; - vTaskDelete(nullptr); + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } output_transfer_buffer->set_sink(this_mixer->output_speaker_); - xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING); bool sent_finished = false; + // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) + FixedVector<SourceSpeaker *> speakers_with_data; + FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; + speakers_with_data.init(this_mixer->source_speakers_.size()); + transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); + while (true) { uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); - if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) { + if (event_group_bits & MIXER_TASK_COMMAND_STOP) { break; } @@ -507,15 +681,20 @@ void MixerSpeaker::audio_mixer_task(void *params) { const uint32_t output_frames_free = this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); - std::vector<SourceSpeaker *> speakers_with_data; - std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; + speakers_with_data.clear(); + transfer_buffers_with_data.clear(); for (auto &speaker : this_mixer->source_speakers_) { - if (speaker->get_transfer_buffer().use_count() > 0) { + if (speaker->is_running() && !speaker->get_pause_state()) { + // Speaker is running and not paused, so it possibly can provide audio data std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock(); - speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers + if (transfer_buffer.use_count() == 0) { + // No transfer buffer allocated, so skip processing this speaker + continue; + } + speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers - if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) { + if (transfer_buffer->available() > 0) { // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop transfer_buffers_with_data.push_back(transfer_buffer); speakers_with_data.push_back(speaker); @@ -547,13 +726,21 @@ void MixerSpeaker::audio_mixer_task(void *params) { reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), this_mixer->audio_stream_info_.value(), frames_to_mix); - // Update source speaker buffer length - transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); - speakers_with_data[0]->pending_playback_frames_ += frames_to_mix; + // Set playback delay for newly contributing source + if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[0]->playback_delay_frames_.store( + this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release); + speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release); + } - // Update output transfer buffer length + // Update source speaker pending frames + speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); + transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + + // Update output transfer buffer length and pipeline frame count output_transfer_buffer->increase_buffer_length( this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); + this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); } else { // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker if (!this_mixer->output_speaker_->is_stopped()) { @@ -568,6 +755,8 @@ void MixerSpeaker::audio_mixer_task(void *params) { active_stream_info.get_sample_rate()); this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); this_mixer->output_speaker_->start(); + // Reset pipeline frame count since we're starting fresh with a new sample rate + this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); sent_finished = false; } } @@ -596,26 +785,39 @@ void MixerSpeaker::audio_mixer_task(void *params) { } } + // Get current pipeline depth for delay calculation (before incrementing) + uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); + // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + // Set playback delay for newly contributing sources + if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); + speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release); + } + + speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); transfer_buffers_with_data[i]->decrease_buffer_length( speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); - speakers_with_data[i]->pending_playback_frames_ += frames_to_mix; } - // Update output transfer buffer length + // Update output transfer buffer length and pipeline frame count (once, not per source) output_transfer_buffer->increase_buffer_length( this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); + this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); } } - xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPING); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING); + + // Reset pipeline frame count since the task is stopping + this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); output_transfer_buffer.reset(); - xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED); - this_mixer->task_created_ = false; - vTaskDelete(nullptr); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED); + + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } } // namespace mixer_speaker diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 48bacc4471..e920f9895a 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -7,26 +7,31 @@ #include "esphome/components/speaker/speaker.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" -#include <freertos/event_groups.h> #include <freertos/FreeRTOS.h> +#include <freertos/event_groups.h> + +#include <atomic> namespace esphome { namespace mixer_speaker { /* Classes for mixing several source speaker audio streams and writing it to another speaker component. * - Volume controls are passed through to the output speaker + * - Source speaker commands are signaled via event group bits and processed in its loop function to ensure thread + * safety * - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker. - * - Audio sent to the SourceSpeaker's must have 16 bits per sample. + * - Audio sent to the SourceSpeaker must have 16 bits per sample. * - Audio sent to the SourceSpeaker can have any number of channels. They are duplicated or ignored as needed to match * the number of channels required for the output speaker. - * - In queue mode, the audio sent to the SoureSpeakers can have different sample rates. + * - In queue mode, the audio sent to the SourceSpeakers can have different sample rates. * - In non-queue mode, the audio sent to the SourceSpeakers must have the same sample rates. * - SourceSpeaker has an internal ring buffer. It also allocates a shared_ptr for an AudioTranserBuffer object. * - Audio Data Flow: * - Audio data played on a SourceSpeaker first writes to its internal ring buffer. * - MixerSpeaker task temporarily takes shared ownership of each SourceSpeaker's AudioTransferBuffer. - * - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which tranfers audio from the SourceSpeaker's + * - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which transfers audio from the SourceSpeaker's * ring buffer to its AudioTransferBuffer. Audio ducking is applied at this step. * - In queue mode, MixerSpeaker prioritizes the earliest configured SourceSpeaker with audio data. Audio data is * sent to the output speaker. @@ -63,13 +68,15 @@ class SourceSpeaker : public speaker::Speaker, public Component { bool get_pause_state() const override { return this->pause_state_; } /// @brief Transfers audio from the ring buffer into the transfer buffer. Ducks audio while transferring. + /// @param transfer_buffer Locked shared_ptr to the transfer buffer (must be valid, not null) /// @param ticks_to_wait FreeRTOS ticks to wait while waiting to read from the ring buffer. /// @return Number of bytes transferred from the ring buffer. - size_t process_data_from_source(TickType_t ticks_to_wait); + size_t process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer, + TickType_t ticks_to_wait); /// @brief Sets the ducking level for the source speaker. - /// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB - /// @param duration (uint32_t) The number of milliseconds to transition from the current level to the new level + /// @param decibel_reduction The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB + /// @param duration The number of milliseconds to transition from the current level to the new level void apply_ducking(uint8_t decibel_reduction, uint32_t duration); void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } @@ -81,14 +88,15 @@ class SourceSpeaker : public speaker::Speaker, public Component { protected: friend class MixerSpeaker; esp_err_t start_(); - void stop_(); + void enter_stopping_state_(); + void send_command_(uint32_t command_bit, bool wake_loop = false); /// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually /// over a specified amount of samples. /// @param input_buffer buffer with audio samples to be ducked in place /// @param input_samples_to_duck number of samples to process in ``input_buffer`` /// @param current_ducking_db_reduction pointer to the current dB reduction - /// @param ducking_transition_samples_remaining pointer to the total number of samples left before the the + /// @param ducking_transition_samples_remaining pointer to the total number of samples left before the /// transition is finished /// @param samples_per_ducking_step total number of samples per ducking step for the transition /// @param db_change_per_ducking_step the change in dB reduction per step @@ -114,7 +122,12 @@ class SourceSpeaker : public speaker::Speaker, public Component { uint32_t ducking_transition_samples_remaining_{0}; uint32_t samples_per_ducking_step_{0}; - uint32_t pending_playback_frames_{0}; + std::atomic<uint32_t> pending_playback_frames_{0}; + std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing + std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session + + EventGroupHandle_t event_group_{nullptr}; + uint32_t stopping_start_ms_{0}; }; class MixerSpeaker : public Component { @@ -123,10 +136,11 @@ class MixerSpeaker : public Component { void setup() override; void loop() override; + void init_source_speakers(size_t count) { this->source_speakers_.init(count); } void add_source_speaker(SourceSpeaker *source_speaker) { this->source_speakers_.push_back(source_speaker); } /// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information - /// @param stream_info The calling source speakers audio stream information + /// @param stream_info The calling source speaker's audio stream information /// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample /// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream /// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack @@ -134,8 +148,6 @@ class MixerSpeaker : public Component { /// ESP_OK if the incoming stream is compatible and the mixer task starts esp_err_t start(audio::AudioStreamInfo &stream_info); - void stop(); - void set_output_channels(uint8_t output_channels) { this->output_channels_ = output_channels; } void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; } void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; } @@ -143,6 +155,9 @@ class MixerSpeaker : public Component { speaker::Speaker *get_output_speaker() const { return this->output_speaker_; } + /// @brief Returns the current number of frames in the output pipeline (written but not yet played) + uint32_t get_frames_in_pipeline() const { return this->frames_in_pipeline_.load(std::memory_order_acquire); } + protected: /// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels /// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has @@ -159,11 +174,11 @@ class MixerSpeaker : public Component { /// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number /// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample /// overflows. - /// @param primary_buffer (int16_t *) samples buffer for the primary stream + /// @param primary_buffer samples buffer for the primary stream /// @param primary_stream_info stream info for the primary stream - /// @param secondary_buffer (int16_t *) samples buffer for secondary stream + /// @param secondary_buffer samples buffer for secondary stream /// @param secondary_stream_info stream info for the secondary stream - /// @param output_buffer (int16_t *) buffer for the mixed samples + /// @param output_buffer buffer for the mixed samples /// @param output_stream_info stream info for the output buffer /// @param frames_to_mix number of frames in the primary and secondary buffers to mix together static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info, @@ -185,20 +200,20 @@ class MixerSpeaker : public Component { EventGroupHandle_t event_group_{nullptr}; - std::vector<SourceSpeaker *> source_speakers_; + FixedVector<SourceSpeaker *> source_speakers_; speaker::Speaker *output_speaker_{nullptr}; uint8_t output_channels_; bool queue_mode_; bool task_stack_in_psram_{false}; - bool task_created_{false}; - TaskHandle_t task_handle_{nullptr}; StaticTask_t task_stack_; StackType_t *task_stack_buffer_{nullptr}; optional<audio::AudioStreamInfo> audio_stream_info_; + + std::atomic<uint32_t> frames_in_pipeline_{0}; // Frames written to output but not yet played }; } // namespace mixer_speaker From 919afa1553a40bd9c756d871a87b55a77de7bfb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 11:47:59 -0600 Subject: [PATCH 152/251] [web_server_base] Fix RP2040 compilation when Crypto-no-arduino is present (#13887) --- esphome/components/web_server_base/__init__.py | 13 +++++++++++++ .../web_server_base/fix_rp2040_hash.py.script | 11 +++++++++++ 2 files changed, 24 insertions(+) create mode 100644 esphome/components/web_server_base/fix_rp2040_hash.py.script diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 11408ae260..7986ac964d 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -1,8 +1,11 @@ +from pathlib import Path + import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.helpers import copy_file_if_changed CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -49,5 +52,15 @@ async def to_code(config): CORE.add_platformio_option( "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] ) + # ESPAsyncWebServer uses Hash library for sha1() on RP2040 + cg.add_library("Hash", None) + # Fix Hash.h include conflict: Crypto-no-arduino (used by dsmr) + # provides a Hash.h that shadows the framework's Hash library. + # Prepend the framework Hash path so it's found first. + copy_file_if_changed( + Path(__file__).parent / "fix_rp2040_hash.py.script", + CORE.relative_build_path("fix_rp2040_hash.py"), + ) + cg.add_platformio_option("extra_scripts", ["pre:fix_rp2040_hash.py"]) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") diff --git a/esphome/components/web_server_base/fix_rp2040_hash.py.script b/esphome/components/web_server_base/fix_rp2040_hash.py.script new file mode 100644 index 0000000000..2cf24569de --- /dev/null +++ b/esphome/components/web_server_base/fix_rp2040_hash.py.script @@ -0,0 +1,11 @@ +# ESPAsyncWebServer includes <Hash.h> expecting the Arduino-Pico framework's Hash +# library (which provides sha1() functions). However, the Crypto-no-arduino library +# (used by dsmr) also provides a Hash.h that can shadow the framework version when +# PlatformIO's chain+ LDF mode auto-discovers it as a dependency. +# Prepend the framework Hash path to CXXFLAGS so it is found first. +import os + +Import("env") +framework_dir = env.PioPlatform().get_package_dir("framework-arduinopico") +hash_src = os.path.join(framework_dir, "libraries", "Hash", "src") +env.Prepend(CXXFLAGS=["-I" + hash_src]) From 04a6238c7b2decb53e89066ce0da49d6e56b34ff Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:49:58 -0500 Subject: [PATCH 153/251] [esp32] Set UV_CACHE_DIR inside data dir so Clean All clears it (#13888) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 90f6035aba..6f5011246c 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1435,6 +1435,10 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) + # Set the uv cache inside the data dir so "Clean All" clears it. + # Avoids persistent corrupted cache from mid-stream download failures. + os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache")) + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") From c658d7b57faa41def0808c11d6fbd046abd4eb6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:02:02 -0600 Subject: [PATCH 154/251] [api] Merge auth check into base read_message, eliminate APIServerConnection (#13873) --- esphome/components/api/api_connection.h | 2 +- esphome/components/api/api_pb2_service.cpp | 41 +++--- esphome/components/api/api_pb2_service.h | 5 - script/api_protobuf/api_protobuf.py | 156 ++++++++++----------- 4 files changed, 91 insertions(+), 113 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index ae7f864568..7f738a9bfd 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -28,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); -class APIConnection final : public APIServerConnection { +class APIConnection final : public APIServerConnectionBase { public: friend class APIServer; friend class ListEntitiesIterator; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 1c04eacc82..2d15deb90d 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -21,6 +21,23 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) { #endif void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { + // Check authentication/connection requirements + switch (msg_type) { + case HelloRequest::MESSAGE_TYPE: // No setup required + case DisconnectRequest::MESSAGE_TYPE: // No setup required + case PingRequest::MESSAGE_TYPE: // No setup required + break; + case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only + if (!this->check_connection_setup_()) { + return; + } + break; + default: + if (!this->check_authenticated_()) { + return; + } + break; + } switch (msg_type) { case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; @@ -623,28 +640,4 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } } -void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { - // Check authentication/connection requirements for messages - switch (msg_type) { - case HelloRequest::MESSAGE_TYPE: // No setup required - case DisconnectRequest::MESSAGE_TYPE: // No setup required - case PingRequest::MESSAGE_TYPE: // No setup required - break; // Skip all checks for these messages - case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only - if (!this->check_connection_setup_()) { - return; // Connection not setup - } - break; - default: - // All other messages require authentication (which includes connection check) - if (!this->check_authenticated_()) { - return; // Authentication failed - } - break; - } - - // Call base implementation to process the message - APIServerConnectionBase::read_message(msg_size, msg_type, msg_data); -} - } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 4dc6ce27d0..1441507406 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -228,9 +228,4 @@ class APIServerConnectionBase : public ProtoService { void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; -class APIServerConnection : public APIServerConnectionBase { - protected: - void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; -}; - } // namespace esphome::api diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 8673996a25..ece0b5692f 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2881,9 +2881,82 @@ static const char *const TAG = "api.service"; cases = list(RECEIVE_CASES.items()) cases.sort() + + serv = file.service[0] + + # Build a mapping of message input types to their authentication requirements + message_auth_map: dict[str, bool] = {} + message_conn_map: dict[str, bool] = {} + + for m in serv.method: + inp = m.input_type[1:] + needs_conn = get_opt(m, pb.needs_setup_connection, True) + needs_auth = get_opt(m, pb.needs_authentication, True) + + # Store authentication requirements for message types + message_auth_map[inp] = needs_auth + message_conn_map[inp] = needs_conn + + # Categorize messages by their authentication requirements + no_conn_ids: set[int] = set() + conn_only_ids: set[int] = set() + + for id_, (_, _, case_msg_name) in cases: + if case_msg_name in message_auth_map: + needs_auth = message_auth_map[case_msg_name] + needs_conn = message_conn_map[case_msg_name] + + if not needs_conn: + no_conn_ids.add(id_) + elif not needs_auth: + conn_only_ids.add(id_) + + # Helper to generate case statements with ifdefs + def generate_cases(ids: set[int], comment: str) -> str: + result = "" + for id_ in sorted(ids): + _, ifdef, msg_name = RECEIVE_CASES[id_] + if ifdef: + result += f"#ifdef {ifdef}\n" + result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n" + if ifdef: + result += "#endif\n" + return result + + # Generate read_message with auth check before dispatch hpp += " protected:\n" hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" + + # Auth check block before dispatch switch + out += " // Check authentication/connection requirements\n" + if no_conn_ids or conn_only_ids: + out += " switch (msg_type) {\n" + + if no_conn_ids: + out += generate_cases(no_conn_ids, "// No setup required") + out += " break;\n" + + if conn_only_ids: + out += generate_cases(conn_only_ids, "// Connection setup only") + out += " if (!this->check_connection_setup_()) {\n" + out += " return;\n" + out += " }\n" + out += " break;\n" + + out += " default:\n" + out += " if (!this->check_authenticated_()) {\n" + out += " return;\n" + out += " }\n" + out += " break;\n" + out += " }\n" + else: + out += " if (!this->check_authenticated_()) {\n" + out += " return;\n" + out += " }\n" + + # Dispatch switch out += " switch (msg_type) {\n" for i, (case, ifdef, message_name) in cases: if ifdef is not None: @@ -2902,89 +2975,6 @@ static const char *const TAG = "api.service"; cpp += out hpp += "};\n" - serv = file.service[0] - class_name = "APIServerConnection" - hpp += "\n" - hpp += f"class {class_name} : public {class_name}Base {{\n" - hpp_protected = "" - cpp += "\n" - - # Build a mapping of message input types to their authentication requirements - message_auth_map: dict[str, bool] = {} - message_conn_map: dict[str, bool] = {} - - for m in serv.method: - inp = m.input_type[1:] - needs_conn = get_opt(m, pb.needs_setup_connection, True) - needs_auth = get_opt(m, pb.needs_authentication, True) - - # Store authentication requirements for message types - message_auth_map[inp] = needs_auth - message_conn_map[inp] = needs_conn - - # Generate optimized read_message with authentication checking - # Categorize messages by their authentication requirements - no_conn_ids: set[int] = set() - conn_only_ids: set[int] = set() - - for id_, (_, _, case_msg_name) in cases: - if case_msg_name in message_auth_map: - needs_auth = message_auth_map[case_msg_name] - needs_conn = message_conn_map[case_msg_name] - - if not needs_conn: - no_conn_ids.add(id_) - elif not needs_auth: - conn_only_ids.add(id_) - - # Generate override if we have messages that skip checks - if no_conn_ids or conn_only_ids: - # Helper to generate case statements with ifdefs - def generate_cases(ids: set[int], comment: str) -> str: - result = "" - for id_ in sorted(ids): - _, ifdef, msg_name = RECEIVE_CASES[id_] - if ifdef: - result += f"#ifdef {ifdef}\n" - result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n" - if ifdef: - result += "#endif\n" - return result - - hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" - - cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" - cpp += " // Check authentication/connection requirements for messages\n" - cpp += " switch (msg_type) {\n" - - # Messages that don't need any checks - if no_conn_ids: - cpp += generate_cases(no_conn_ids, "// No setup required") - cpp += " break; // Skip all checks for these messages\n" - - # Messages that only need connection setup - if conn_only_ids: - cpp += generate_cases(conn_only_ids, "// Connection setup only") - cpp += " if (!this->check_connection_setup_()) {\n" - cpp += " return; // Connection not setup\n" - cpp += " }\n" - cpp += " break;\n" - - cpp += " default:\n" - cpp += " // All other messages require authentication (which includes connection check)\n" - cpp += " if (!this->check_authenticated_()) {\n" - cpp += " return; // Authentication failed\n" - cpp += " }\n" - cpp += " break;\n" - cpp += " }\n\n" - cpp += " // Call base implementation to process the message\n" - cpp += f" {class_name}Base::read_message(msg_size, msg_type, msg_data);\n" - cpp += "}\n" - - hpp += " protected:\n" - hpp += hpp_protected - hpp += "};\n" - hpp += """\ } // namespace esphome::api From 2383b6b8b461d14594d9cd08199d7bc3db444a8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:05:32 -0600 Subject: [PATCH 155/251] [core] Deprecate set_retry, cancel_retry, and RetryResult (#13845) --- esphome/core/component.cpp | 19 +++++++++- esphome/core/component.h | 71 ++++++++++++-------------------------- esphome/core/scheduler.cpp | 7 ++++ esphome/core/scheduler.h | 23 +++++++++--- 4 files changed, 66 insertions(+), 54 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f09a39d2bb..6d8d1c57af 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -152,7 +152,10 @@ void Component::set_retry(const std::string &name, uint32_t initial_wait_time, u void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); +#pragma GCC diagnostic pop } bool Component::cancel_retry(const std::string &name) { // NOLINT @@ -163,7 +166,10 @@ bool Component::cancel_retry(const std::string &name) { // NOLINT } bool Component::cancel_retry(const char *name) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return App.scheduler.cancel_retry(this, name); +#pragma GCC diagnostic pop } void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT @@ -203,10 +209,18 @@ bool Component::cancel_interval(uint32_t id) { return App.scheduler.cancel_inter void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); +#pragma GCC diagnostic pop } -bool Component::cancel_retry(uint32_t id) { return App.scheduler.cancel_retry(this, id); } +bool Component::cancel_retry(uint32_t id) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return App.scheduler.cancel_retry(this, id); +#pragma GCC diagnostic pop +} void Component::call_loop() { this->loop(); } void Component::call_setup() { this->setup(); } @@ -371,7 +385,10 @@ void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // } void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); +#pragma GCC diagnostic pop } bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_ready() const { diff --git a/esphome/core/component.h b/esphome/core/component.h index 97f2afe1a4..c3582e23b1 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -68,6 +68,7 @@ extern const uint8_t STATUS_LED_OK; extern const uint8_t STATUS_LED_WARNING; extern const uint8_t STATUS_LED_ERROR; +// Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; extern const uint16_t WARN_IF_BLOCKING_OVER_MS; @@ -347,68 +348,40 @@ class Component { bool cancel_interval(const char *name); // NOLINT bool cancel_interval(uint32_t id); // NOLINT - /** Set an retry function with a unique name. Empty name means no cancelling possible. - * - * This will call the retry function f on the next scheduler loop. f should return RetryResult::DONE if - * it is successful and no repeat is required. Otherwise, returning RetryResult::RETRY will call f - * again in the future. - * - * The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is - * increased by multiplying by `backoff_increase_factor` each time. If no backoff_increase_factor is - * supplied (default = 1.0), the wait time will stay constant. - * - * The retry function f needs to accept a single argument: the number of attempts remaining. On the - * final retry of f, this value will be 0. - * - * This retry function can also be cancelled by name via cancel_retry(). - * - * IMPORTANT: Do not rely on this having correct timing. This is only called from - * loop() and therefore can be significantly delayed. - * - * REMARK: It is an error to supply a negative or zero `backoff_increase_factor`, and 1.0 will be used instead. - * - * REMARK: The interval between retries is stored into a `uint32_t`, so this doesn't behave correctly - * if `initial_wait_time * (backoff_increase_factor ** (max_attempts - 2))` overflows. - * - * @param name The identifier for this retry function. - * @param initial_wait_time The time in ms before f is called again - * @param max_attempts The maximum number of executions - * @param f The function (or lambda) that should be called - * @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first - * @see cancel_retry() - */ - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") + /// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0. + // Remove before 2026.8.0 + ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", + "2026.2.0") void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT + // Remove before 2026.8.0 + ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", + "2026.2.0") void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT - /** Set a retry function with a numeric ID (zero heap allocation). - * - * @param id The numeric identifier for this retry function - * @param initial_wait_time The wait time after the first execution - * @param max_attempts The max number of attempts - * @param f The function to call - * @param backoff_increase_factor The factor to increase the retry interval by - */ + // Remove before 2026.8.0 + ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", + "2026.2.0") void set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT + // Remove before 2026.8.0 + ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", + "2026.2.0") void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, // NOLINT float backoff_increase_factor = 1.0f); // NOLINT - /** Cancel a retry function. - * - * @param name The identifier for this retry function. - * @return Whether a retry function was deleted. - */ - // Remove before 2026.7.0 - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") + // Remove before 2026.8.0 + ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(const std::string &name); // NOLINT - bool cancel_retry(const char *name); // NOLINT - bool cancel_retry(uint32_t id); // NOLINT + // Remove before 2026.8.0 + ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") + bool cancel_retry(const char *name); // NOLINT + // Remove before 2026.8.0 + ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") + bool cancel_retry(uint32_t id); // NOLINT /** Set a timeout function with a unique name. * diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 6797640f54..a5e308829a 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -252,6 +252,11 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) { return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL); } +// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation. +// Remove before 2026.8.0 along with all retry code. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + struct RetryArgs { // Ordered to minimize padding on 32-bit systems std::function<RetryResult(uint8_t)> func; @@ -364,6 +369,8 @@ bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) { return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id); } +#pragma GCC diagnostic pop // End suppression of deprecated RetryResult warnings + optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { // IMPORTANT: This method should only be called from the main thread (loop task). // It performs cleanup and accesses items_[0] without holding a lock, which is only diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 7de1023e6d..20b069f3f0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -72,18 +72,30 @@ class Scheduler { bool cancel_interval(Component *component, const char *name); bool cancel_interval(Component *component, uint32_t id); - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") + // Remove before 2026.8.0 + ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", + "2026.2.0") void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); + // Remove before 2026.8.0 + ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", + "2026.2.0") void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); - /// Set a retry with a numeric ID (zero heap allocation) + // Remove before 2026.8.0 + ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", + "2026.2.0") void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); - ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") + // Remove before 2026.8.0 + ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(Component *component, const std::string &name); + // Remove before 2026.8.0 + ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(Component *component, const char *name); + // Remove before 2026.8.0 + ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(Component *component, uint32_t id); // Calculate when the next scheduled item should run @@ -231,11 +243,14 @@ class Scheduler { uint32_t hash_or_id, uint32_t delay, std::function<void()> func, bool is_retry = false, bool skip_cancel = false); - // Common implementation for retry + // Common implementation for retry - Remove before 2026.8.0 // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor); +#pragma GCC diagnostic pop // Common implementation for cancel_retry bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); From 3b0df145b72f8df89b690ab8b3aaa7175d4d8089 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:05:59 -0600 Subject: [PATCH 156/251] [cse7766] Batch UART reads to reduce loop overhead (#13817) --- esphome/components/cse7766/cse7766.cpp | 48 +++++++++++++++++--------- esphome/components/cse7766/cse7766.h | 4 ++- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index df4872deac..45abd3ca3d 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -7,7 +7,6 @@ namespace esphome { namespace cse7766 { static const char *const TAG = "cse7766"; -static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; void CSE7766Component::loop() { const uint32_t now = App.get_loop_component_start_time(); @@ -16,25 +15,39 @@ void CSE7766Component::loop() { this->raw_data_index_ = 0; } - if (this->available() == 0) { + // Early return prevents updating last_transmission_ when no data is available. + int avail = this->available(); + if (avail <= 0) { return; } this->last_transmission_ = now; - while (this->available() != 0) { - this->read_byte(&this->raw_data_[this->raw_data_index_]); - if (!this->check_byte_()) { - this->raw_data_index_ = 0; - this->status_set_warning(); - continue; - } - if (this->raw_data_index_ == 23) { - this->parse_data_(); - this->status_clear_warning(); + // Read all available bytes in batches to reduce UART call overhead. + // At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call. + uint8_t buf[CSE7766_RAW_DATA_SIZE]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; } + avail -= to_read; - this->raw_data_index_ = (this->raw_data_index_ + 1) % 24; + for (size_t i = 0; i < to_read; i++) { + this->raw_data_[this->raw_data_index_] = buf[i]; + if (!this->check_byte_()) { + this->raw_data_index_ = 0; + this->status_set_warning(); + continue; + } + + if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) { + this->parse_data_(); + this->status_clear_warning(); + } + + this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE; + } } } @@ -53,14 +66,15 @@ bool CSE7766Component::check_byte_() { return true; } - if (index == 23) { + if (index == CSE7766_RAW_DATA_SIZE - 1) { uint8_t checksum = 0; - for (uint8_t i = 2; i < 23; i++) { + for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) { checksum += this->raw_data_[i]; } - if (checksum != this->raw_data_[23]) { - ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]); + if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) { + ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, + this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]); return false; } return true; diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index efddccd3c5..66a4e04633 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -8,6 +8,8 @@ namespace esphome { namespace cse7766 { +static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; + class CSE7766Component : public Component, public uart::UARTDevice { public: void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } @@ -33,7 +35,7 @@ class CSE7766Component : public Component, public uart::UARTDevice { this->raw_data_[start_index + 2]); } - uint8_t raw_data_[24]; + uint8_t raw_data_[CSE7766_RAW_DATA_SIZE]; uint8_t raw_data_index_{0}; uint32_t last_transmission_{0}; sensor::Sensor *voltage_sensor_{nullptr}; From c7883cb5ae6f69ca7de1f4281c5661b41f98fb95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:06:38 -0600 Subject: [PATCH 157/251] [ld2450] Batch UART reads to reduce loop overhead (#13818) --- esphome/components/ld2450/ld2450.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index ca8d918441..38ba0d7f96 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -276,8 +276,19 @@ void LD2450Component::dump_config() { } void LD2450Component::loop() { - while (this->available()) { - this->readline_(this->read()); + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[MAX_LINE_LENGTH]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + + for (size_t i = 0; i < to_read; i++) { + this->readline_(buf[i]); + } } } From 50fe8e51f9dfb610e74049fcc0db440e22301d54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:07:28 -0600 Subject: [PATCH 158/251] [ld2412] Batch UART reads to reduce loop overhead (#13819) --- esphome/components/ld2412/ld2412.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index c2f441e472..f8ceee78eb 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -310,8 +310,19 @@ void LD2412Component::restart_and_read_all_info() { } void LD2412Component::loop() { - while (this->available()) { - this->readline_(this->read()); + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[MAX_LINE_LENGTH]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + + for (size_t i = 0; i < to_read; i++) { + this->readline_(buf[i]); + } } } From c43d3889b041ee1322232b1ea089c5cb5544d738 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:07:42 -0600 Subject: [PATCH 159/251] [modbus] Use stack buffer instead of heap vector in send() (#13853) --- esphome/components/modbus/modbus.cpp | 39 ++++++++++++++++++---------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 5e9387b843..357cd48e11 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -219,39 +219,50 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address return; } - std::vector<uint8_t> data; - data.push_back(address); - data.push_back(function_code); + static constexpr size_t ADDR_SIZE = 1; + static constexpr size_t FC_SIZE = 1; + static constexpr size_t START_ADDR_SIZE = 2; + static constexpr size_t NUM_ENTITIES_SIZE = 2; + static constexpr size_t BYTE_COUNT_SIZE = 1; + static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max(); + static constexpr size_t CRC_SIZE = 2; + static constexpr size_t MAX_FRAME_SIZE = + ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE; + uint8_t data[MAX_FRAME_SIZE]; + size_t pos = 0; + + data[pos++] = address; + data[pos++] = function_code; if (this->role == ModbusRole::CLIENT) { - data.push_back(start_address >> 8); - data.push_back(start_address >> 0); + data[pos++] = start_address >> 8; + data[pos++] = start_address >> 0; if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL && function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) { - data.push_back(number_of_entities >> 8); - data.push_back(number_of_entities >> 0); + data[pos++] = number_of_entities >> 8; + data[pos++] = number_of_entities >> 0; } } if (payload != nullptr) { if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple - data.push_back(payload_len); // Byte count is required for write + data[pos++] = payload_len; // Byte count is required for write } else { payload_len = 2; // Write single register or coil } for (int i = 0; i < payload_len; i++) { - data.push_back(payload[i]); + data[pos++] = payload[i]; } } - auto crc = crc16(data.data(), data.size()); - data.push_back(crc >> 0); - data.push_back(crc >> 8); + auto crc = crc16(data, pos); + data[pos++] = crc >> 0; + data[pos++] = crc >> 8; if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(true); - this->write_array(data); + this->write_array(data, pos); this->flush(); if (this->flow_control_pin_ != nullptr) @@ -261,7 +272,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; #endif - ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size())); + ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos)); } // Helper function for lambdas From d33f23dc43dad9c7891f2789e811dec248d83e5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:07:55 -0600 Subject: [PATCH 160/251] [ld2410] Batch UART reads to reduce loop overhead (#13820) --- esphome/components/ld2410/ld2410.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 5294f7cd36..b57b1d9978 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -275,8 +275,19 @@ void LD2410Component::restart_and_read_all_info() { } void LD2410Component::loop() { - while (this->available()) { - this->readline_(this->read()); + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[MAX_LINE_LENGTH]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + + for (size_t i = 0; i < to_read; i++) { + this->readline_(buf[i]); + } } } From 8b24112be5aadb2952d7dff58cbfd05341349e29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:14:48 -0600 Subject: [PATCH 161/251] [pipsolar] Batch UART reads to reduce per-loop overhead (#13829) --- esphome/components/pipsolar/pipsolar.cpp | 64 +++++++++++++++--------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index bafd5273da..d7b37f6130 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -13,9 +13,12 @@ void Pipsolar::setup() { } void Pipsolar::empty_uart_buffer_() { - uint8_t byte; - while (this->available()) { - this->read_byte(&byte); + uint8_t buf[64]; + int avail; + while ((avail = this->available()) > 0) { + if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) { + break; + } } } @@ -94,32 +97,47 @@ void Pipsolar::loop() { } if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { - while (this->available()) { - uint8_t byte; - this->read_byte(&byte); - - // make sure data and null terminator fit in buffer - if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) { - this->read_pos_ = 0; - this->empty_uart_buffer_(); - ESP_LOGW(TAG, "response data too long, discarding."); + int avail = this->available(); + while (avail > 0) { + uint8_t buf[64]; + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { break; } - this->read_buffer_[this->read_pos_] = byte; - this->read_pos_++; + avail -= to_read; + bool done = false; + for (size_t i = 0; i < to_read; i++) { + uint8_t byte = buf[i]; - // end of answer - if (byte == 0x0D) { - this->read_buffer_[this->read_pos_] = 0; - this->empty_uart_buffer_(); - if (this->state_ == STATE_POLL) { - this->state_ = STATE_POLL_COMPLETE; + // make sure data and null terminator fit in buffer + if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) { + this->read_pos_ = 0; + this->empty_uart_buffer_(); + ESP_LOGW(TAG, "response data too long, discarding."); + done = true; + break; } - if (this->state_ == STATE_COMMAND) { - this->state_ = STATE_COMMAND_COMPLETE; + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + + // end of answer + if (byte == 0x0D) { + this->read_buffer_[this->read_pos_] = 0; + this->empty_uart_buffer_(); + if (this->state_ == STATE_POLL) { + this->state_ = STATE_POLL_COMPLETE; + } + if (this->state_ == STATE_COMMAND) { + this->state_ = STATE_COMMAND_COMPLETE; + } + done = true; + break; } } - } // available + if (done) { + break; + } + } } if (this->state_ == STATE_COMMAND) { if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { From 623f33c9f9ea480d35d258b34c2fca427ce54ae4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:15:04 -0600 Subject: [PATCH 162/251] [rd03d] Batch UART reads to reduce per-loop overhead (#13830) --- esphome/components/rd03d/rd03d.cpp | 67 +++++++++++++++++------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index 090e4dcf32..e4dbdf41cb 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -1,4 +1,5 @@ #include "rd03d.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include <cmath> @@ -80,37 +81,47 @@ void RD03DComponent::dump_config() { } void RD03DComponent::loop() { - while (this->available()) { - uint8_t byte = this->read(); - ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_); + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + for (size_t i = 0; i < to_read; i++) { + uint8_t byte = buf[i]; + ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_); - // Check if we're looking for frame header - if (this->buffer_pos_ < FRAME_HEADER_SIZE) { - if (byte == FRAME_HEADER[this->buffer_pos_]) { - this->buffer_[this->buffer_pos_++] = byte; - } else if (byte == FRAME_HEADER[0]) { - // Start over if we see a potential new header - this->buffer_[0] = byte; - this->buffer_pos_ = 1; - } else { + // Check if we're looking for frame header + if (this->buffer_pos_ < FRAME_HEADER_SIZE) { + if (byte == FRAME_HEADER[this->buffer_pos_]) { + this->buffer_[this->buffer_pos_++] = byte; + } else if (byte == FRAME_HEADER[0]) { + // Start over if we see a potential new header + this->buffer_[0] = byte; + this->buffer_pos_ = 1; + } else { + this->buffer_pos_ = 0; + } + continue; + } + + // Accumulate data bytes + this->buffer_[this->buffer_pos_++] = byte; + + // Check if we have a complete frame + if (this->buffer_pos_ == FRAME_SIZE) { + // Validate footer + if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) { + this->process_frame_(); + } else { + ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2], + this->buffer_[FRAME_SIZE - 1]); + } this->buffer_pos_ = 0; } - continue; - } - - // Accumulate data bytes - this->buffer_[this->buffer_pos_++] = byte; - - // Check if we have a complete frame - if (this->buffer_pos_ == FRAME_SIZE) { - // Validate footer - if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) { - this->process_frame_(); - } else { - ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2], - this->buffer_[FRAME_SIZE - 1]); - } - this->buffer_pos_ = 0; } } } From e7a900fbaa082c906dcb54f438a876da11763456 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:15:15 -0600 Subject: [PATCH 163/251] [rf_bridge] Batch UART reads to reduce per-loop overhead (#13831) --- esphome/components/rf_bridge/rf_bridge.cpp | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 8105767485..e33c13aafe 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -136,14 +136,21 @@ void RFBridgeComponent::loop() { this->last_bridge_byte_ = now; } - while (this->available()) { - uint8_t byte; - this->read_byte(&byte); - if (this->parse_bridge_byte_(byte)) { - ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); - this->last_bridge_byte_ = now; - } else { - this->rx_buffer_.clear(); + int avail = this->available(); + while (avail > 0) { + uint8_t buf[64]; + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + for (size_t i = 0; i < to_read; i++) { + if (this->parse_bridge_byte_(buf[i])) { + ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]); + this->last_bridge_byte_ = now; + } else { + this->rx_buffer_.clear(); + } } } } From e176cf50abb2acfb911d72c8f1c9445530542a5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:15:28 -0600 Subject: [PATCH 164/251] [dfplayer] Batch UART reads to reduce per-loop overhead (#13832) --- esphome/components/dfplayer/dfplayer.cpp | 276 ++++++++++++----------- 1 file changed, 143 insertions(+), 133 deletions(-) diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 70bd42e1a5..48c06be558 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -1,4 +1,5 @@ #include "dfplayer.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -131,140 +132,149 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { } void DFPlayer::loop() { - // Read message - while (this->available()) { - uint8_t byte; - this->read_byte(&byte); - - if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) - this->read_pos_ = 0; - - switch (this->read_pos_) { - case 0: // Start mark - if (byte != 0x7E) - continue; - break; - case 1: // Version - if (byte != 0xFF) { - ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); - this->read_pos_ = 0; - continue; - } - break; - case 2: // Buffer length - if (byte != 0x06) { - ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); - this->read_pos_ = 0; - continue; - } - break; - case 9: // End byte -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char byte_sequence[100]; - byte_sequence[0] = '\0'; - for (size_t i = 0; i < this->read_pos_ + 1; ++i) { - snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ", - this->read_buffer_[i]); - } - ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence); -#endif - if (byte != 0xEF) { - ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); - this->read_pos_ = 0; - continue; - } - // Parse valid received command - uint8_t cmd = this->read_buffer_[3]; - uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; - - ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); - - switch (cmd) { - case 0x3A: - if (argument == 1) { - ESP_LOGI(TAG, "USB loaded"); - } else if (argument == 2) { - ESP_LOGI(TAG, "TF Card loaded"); - } - break; - case 0x3B: - if (argument == 1) { - ESP_LOGI(TAG, "USB unloaded"); - } else if (argument == 2) { - ESP_LOGI(TAG, "TF Card unloaded"); - } - break; - case 0x3F: - if (argument == 1) { - ESP_LOGI(TAG, "USB available"); - } else if (argument == 2) { - ESP_LOGI(TAG, "TF Card available"); - } else if (argument == 3) { - ESP_LOGI(TAG, "USB, TF Card available"); - } - break; - case 0x40: - ESP_LOGV(TAG, "Nack"); - this->ack_set_is_playing_ = false; - this->ack_reset_is_playing_ = false; - switch (argument) { - case 0x01: - ESP_LOGE(TAG, "Module is busy or uninitialized"); - break; - case 0x02: - ESP_LOGE(TAG, "Module is in sleep mode"); - break; - case 0x03: - ESP_LOGE(TAG, "Serial receive error"); - break; - case 0x04: - ESP_LOGE(TAG, "Checksum incorrect"); - break; - case 0x05: - ESP_LOGE(TAG, "Specified track is out of current track scope"); - this->is_playing_ = false; - break; - case 0x06: - ESP_LOGE(TAG, "Specified track is not found"); - this->is_playing_ = false; - break; - case 0x07: - ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)"); - break; - case 0x08: - ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)"); - break; - case 0x09: - ESP_LOGE(TAG, "Entered into sleep mode"); - this->is_playing_ = false; - break; - } - break; - case 0x41: - ESP_LOGV(TAG, "Ack ok"); - this->is_playing_ |= this->ack_set_is_playing_; - this->is_playing_ &= !this->ack_reset_is_playing_; - this->ack_set_is_playing_ = false; - this->ack_reset_is_playing_ = false; - break; - case 0x3C: - ESP_LOGV(TAG, "Playback finished (USB drive)"); - this->is_playing_ = false; - this->on_finished_playback_callback_.call(); - case 0x3D: - ESP_LOGV(TAG, "Playback finished (SD card)"); - this->is_playing_ = false; - this->on_finished_playback_callback_.call(); - break; - default: - ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); - } - this->sent_cmd_ = 0; - this->read_pos_ = 0; - continue; + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + for (size_t bi = 0; bi < to_read; bi++) { + uint8_t byte = buf[bi]; + + if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + switch (this->read_pos_) { + case 0: // Start mark + if (byte != 0x7E) + continue; + break; + case 1: // Version + if (byte != 0xFF) { + ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 2: // Buffer length + if (byte != 0x06) { + ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 9: // End byte +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char byte_sequence[100]; + byte_sequence[0] = '\0'; + for (size_t i = 0; i < this->read_pos_ + 1; ++i) { + snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ", + this->read_buffer_[i]); + } + ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence); +#endif + if (byte != 0xEF) { + ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + // Parse valid received command + uint8_t cmd = this->read_buffer_[3]; + uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; + + ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); + + switch (cmd) { + case 0x3A: + if (argument == 1) { + ESP_LOGI(TAG, "USB loaded"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card loaded"); + } + break; + case 0x3B: + if (argument == 1) { + ESP_LOGI(TAG, "USB unloaded"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card unloaded"); + } + break; + case 0x3F: + if (argument == 1) { + ESP_LOGI(TAG, "USB available"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card available"); + } else if (argument == 3) { + ESP_LOGI(TAG, "USB, TF Card available"); + } + break; + case 0x40: + ESP_LOGV(TAG, "Nack"); + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + switch (argument) { + case 0x01: + ESP_LOGE(TAG, "Module is busy or uninitialized"); + break; + case 0x02: + ESP_LOGE(TAG, "Module is in sleep mode"); + break; + case 0x03: + ESP_LOGE(TAG, "Serial receive error"); + break; + case 0x04: + ESP_LOGE(TAG, "Checksum incorrect"); + break; + case 0x05: + ESP_LOGE(TAG, "Specified track is out of current track scope"); + this->is_playing_ = false; + break; + case 0x06: + ESP_LOGE(TAG, "Specified track is not found"); + this->is_playing_ = false; + break; + case 0x07: + ESP_LOGE(TAG, + "Insertion error (an inserting operation only can be done when a track is being played)"); + break; + case 0x08: + ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)"); + break; + case 0x09: + ESP_LOGE(TAG, "Entered into sleep mode"); + this->is_playing_ = false; + break; + } + break; + case 0x41: + ESP_LOGV(TAG, "Ack ok"); + this->is_playing_ |= this->ack_set_is_playing_; + this->is_playing_ &= !this->ack_reset_is_playing_; + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + break; + case 0x3C: + ESP_LOGV(TAG, "Playback finished (USB drive)"); + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + case 0x3D: + ESP_LOGV(TAG, "Playback finished (SD card)"); + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + break; + default: + ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); + } + this->sent_cmd_ = 0; + this->read_pos_ = 0; + continue; + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; } - this->read_buffer_[this->read_pos_] = byte; - this->read_pos_++; } } void DFPlayer::dump_config() { From a5ee4510433c247e8eebe982ff82d587a2e303f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:17:58 -0600 Subject: [PATCH 165/251] [tuya] Batch UART reads to reduce per-loop overhead (#13827) --- esphome/components/tuya/tuya.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 2812fb6ad6..9ee4c09b86 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -31,10 +31,19 @@ void Tuya::setup() { } void Tuya::loop() { - while (this->available()) { - uint8_t c; - this->read_byte(&c); - this->handle_char_(c); + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + + for (size_t i = 0; i < to_read; i++) { + this->handle_char_(buf[i]); + } } process_command_queue_(); } From 8fffe7453dd4cca340808964e8cd7ea24c8002df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:18:12 -0600 Subject: [PATCH 166/251] [seeed_mr24hpc1/mr60fda2/mr60bha2] Batch UART reads to reduce per-loop overhead (#13825) --- .../seeed_mr24hpc1/seeed_mr24hpc1.cpp | 17 ++++++++++----- .../seeed_mr60bha2/seeed_mr60bha2.cpp | 21 ++++++++++++------- .../seeed_mr60fda2/seeed_mr60fda2.cpp | 17 ++++++++++----- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 08d83f9390..3f2103b401 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -106,12 +106,19 @@ void MR24HPC1Component::update_() { // main loop void MR24HPC1Component::loop() { - uint8_t byte; + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; - // Is there data on the serial port - while (this->available()) { - this->read_byte(&byte); - this->r24_split_data_frame_(byte); // split data frame + for (size_t i = 0; i < to_read; i++) { + this->r24_split_data_frame_(buf[i]); // split data frame + } } if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) && diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index b9ce1f9151..d95e13241d 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -30,14 +30,21 @@ void MR60BHA2Component::dump_config() { // main loop void MR60BHA2Component::loop() { - uint8_t byte; + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; - // Is there data on the serial port - while (this->available()) { - this->read_byte(&byte); - this->rx_message_.push_back(byte); - if (!this->validate_message_()) { - this->rx_message_.clear(); + for (size_t i = 0; i < to_read; i++) { + this->rx_message_.push_back(buf[i]); + if (!this->validate_message_()) { + this->rx_message_.clear(); + } } } } diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index b5b5b4d05a..441ee2b5c2 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -49,12 +49,19 @@ void MR60FDA2Component::setup() { // main loop void MR60FDA2Component::loop() { - uint8_t byte; + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; - // Is there data on the serial port - while (this->available()) { - this->read_byte(&byte); - this->split_frame_(byte); // split data frame + for (size_t i = 0; i < to_read; i++) { + this->split_frame_(buf[i]); // split data frame + } } } From 4a9ff48f0246a4bd6131f19a76fce42db9af8781 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:20:50 -0600 Subject: [PATCH 167/251] [nextion] Batch UART reads to reduce loop overhead (#13823) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fd6ce0a24b..56bbc840fb 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -397,11 +397,17 @@ bool Nextion::remove_from_q_(bool report_empty) { } void Nextion::process_serial_() { - uint8_t d; + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; - while (this->available()) { - read_byte(&d); - this->command_data_ += d; + this->command_data_.append(reinterpret_cast<const char *>(buf), to_read); } } // nextion.tech/instruction-set/ From cd55eb927ddba835775dd71f2da7e2cdb7b59ea7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:21:15 -0600 Subject: [PATCH 168/251] [modbus] Batch UART reads to reduce loop overhead (#13822) --- esphome/components/modbus/modbus.cpp | 29 ++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 357cd48e11..c1f5635028 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -19,16 +19,25 @@ void Modbus::setup() { void Modbus::loop() { const uint32_t now = App.get_loop_component_start_time(); - while (this->available()) { - uint8_t byte; - this->read_byte(&byte); - if (this->parse_modbus_byte_(byte)) { - this->last_modbus_byte_ = now; - } else { - size_t at = this->rx_buffer_.size(); - if (at > 0) { - ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at); - this->rx_buffer_.clear(); + // Read all available bytes in batches to reduce UART call overhead. + int avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + + for (size_t i = 0; i < to_read; i++) { + if (this->parse_modbus_byte_(buf[i])) { + this->last_modbus_byte_ = now; + } else { + size_t at = this->rx_buffer_.size(); + if (at > 0) { + ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at); + this->rx_buffer_.clear(); + } } } } From 41a9588d811088ad72e9e6655c29256aa597b0c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:26:06 -0600 Subject: [PATCH 169/251] [i2c] Replace switch with if-else to avoid CSWTCH table in RAM (#13815) --- esphome/components/i2c/i2c_bus_arduino.cpp | 34 ++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index e728830147..edd6b81588 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -134,25 +134,23 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe for (size_t j = 0; j != read_count; j++) read_buffer[j] = wire_->read(); } - switch (status) { - case 0: - return ERROR_OK; - case 1: - // transmit buffer not large enough - ESP_LOGVV(TAG, "TX failed: buffer not large enough"); - return ERROR_UNKNOWN; - case 2: - case 3: - ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status); - return ERROR_NOT_ACKNOWLEDGED; - case 5: - ESP_LOGVV(TAG, "TX failed: timeout"); - return ERROR_UNKNOWN; - case 4: - default: - ESP_LOGVV(TAG, "TX failed: unknown error %u", status); - return ERROR_UNKNOWN; + // Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266 + if (status == 0) + return ERROR_OK; + if (status == 1) { + ESP_LOGVV(TAG, "TX failed: buffer not large enough"); + return ERROR_UNKNOWN; } + if (status == 2 || status == 3) { + ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status); + return ERROR_NOT_ACKNOWLEDGED; + } + if (status == 5) { + ESP_LOGVV(TAG, "TX failed: timeout"); + return ERROR_UNKNOWN; + } + ESP_LOGVV(TAG, "TX failed: unknown error %u", status); + return ERROR_UNKNOWN; } /// Perform I2C bus recovery, see: From e4ea016d1e6a7cb7a93f12c6cd2c1e419858c899 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:26:19 -0600 Subject: [PATCH 170/251] [ci] Block new std::to_string() usage, suggest snprintf alternatives (#13369) --- .../components/api/homeassistant_service.h | 4 +- esphome/components/esp32_ble/ble_uuid.h | 2 +- .../voice_assistant/voice_assistant.h | 2 +- script/ci-custom.py | 47 +++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 8ee23c75fe..2322d96eef 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -25,7 +25,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s private: // Helper to convert value to string - handles the case where value is already a string - template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); } + template<typename T> static std::string value_to_string(T &&val) { + return to_string(std::forward<T>(val)); // NOLINT + } // Overloads for string types - needed because std::to_string doesn't support them static std::string value_to_string(char *val) { diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 6c8ef7bfd9..503fde6945 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -48,7 +48,7 @@ class ESPBTUUID { // Remove before 2026.8.0 ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") - std::string to_string() const; + std::string to_string() const; // NOLINT const char *to_str(std::span<char, UUID_STR_LEN> output) const; protected: diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 2a5f3a55a7..0ef7ecc81a 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -83,7 +83,7 @@ struct Timer { } // Remove before 2026.8.0 ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") - std::string to_string() const { + std::string to_string() const { // NOLINT char buffer[TO_STR_BUFFER_SIZE]; return this->to_str(buffer); } diff --git a/script/ci-custom.py b/script/ci-custom.py index b5bec74fa7..8c405b04ae 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -756,6 +756,53 @@ def lint_no_sprintf(fname, match): ) +@lint_re_check( + # Match std::to_string() or unqualified to_string() calls + # The esphome namespace has "using std::to_string;" so unqualified calls resolve to std::to_string + # Use negative lookbehind for unqualified calls to avoid matching: + # - Function definitions: "const char *to_string(" or "std::string to_string(" + # - Method definitions: "Class::to_string(" + # - Method calls: ".to_string(" or "->to_string(" + # - Other identifiers: "_to_string(" + # Also explicitly match std::to_string since : is in the lookbehind + r"(?:(?<![*&.\w>:])to_string|std\s*::\s*to_string)\s*\(" + CPP_RE_EOL, + include=cpp_include, + exclude=[ + # Vendored library + "esphome/components/http_request/httplib.h", + # Deprecated helpers that return std::string + "esphome/core/helpers.cpp", + # The using declaration itself + "esphome/core/helpers.h", + # Test fixtures - not production embedded code + "tests/integration/fixtures/*", + ], +) +def lint_no_std_to_string(fname, match): + return ( + f"{highlight('std::to_string()')} (including unqualified {highlight('to_string()')}) " + f"allocates heap memory. On long-running embedded devices, repeated heap allocations " + f"fragment memory over time.\n" + f"Please use {highlight('snprintf()')} with a stack buffer instead.\n" + f"\n" + f"Buffer sizes and format specifiers (sizes include sign and null terminator):\n" + f" uint8_t: 4 chars - %u (or PRIu8)\n" + f" int8_t: 5 chars - %d (or PRId8)\n" + f" uint16_t: 6 chars - %u (or PRIu16)\n" + f" int16_t: 7 chars - %d (or PRId16)\n" + f" uint32_t: 11 chars - %" + "PRIu32\n" + " int32_t: 12 chars - %" + "PRId32\n" + " uint64_t: 21 chars - %" + "PRIu64\n" + " int64_t: 21 chars - %" + "PRId64\n" + f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n" + f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n" + f"\n" + f"For sensor values, use value_accuracy_to_buf() from helpers.h.\n" + f'Example: char buf[11]; snprintf(buf, sizeof(buf), "%" PRIu32, value);\n' + f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)" + ) + + @lint_re_check( # Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf # Also match std:: prefixed versions From 6c6da8a3cd2ab8dd41c6cd20cbed8884a30a7906 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 12:45:24 -0600 Subject: [PATCH 171/251] [api] Skip class generation for empty SOURCE_CLIENT protobuf messages (#13880) --- esphome/components/api/api_pb2.h | 91 ---------------------- esphome/components/api/api_pb2_dump.cpp | 28 ------- esphome/components/api/api_pb2_service.cpp | 16 ++-- script/api_protobuf/api_protobuf.py | 53 ++++++++++--- 4 files changed, 52 insertions(+), 136 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index cf6c65f285..15819da172 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -440,19 +440,6 @@ class PingResponse final : public ProtoMessage { protected: }; -class DeviceInfoRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 9; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "device_info_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; #ifdef USE_AREAS class AreaInfo final : public ProtoMessage { public: @@ -546,19 +533,6 @@ class DeviceInfoResponse final : public ProtoMessage { protected: }; -class ListEntitiesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 11; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class ListEntitiesDoneResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 19; @@ -572,19 +546,6 @@ class ListEntitiesDoneResponse final : public ProtoMessage { protected: }; -class SubscribeStatesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 20; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_states_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; #ifdef USE_BINARY_SENSOR class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { public: @@ -1037,19 +998,6 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -class SubscribeHomeassistantServicesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 34; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_homeassistant_services_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; @@ -1117,19 +1065,6 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_STATES -class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 38; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_home_assistant_states_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class SubscribeHomeAssistantStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 39; @@ -2160,19 +2095,6 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { protected: }; -class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 80; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class BluetoothConnectionsFreeResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 81; @@ -2279,19 +2201,6 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { protected: }; -class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 87; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class BluetoothDeviceClearCacheResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 88; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index e9db36ae21..f1e3bdcafe 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -764,10 +764,6 @@ const char *PingResponse::dump_to(DumpBuffer &out) const { out.append("PingResponse {}"); return out.c_str(); } -const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const { - out.append("DeviceInfoRequest {}"); - return out.c_str(); -} #ifdef USE_AREAS const char *AreaInfo::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "AreaInfo"); @@ -848,18 +844,10 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { #endif return out.c_str(); } -const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const { - out.append("ListEntitiesRequest {}"); - return out.c_str(); -} const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const { out.append("ListEntitiesDoneResponse {}"); return out.c_str(); } -const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeStatesRequest {}"); - return out.c_str(); -} #ifdef USE_BINARY_SENSOR const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); @@ -1191,10 +1179,6 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const { } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeHomeassistantServicesRequest {}"); - return out.c_str(); -} const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "HomeassistantServiceMap"); dump_field(out, "key", this->key); @@ -1245,10 +1229,6 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const { } #endif #ifdef USE_API_HOMEASSISTANT_STATES -const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeHomeAssistantStatesRequest {}"); - return out.c_str(); -} const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); dump_field(out, "entity_id", this->entity_id); @@ -1924,10 +1904,6 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const { dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); return out.c_str(); } -const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeBluetoothConnectionsFreeRequest {}"); - return out.c_str(); -} const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); dump_field(out, "free", this->free); @@ -1970,10 +1946,6 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const { dump_field(out, "error", this->error); return out.c_str(); } -const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const { - out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); - return out.c_str(); -} const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); dump_field(out, "address", this->address); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 2d15deb90d..f9151ae3b4 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -27,7 +27,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, case DisconnectRequest::MESSAGE_TYPE: // No setup required case PingRequest::MESSAGE_TYPE: // No setup required break; - case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only + case 9 /* DeviceInfoRequest is empty */: // Connection setup only if (!this->check_connection_setup_()) { return; } @@ -76,21 +76,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_ping_response(); break; } - case DeviceInfoRequest::MESSAGE_TYPE: { + case 9 /* DeviceInfoRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_device_info_request")); #endif this->on_device_info_request(); break; } - case ListEntitiesRequest::MESSAGE_TYPE: { + case 11 /* ListEntitiesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_list_entities_request")); #endif this->on_list_entities_request(); break; } - case SubscribeStatesRequest::MESSAGE_TYPE: { + case 20 /* SubscribeStatesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_states_request")); #endif @@ -151,7 +151,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { + case 34 /* SubscribeHomeassistantServicesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request")); #endif @@ -169,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #ifdef USE_API_HOMEASSISTANT_STATES - case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { + case 38 /* SubscribeHomeAssistantStatesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request")); #endif @@ -376,7 +376,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { + case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request")); #endif @@ -385,7 +385,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { + case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request")); #endif diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index ece0b5692f..4fbee49dae 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2277,6 +2277,12 @@ ifdefs: dict[str, str] = {} # Track messages with no fields (empty messages) for parameter elision EMPTY_MESSAGES: set[str] = set() +# Track empty SOURCE_CLIENT messages that don't need class generation +# These messages have no fields and are only received (never sent), so the +# class definition (vtable, dump_to, message_name, ESTIMATED_SIZE) is dead code +# that the compiler compiles but the linker strips away. +SKIP_CLASS_GENERATION: set[str] = set() + def get_opt( desc: descriptor.DescriptorProto, @@ -2527,7 +2533,11 @@ def build_service_message_type( case += "#endif\n" case += f"this->{func}({'msg' if not is_empty else ''});\n" case += "break;" - RECEIVE_CASES[id_] = (case, ifdef, mt.name) + if mt.name in SKIP_CLASS_GENERATION: + case_label = f"{id_} /* {mt.name} is empty */" + else: + case_label = f"{mt.name}::MESSAGE_TYPE" + RECEIVE_CASES[id_] = (case, ifdef, case_label) # Only close ifdef if we opened it if ifdef is not None: @@ -2723,6 +2733,19 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint mt = file.message_type + # Identify empty SOURCE_CLIENT messages that don't need class generation + for m in mt: + if m.options.deprecated: + continue + if not m.options.HasExtension(pb.id): + continue + source = message_source_map.get(m.name) + if source != SOURCE_CLIENT: + continue + has_fields = any(not field.options.deprecated for field in m.field) + if not has_fields: + SKIP_CLASS_GENERATION.add(m.name) + # Collect messages by base class base_class_groups = collect_messages_by_base_class(mt) @@ -2755,6 +2778,10 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint if m.name not in used_messages and not m.options.HasExtension(pb.id): continue + # Skip class generation for empty SOURCE_CLIENT messages + if m.name in SKIP_CLASS_GENERATION: + continue + s, c, dc = build_message_type(m, base_class_fields, message_source_map) msg_ifdef = message_ifdef_map.get(m.name) @@ -2901,10 +2928,18 @@ static const char *const TAG = "api.service"; no_conn_ids: set[int] = set() conn_only_ids: set[int] = set() - for id_, (_, _, case_msg_name) in cases: - if case_msg_name in message_auth_map: - needs_auth = message_auth_map[case_msg_name] - needs_conn = message_conn_map[case_msg_name] + # Build a reverse lookup from message id to message name for auth lookups + id_to_msg_name: dict[int, str] = {} + for mt in file.message_type: + id_ = get_opt(mt, pb.id) + if id_ is not None and not mt.options.deprecated: + id_to_msg_name[id_] = mt.name + + for id_, (_, _, case_label) in cases: + msg_name = id_to_msg_name.get(id_, "") + if msg_name in message_auth_map: + needs_auth = message_auth_map[msg_name] + needs_conn = message_conn_map[msg_name] if not needs_conn: no_conn_ids.add(id_) @@ -2915,10 +2950,10 @@ static const char *const TAG = "api.service"; def generate_cases(ids: set[int], comment: str) -> str: result = "" for id_ in sorted(ids): - _, ifdef, msg_name = RECEIVE_CASES[id_] + _, ifdef, case_label = RECEIVE_CASES[id_] if ifdef: result += f"#ifdef {ifdef}\n" - result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n" + result += f" case {case_label}: {comment}\n" if ifdef: result += "#endif\n" return result @@ -2958,11 +2993,11 @@ static const char *const TAG = "api.service"; # Dispatch switch out += " switch (msg_type) {\n" - for i, (case, ifdef, message_name) in cases: + for i, (case, ifdef, case_label) in cases: if ifdef is not None: out += f"#ifdef {ifdef}\n" - c = f" case {message_name}::MESSAGE_TYPE: {{\n" + c = f" case {case_label}: {{\n" c += indent(case, " ") + "\n" c += " }" out += c + "\n" From e0712cc53b464ebf3af3c4cb811eabc11173524e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 13:16:22 -0600 Subject: [PATCH 172/251] [scheduler] Make core timer ID collisions impossible with type-safe internal IDs (#13882) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/binary_sensor/automation.cpp | 34 +++-- esphome/components/binary_sensor/filter.cpp | 39 ++++-- esphome/components/sensor/filter.cpp | 11 +- esphome/core/base_automation.h | 10 +- esphome/core/component.cpp | 16 ++- esphome/core/component.h | 14 ++ esphome/core/scheduler.cpp | 8 +- esphome/core/scheduler.h | 37 +++++- .../scheduler_internal_id_no_collision.yaml | 109 +++++++++++++++ ...test_scheduler_internal_id_no_collision.py | 124 ++++++++++++++++++ 10 files changed, 359 insertions(+), 43 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_internal_id_no_collision.yaml create mode 100644 tests/integration/test_scheduler_internal_id_no_collision.py diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index dfe911a2f8..faebe7e88f 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -5,6 +5,14 @@ namespace esphome::binary_sensor { static const char *const TAG = "binary_sensor.automation"; +// MultiClickTrigger timeout IDs. +// MultiClickTrigger is its own Component instance, so the scheduler scopes +// IDs by component pointer — no risk of collisions between instances. +constexpr uint32_t MULTICLICK_TRIGGER_ID = 0; +constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1; +constexpr uint32_t MULTICLICK_IS_VALID_ID = 2; +constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3; + void MultiClickTrigger::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { @@ -27,7 +35,7 @@ void MultiClickTrigger::on_state_(bool state) { evt.min_length, evt.max_length); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { - this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); + this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); } else { this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); @@ -57,13 +65,13 @@ void MultiClickTrigger::on_state_(bool state) { this->schedule_is_not_valid_(evt.max_length); } else if (*this->at_index_ + 1 != this->timing_.size()) { ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT - this->cancel_timeout("is_not_valid"); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->schedule_is_valid_(evt.min_length); } else { ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->is_valid_ = false; - this->cancel_timeout("is_not_valid"); - this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); + this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); } *this->at_index_ = *this->at_index_ + 1; @@ -71,14 +79,14 @@ void MultiClickTrigger::on_state_(bool state) { void MultiClickTrigger::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; - this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { + this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() { ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again."); this->is_in_cooldown_ = false; }); this->at_index_.reset(); - this->cancel_timeout("trigger"); - this->cancel_timeout("is_valid"); - this->cancel_timeout("is_not_valid"); + this->cancel_timeout(MULTICLICK_TRIGGER_ID); + this->cancel_timeout(MULTICLICK_IS_VALID_ID); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); } void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { @@ -86,13 +94,13 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { return; } this->is_valid_ = false; - this->set_timeout("is_valid", min_length, [this]() { + this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() { ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = true; }); } void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { - this->set_timeout("is_not_valid", max_length, [this]() { + this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); @@ -106,9 +114,9 @@ void MultiClickTrigger::cancel() { void MultiClickTrigger::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); - this->cancel_timeout("trigger"); - this->cancel_timeout("is_valid"); - this->cancel_timeout("is_not_valid"); + this->cancel_timeout(MULTICLICK_TRIGGER_ID); + this->cancel_timeout(MULTICLICK_IS_VALID_ID); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->trigger(); } diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 9c7238f6d7..d69671c5bf 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -6,6 +6,14 @@ namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; +// Timeout IDs for filter classes. +// Each filter is its own Component instance, so the scheduler scopes +// IDs by component pointer — no risk of collisions between instances. +constexpr uint32_t FILTER_TIMEOUT_ID = 0; +// AutorepeatFilter needs two distinct IDs (both timeouts on the same component) +constexpr uint32_t AUTOREPEAT_TIMING_ID = 0; +constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1; + void Filter::output(bool value) { if (this->next_ == nullptr) { this->parent_->send_state_internal(value); @@ -23,16 +31,16 @@ void Filter::input(bool value) { } void TimeoutFilter::input(bool value) { - this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); // we do not de-dup here otherwise changes from invalid to valid state will not be output this->output(value); } optional<bool> DelayedOnOffFilter::new_value(bool value) { if (value) { - this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); }); } else { - this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); }); } return {}; } @@ -41,10 +49,10 @@ float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HA optional<bool> DelayedOnFilter::new_value(bool value) { if (value) { - this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); }); return {}; } else { - this->cancel_timeout("ON"); + this->cancel_timeout(FILTER_TIMEOUT_ID); return false; } } @@ -53,10 +61,10 @@ float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDW optional<bool> DelayedOffFilter::new_value(bool value) { if (!value) { - this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); }); return {}; } else { - this->cancel_timeout("OFF"); + this->cancel_timeout(FILTER_TIMEOUT_ID); return true; } } @@ -76,8 +84,8 @@ optional<bool> AutorepeatFilter::new_value(bool value) { this->next_timing_(); return true; } else { - this->cancel_timeout("TIMING"); - this->cancel_timeout("ON_OFF"); + this->cancel_timeout(AUTOREPEAT_TIMING_ID); + this->cancel_timeout(AUTOREPEAT_ON_OFF_ID); this->active_timing_ = 0; return false; } @@ -88,8 +96,10 @@ void AutorepeatFilter::next_timing_() { // 1st time: starts waiting the first delay // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on // last time: no delay to start but have to bump the index to reflect the last - if (this->active_timing_ < this->timings_.size()) - this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); + if (this->active_timing_ < this->timings_.size()) { + this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay, + [this]() { this->next_timing_(); }); + } if (this->active_timing_ <= this->timings_.size()) { this->active_timing_++; @@ -104,7 +114,8 @@ void AutorepeatFilter::next_timing_() { void AutorepeatFilter::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; this->output(val); // This is at least the second one so not initial - this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); + this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off, + [this, val]() { this->next_value_(!val); }); } float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -115,7 +126,7 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); } optional<bool> SettleFilter::new_value(bool value) { if (!this->steady_) { - this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() { this->steady_ = true; this->output(value); }); @@ -123,7 +134,7 @@ optional<bool> SettleFilter::new_value(bool value) { } else { this->steady_ = false; this->output(value); - this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); return value; } } diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 375505a557..ea0e2f0d7c 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -9,6 +9,11 @@ namespace esphome::sensor { static const char *const TAG = "sensor.filter"; +// Filter scheduler IDs. +// Each filter is its own Component instance, so the scheduler scopes +// IDs by component pointer — no risk of collisions between instances. +constexpr uint32_t FILTER_ID = 0; + // Filter void Filter::input(float value) { ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value); @@ -191,7 +196,7 @@ optional<float> ThrottleAverageFilter::new_value(float value) { return {}; } void ThrottleAverageFilter::setup() { - this->set_interval("throttle_average", this->time_period_, [this]() { + this->set_interval(FILTER_ID, this->time_period_, [this]() { ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_); if (this->n_ == 0) { if (this->have_nan_) @@ -383,7 +388,7 @@ optional<float> TimeoutFilterConfigured::new_value(float value) { // DebounceFilter optional<float> DebounceFilter::new_value(float value) { - this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); }); + this->set_timeout(FILTER_ID, this->time_period_, [this, value]() { this->output(value); }); return {}; } @@ -406,7 +411,7 @@ optional<float> HeartbeatFilter::new_value(float value) { } void HeartbeatFilter::setup() { - this->set_interval("heartbeat", this->time_period_, [this]() { + this->set_interval(FILTER_ID, this->time_period_, [this]() { ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), this->last_input_); if (!this->has_value_) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 19d0ccf972..67e1755cc9 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -191,15 +191,17 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( - this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING, "delay", 0, this->delay_.value(), + this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, + static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(), [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { // For delays with arguments, use std::bind to preserve argument values // Arguments must be copied because original references may be invalid after delay auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...); - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING, - "delay", 0, this->delay_.value(x...), std::move(f), + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, + nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), + this->delay_.value(x...), std::move(f), /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } } @@ -208,7 +210,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon void play(const Ts &...x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout("delay"); } + void stop() override { this->cancel_timeout(InternalSchedulerID::DELAY_ACTION); } }; template<typename... Ts> class LambdaAction : public Action<Ts...> { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 6d8d1c57af..90aa36f4db 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -201,12 +201,24 @@ void Component::set_timeout(uint32_t id, uint32_t timeout, std::function<void()> bool Component::cancel_timeout(uint32_t id) { return App.scheduler.cancel_timeout(this, id); } +void Component::set_timeout(InternalSchedulerID id, uint32_t timeout, std::function<void()> &&f) { // NOLINT + App.scheduler.set_timeout(this, id, timeout, std::move(f)); +} + +bool Component::cancel_timeout(InternalSchedulerID id) { return App.scheduler.cancel_timeout(this, id); } + void Component::set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f) { // NOLINT App.scheduler.set_interval(this, id, interval, std::move(f)); } bool Component::cancel_interval(uint32_t id) { return App.scheduler.cancel_interval(this, id); } +void Component::set_interval(InternalSchedulerID id, uint32_t interval, std::function<void()> &&f) { // NOLINT + App.scheduler.set_interval(this, id, interval, std::move(f)); +} + +bool Component::cancel_interval(InternalSchedulerID id) { return App.scheduler.cancel_interval(this, id); } + void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT #pragma GCC diagnostic push @@ -533,12 +545,12 @@ void PollingComponent::call_setup() { void PollingComponent::start_poller() { // Register interval. - this->set_interval("update", this->get_update_interval(), [this]() { this->update(); }); + this->set_interval(InternalSchedulerID::POLLING_UPDATE, this->get_update_interval(), [this]() { this->update(); }); } void PollingComponent::stop_poller() { // Clear the interval to suspend component - this->cancel_interval("update"); + this->cancel_interval(InternalSchedulerID::POLLING_UPDATE); } uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } diff --git a/esphome/core/component.h b/esphome/core/component.h index c3582e23b1..9ab77cc2f9 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -49,6 +49,14 @@ extern const float LATE; static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; +/// Type-safe scheduler IDs for core base classes. +/// Uses a separate NameType (NUMERIC_ID_INTERNAL) so IDs can never collide +/// with component-level NUMERIC_ID values, even if the uint32_t values overlap. +enum class InternalSchedulerID : uint32_t { + POLLING_UPDATE = 0, // PollingComponent interval + DELAY_ACTION = 1, // DelayAction timeout +}; + // Forward declaration class PollingComponent; @@ -335,6 +343,8 @@ class Component { */ void set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f); // NOLINT + void set_interval(InternalSchedulerID id, uint32_t interval, std::function<void()> &&f); // NOLINT + void set_interval(uint32_t interval, std::function<void()> &&f); // NOLINT /** Cancel an interval function. @@ -347,6 +357,7 @@ class Component { bool cancel_interval(const std::string &name); // NOLINT bool cancel_interval(const char *name); // NOLINT bool cancel_interval(uint32_t id); // NOLINT + bool cancel_interval(InternalSchedulerID id); // NOLINT /// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0. // Remove before 2026.8.0 @@ -425,6 +436,8 @@ class Component { */ void set_timeout(uint32_t id, uint32_t timeout, std::function<void()> &&f); // NOLINT + void set_timeout(InternalSchedulerID id, uint32_t timeout, std::function<void()> &&f); // NOLINT + void set_timeout(uint32_t timeout, std::function<void()> &&f); // NOLINT /** Cancel a timeout function. @@ -437,6 +450,7 @@ class Component { bool cancel_timeout(const std::string &name); // NOLINT bool cancel_timeout(const char *name); // NOLINT bool cancel_timeout(uint32_t id); // NOLINT + bool cancel_timeout(InternalSchedulerID id); // NOLINT /** Defer a callback to the next loop() call. * diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index a5e308829a..97ac28b623 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -53,9 +53,12 @@ struct SchedulerNameLog { } else if (name_type == NameType::HASHED_STRING) { ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("hash:0x%08" PRIX32), hash_or_id); return buffer; - } else { // NUMERIC_ID + } else if (name_type == NameType::NUMERIC_ID) { ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id); return buffer; + } else { // NUMERIC_ID_INTERNAL + ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("iid:%" PRIu32), hash_or_id); + return buffer; } } }; @@ -137,6 +140,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type case NameType::NUMERIC_ID: item->set_numeric_id(hash_or_id); break; + case NameType::NUMERIC_ID_INTERNAL: + item->set_internal_id(hash_or_id); + break; } item->type = type; item->callback = std::move(func); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 20b069f3f0..ede729d164 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -46,11 +46,20 @@ class Scheduler { void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func); /// Set a timeout with a numeric ID (zero heap allocation) void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> func); + /// Set a timeout with an internal scheduler ID (separate namespace from component NUMERIC_ID) + void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function<void()> func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID_INTERNAL, nullptr, + static_cast<uint32_t>(id), timeout, std::move(func)); + } ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(Component *component, const std::string &name); bool cancel_timeout(Component *component, const char *name); bool cancel_timeout(Component *component, uint32_t id); + bool cancel_timeout(Component *component, InternalSchedulerID id) { + return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id), + SchedulerItem::TIMEOUT); + } ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> func); @@ -66,11 +75,20 @@ class Scheduler { void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> func); /// Set an interval with a numeric ID (zero heap allocation) void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> func); + /// Set an interval with an internal scheduler ID (separate namespace from component NUMERIC_ID) + void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function<void()> func) { + this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID_INTERNAL, nullptr, + static_cast<uint32_t>(id), interval, std::move(func)); + } ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(Component *component, const std::string &name); bool cancel_interval(Component *component, const char *name); bool cancel_interval(Component *component, uint32_t id); + bool cancel_interval(Component *component, InternalSchedulerID id) { + return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id), + SchedulerItem::INTERVAL); + } // Remove before 2026.8.0 ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", @@ -112,11 +130,12 @@ class Scheduler { void process_to_add(); // Name storage type discriminator for SchedulerItem - // Used to distinguish between static strings, hashed strings, and numeric IDs + // Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs enum class NameType : uint8_t { - STATIC_STRING = 0, // const char* pointer to static/flash storage - HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string - NUMERIC_ID = 2 // uint32_t numeric identifier + STATIC_STRING = 0, // const char* pointer to static/flash storage + HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string + NUMERIC_ID = 2, // uint32_t numeric identifier (component-level) + NUMERIC_ID_INTERNAL = 3 // uint32_t numeric identifier (core/internal, separate namespace) }; protected: @@ -147,7 +166,7 @@ class Scheduler { // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; - NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID) + NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum) bool is_retry : 1; // True if this is a retry timeout // 4 bits padding #else @@ -155,7 +174,7 @@ class Scheduler { // Bit-packed fields (5 bits used, 3 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; bool remove : 1; - NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID) + NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum) bool is_retry : 1; // True if this is a retry timeout // 3 bits padding #endif @@ -218,6 +237,12 @@ class Scheduler { name_type_ = NameType::NUMERIC_ID; } + // Helper to set an internal numeric ID (separate namespace from NUMERIC_ID) + void set_internal_id(uint32_t id) { + name_.hash_or_id = id; + name_type_ = NameType::NUMERIC_ID_INTERNAL; + } + static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b); // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. diff --git a/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml b/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml new file mode 100644 index 0000000000..46dbb8e728 --- /dev/null +++ b/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml @@ -0,0 +1,109 @@ +esphome: + name: scheduler-internal-id-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler internal ID collision tests" + +host: +api: +logger: + level: VERBOSE + +globals: + - id: tests_done + type: bool + initial_value: 'false' + +script: + - id: test_internal_id_no_collision + then: + - logger.log: "Testing NUMERIC_ID_INTERNAL vs NUMERIC_ID isolation" + - lambda: |- + // All tests use the same component and the same uint32_t value (0). + // NUMERIC_ID_INTERNAL and NUMERIC_ID are separate NameType values, + // so the scheduler must treat them as independent timers. + auto *comp = id(test_sensor); + + // ---- Test 1: Both timeout types fire independently ---- + // Set an internal timeout with ID 0 + App.scheduler.set_timeout(comp, InternalSchedulerID{0}, 50, []() { + ESP_LOGI("test", "Internal timeout 0 fired"); + }); + // Set a component numeric timeout with the same ID 0 + App.scheduler.set_timeout(comp, 0U, 50, []() { + ESP_LOGI("test", "Numeric timeout 0 fired"); + }); + + // ---- Test 2: Cancelling numeric ID does NOT cancel internal ID ---- + // Set an internal timeout with ID 1 + App.scheduler.set_timeout(comp, InternalSchedulerID{1}, 100, []() { + ESP_LOGI("test", "Internal timeout 1 survived cancel"); + }); + // Set a numeric timeout with the same ID 1 + App.scheduler.set_timeout(comp, 1U, 100, []() { + ESP_LOGE("test", "ERROR: Numeric timeout 1 should have been cancelled"); + }); + // Cancel only the numeric one + App.scheduler.cancel_timeout(comp, 1U); + + // ---- Test 3: Cancelling internal ID does NOT cancel numeric ID ---- + // Set a numeric timeout with ID 2 + App.scheduler.set_timeout(comp, 2U, 150, []() { + ESP_LOGI("test", "Numeric timeout 2 survived cancel"); + }); + // Set an internal timeout with the same ID 2 + App.scheduler.set_timeout(comp, InternalSchedulerID{2}, 150, []() { + ESP_LOGE("test", "ERROR: Internal timeout 2 should have been cancelled"); + }); + // Cancel only the internal one + App.scheduler.cancel_timeout(comp, InternalSchedulerID{2}); + + // ---- Test 4: Both interval types fire independently ---- + static int internal_interval_count = 0; + static int numeric_interval_count = 0; + App.scheduler.set_interval(comp, InternalSchedulerID{3}, 100, []() { + internal_interval_count++; + if (internal_interval_count == 2) { + ESP_LOGI("test", "Internal interval 3 fired twice"); + App.scheduler.cancel_interval(id(test_sensor), InternalSchedulerID{3}); + } + }); + App.scheduler.set_interval(comp, 3U, 100, []() { + numeric_interval_count++; + if (numeric_interval_count == 2) { + ESP_LOGI("test", "Numeric interval 3 fired twice"); + App.scheduler.cancel_interval(id(test_sensor), 3U); + } + }); + + // ---- Test 5: String name does NOT collide with internal ID ---- + // Use string name and internal ID 10 on same component + App.scheduler.set_timeout(comp, "collision_test", 200, []() { + ESP_LOGI("test", "String timeout collision_test fired"); + }); + App.scheduler.set_timeout(comp, InternalSchedulerID{10}, 200, []() { + ESP_LOGI("test", "Internal timeout 10 fired"); + }); + + // Log completion after all timers should have fired + App.scheduler.set_timeout(comp, 9999U, 1500, []() { + ESP_LOGI("test", "All collision tests complete"); + }); + +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 1.0; + update_interval: never + +interval: + - interval: 0.1s + then: + - if: + condition: + lambda: 'return id(tests_done) == false;' + then: + - lambda: 'id(tests_done) = true;' + - script.execute: test_internal_id_no_collision diff --git a/tests/integration/test_scheduler_internal_id_no_collision.py b/tests/integration/test_scheduler_internal_id_no_collision.py new file mode 100644 index 0000000000..d30e725e00 --- /dev/null +++ b/tests/integration/test_scheduler_internal_id_no_collision.py @@ -0,0 +1,124 @@ +"""Test that NUMERIC_ID_INTERNAL and NUMERIC_ID cannot collide. + +Verifies that InternalSchedulerID (used by core base classes like +PollingComponent and DelayAction) and uint32_t numeric IDs (used by +components) are in completely separate matching namespaces, even when +the underlying uint32_t values are identical and on the same component. +""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_internal_id_no_collision( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that internal and numeric IDs with same value don't collide.""" + # Test 1: Both types fire independently with same ID + internal_timeout_0_fired = asyncio.Event() + numeric_timeout_0_fired = asyncio.Event() + + # Test 2: Cancelling numeric doesn't cancel internal + internal_timeout_1_survived = asyncio.Event() + numeric_timeout_1_error = asyncio.Event() + + # Test 3: Cancelling internal doesn't cancel numeric + numeric_timeout_2_survived = asyncio.Event() + internal_timeout_2_error = asyncio.Event() + + # Test 4: Both interval types fire independently + internal_interval_3_done = asyncio.Event() + numeric_interval_3_done = asyncio.Event() + + # Test 5: String name doesn't collide with internal ID + string_timeout_fired = asyncio.Event() + internal_timeout_10_fired = asyncio.Event() + + # Completion + all_tests_complete = asyncio.Event() + + def on_log_line(line: str) -> None: + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + if "Internal timeout 0 fired" in clean_line: + internal_timeout_0_fired.set() + elif "Numeric timeout 0 fired" in clean_line: + numeric_timeout_0_fired.set() + elif "Internal timeout 1 survived cancel" in clean_line: + internal_timeout_1_survived.set() + elif "ERROR: Numeric timeout 1 should have been cancelled" in clean_line: + numeric_timeout_1_error.set() + elif "Numeric timeout 2 survived cancel" in clean_line: + numeric_timeout_2_survived.set() + elif "ERROR: Internal timeout 2 should have been cancelled" in clean_line: + internal_timeout_2_error.set() + elif "Internal interval 3 fired twice" in clean_line: + internal_interval_3_done.set() + elif "Numeric interval 3 fired twice" in clean_line: + numeric_interval_3_done.set() + elif "String timeout collision_test fired" in clean_line: + string_timeout_fired.set() + elif "Internal timeout 10 fired" in clean_line: + internal_timeout_10_fired.set() + elif "All collision tests complete" in clean_line: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-internal-id-test" + + try: + await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail("Not all collision tests completed within 5 seconds") + + # Test 1: Both timeout types with same ID 0 must fire + assert internal_timeout_0_fired.is_set(), ( + "Internal timeout with ID 0 should have fired" + ) + assert numeric_timeout_0_fired.is_set(), ( + "Numeric timeout with ID 0 should have fired" + ) + + # Test 2: Cancelling numeric ID must NOT cancel internal ID + assert internal_timeout_1_survived.is_set(), ( + "Internal timeout 1 should survive cancellation of numeric timeout 1" + ) + assert not numeric_timeout_1_error.is_set(), ( + "Numeric timeout 1 should have been cancelled" + ) + + # Test 3: Cancelling internal ID must NOT cancel numeric ID + assert numeric_timeout_2_survived.is_set(), ( + "Numeric timeout 2 should survive cancellation of internal timeout 2" + ) + assert not internal_timeout_2_error.is_set(), ( + "Internal timeout 2 should have been cancelled" + ) + + # Test 4: Both interval types with same ID must fire independently + assert internal_interval_3_done.is_set(), ( + "Internal interval 3 should have fired at least twice" + ) + assert numeric_interval_3_done.is_set(), ( + "Numeric interval 3 should have fired at least twice" + ) + + # Test 5: String name and internal ID don't collide + assert string_timeout_fired.is_set(), ( + "String timeout 'collision_test' should have fired" + ) + assert internal_timeout_10_fired.is_set(), ( + "Internal timeout 10 should have fired alongside string timeout" + ) From 00256e3ca0bf29f7ab3c7226b9a92aa292848394 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 10 Feb 2026 06:35:41 +1100 Subject: [PATCH 173/251] [mipi_rgb] Allow use on P4 (#13740) --- esphome/components/mipi_rgb/display.py | 4 +- esphome/components/mipi_rgb/mipi_rgb.cpp | 4 +- esphome/components/mipi_rgb/mipi_rgb.h | 6 +- tests/components/mipi_rgb/common.yaml | 52 +++++++++++++++++ .../mipi_rgb/test.esp32-p4-idf.yaml | 6 ++ .../mipi_rgb/test.esp32-s3-idf.yaml | 56 +------------------ .../common/spi/esp32-p4-idf.yaml | 12 ++++ 7 files changed, 78 insertions(+), 62 deletions(-) create mode 100644 tests/components/mipi_rgb/common.yaml create mode 100644 tests/components/mipi_rgb/test.esp32-p4-idf.yaml create mode 100644 tests/test_build_components/common/spi/esp32-p4-idf.yaml diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 084fe6de14..24988cfcf8 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -11,7 +11,7 @@ from esphome.components.const import ( CONF_DRAW_ROUNDING, ) from esphome.components.display import CONF_SHOW_TEST_CARD -from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant +from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant from esphome.components.mipi import ( COLOR_ORDERS, CONF_DE_PIN, @@ -225,7 +225,7 @@ def _config_schema(config): return cv.All( schema, cv.only_on_esp32, - only_on_variant(supported=[VARIANT_ESP32S3]), + only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]), )(config) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index d0e716bd24..7ff6868c15 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "mipi_rgb.h" #include "esphome/core/gpio.h" #include "esphome/core/hal.h" @@ -401,4 +401,4 @@ void MipiRgb::dump_config() { } // namespace mipi_rgb } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S3 +#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index 173e23752d..76b48bb249 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "esphome/core/gpio.h" #include "esphome/components/display/display.h" #include "esp_lcd_panel_ops.h" @@ -28,7 +28,7 @@ class MipiRgb : public display::Display { void setup() override; void loop() override; void update() override; - void fill(Color color); + void fill(Color color) override; void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, @@ -115,7 +115,7 @@ class MipiRgbSpi : public MipiRgb, void write_command_(uint8_t value); void write_data_(uint8_t value); void write_init_sequence_(); - void dump_config(); + void dump_config() override; GPIOPin *dc_pin_{nullptr}; std::vector<uint8_t> init_sequence_; diff --git a/tests/components/mipi_rgb/common.yaml b/tests/components/mipi_rgb/common.yaml new file mode 100644 index 0000000000..c137bc6af3 --- /dev/null +++ b/tests/components/mipi_rgb/common.yaml @@ -0,0 +1,52 @@ +display: + - platform: mipi_rgb + spi_id: spi_bus + model: ZX2D10GE01R-V4848 + update_interval: 1s + color_order: BGR + draw_rounding: 2 + pixel_mode: 18bit + invert_colors: false + use_axis_flips: true + pclk_frequency: 15000000.0 + pclk_inverted: true + byte_order: big_endian + hsync_pulse_width: 10 + hsync_back_porch: 10 + hsync_front_porch: 10 + vsync_pulse_width: 2 + vsync_back_porch: 12 + vsync_front_porch: 14 + data_pins: + red: + - number: 10 + - number: 16 + - number: 9 + - number: 15 + - number: 46 + green: + - number: 8 + - number: 13 + - number: 18 + - number: 12 + - number: 11 + - number: 17 + blue: + - number: 47 + - number: 1 + - number: 0 + - number: 42 + - number: 14 + de_pin: + number: 39 + pclk_pin: + number: 45 + hsync_pin: + number: 38 + vsync_pin: + number: 48 + data_rate: 1000000.0 + spi_mode: MODE0 + cs_pin: + number: 21 + show_test_card: true diff --git a/tests/components/mipi_rgb/test.esp32-p4-idf.yaml b/tests/components/mipi_rgb/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..62427492eb --- /dev/null +++ b/tests/components/mipi_rgb/test.esp32-p4-idf.yaml @@ -0,0 +1,6 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-p4-idf.yaml + +psram: + +<<: !include common.yaml diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml index 642292f7c4..399c25c1d0 100644 --- a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -4,58 +4,4 @@ packages: psram: mode: octal -display: - - platform: mipi_rgb - spi_id: spi_bus - model: ZX2D10GE01R-V4848 - update_interval: 1s - color_order: BGR - draw_rounding: 2 - pixel_mode: 18bit - invert_colors: false - use_axis_flips: true - pclk_frequency: 15000000.0 - pclk_inverted: true - byte_order: big_endian - hsync_pulse_width: 10 - hsync_back_porch: 10 - hsync_front_porch: 10 - vsync_pulse_width: 2 - vsync_back_porch: 12 - vsync_front_porch: 14 - data_pins: - red: - - number: 10 - - number: 16 - - number: 9 - - number: 15 - - number: 46 - ignore_strapping_warning: true - green: - - number: 8 - - number: 13 - - number: 18 - - number: 12 - - number: 11 - - number: 17 - blue: - - number: 47 - - number: 1 - - number: 0 - ignore_strapping_warning: true - - number: 42 - - number: 14 - de_pin: - number: 39 - pclk_pin: - number: 45 - ignore_strapping_warning: true - hsync_pin: - number: 38 - vsync_pin: - number: 48 - data_rate: 1000000.0 - spi_mode: MODE0 - cs_pin: - number: 21 - show_test_card: true +<<: !include common.yaml diff --git a/tests/test_build_components/common/spi/esp32-p4-idf.yaml b/tests/test_build_components/common/spi/esp32-p4-idf.yaml new file mode 100644 index 0000000000..5f232a7e94 --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-p4-idf.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP32-P4 IDF tests + +substitutions: + clk_pin: GPIO36 + mosi_pin: GPIO32 + miso_pin: GPIO33 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} From b6fdd299537529f128b570594b3a3584cf7cc8df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 14:42:40 -0600 Subject: [PATCH 174/251] [voice_assistant] Replace timer unordered_map with vector to eliminate per-tick heap allocation (#13857) --- .../components/voice_assistant/__init__.py | 7 ++- .../voice_assistant/voice_assistant.cpp | 46 ++++++++++--------- .../voice_assistant/voice_assistant.h | 9 ++-- .../voice_assistant/common-idf.yaml | 21 +++++++++ tests/components/voice_assistant/common.yaml | 21 +++++++++ 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index d28c786dd8..8b7dcb4f21 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -371,7 +371,12 @@ async def to_code(config): if on_timer_tick := config.get(CONF_ON_TIMER_TICK): await automation.build_automation( var.get_timer_tick_trigger(), - [(cg.std_vector.template(Timer), "timers")], + [ + ( + cg.std_vector.template(Timer).operator("const").operator("ref"), + "timers", + ) + ], on_timer_tick, ) has_timers = True diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7f5fbe62e1..6267d97480 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -859,35 +859,43 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) { } void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) { - Timer timer = { - .id = msg.timer_id, - .name = msg.name, - .total_seconds = msg.total_seconds, - .seconds_left = msg.seconds_left, - .is_active = msg.is_active, - }; - this->timers_[timer.id] = timer; + // Find existing timer or add a new one + auto it = this->timers_.begin(); + for (; it != this->timers_.end(); ++it) { + if (it->id == msg.timer_id) + break; + } + if (it == this->timers_.end()) { + this->timers_.push_back({}); + it = this->timers_.end() - 1; + } + it->id = msg.timer_id; + it->name = msg.name; + it->total_seconds = msg.total_seconds; + it->seconds_left = msg.seconds_left; + it->is_active = msg.is_active; + char timer_buf[Timer::TO_STR_BUFFER_SIZE]; ESP_LOGD(TAG, "Timer Event\n" " Type: %" PRId32 "\n" " %s", - msg.event_type, timer.to_str(timer_buf)); + msg.event_type, it->to_str(timer_buf)); switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_TIMER_STARTED: - this->timer_started_trigger_.trigger(timer); + this->timer_started_trigger_.trigger(*it); break; case api::enums::VOICE_ASSISTANT_TIMER_UPDATED: - this->timer_updated_trigger_.trigger(timer); + this->timer_updated_trigger_.trigger(*it); break; case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED: - this->timer_cancelled_trigger_.trigger(timer); - this->timers_.erase(timer.id); + this->timer_cancelled_trigger_.trigger(*it); + this->timers_.erase(it); break; case api::enums::VOICE_ASSISTANT_TIMER_FINISHED: - this->timer_finished_trigger_.trigger(timer); - this->timers_.erase(timer.id); + this->timer_finished_trigger_.trigger(*it); + this->timers_.erase(it); break; } @@ -901,16 +909,12 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse } void VoiceAssistant::timer_tick_() { - std::vector<Timer> res; - res.reserve(this->timers_.size()); - for (auto &pair : this->timers_) { - auto &timer = pair.second; + for (auto &timer : this->timers_) { if (timer.is_active && timer.seconds_left > 0) { timer.seconds_left--; } - res.push_back(timer); } - this->timer_tick_trigger_.trigger(res); + this->timer_tick_trigger_.trigger(this->timers_); } void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 0ef7ecc81a..b1b5f20bff 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -24,7 +24,6 @@ #include "esphome/components/socket/socket.h" #include <span> -#include <unordered_map> #include <vector> namespace esphome { @@ -226,9 +225,9 @@ class VoiceAssistant : public Component { Trigger<Timer> *get_timer_updated_trigger() { return &this->timer_updated_trigger_; } Trigger<Timer> *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; } Trigger<Timer> *get_timer_finished_trigger() { return &this->timer_finished_trigger_; } - Trigger<std::vector<Timer>> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; } + Trigger<const std::vector<Timer> &> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; } void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; } - const std::unordered_map<std::string, Timer> &get_timers() const { return this->timers_; } + const std::vector<Timer> &get_timers() const { return this->timers_; } protected: bool allocate_buffers_(); @@ -267,13 +266,13 @@ class VoiceAssistant : public Component { api::APIConnection *api_client_{nullptr}; - std::unordered_map<std::string, Timer> timers_; + std::vector<Timer> timers_; void timer_tick_(); Trigger<Timer> timer_started_trigger_; Trigger<Timer> timer_finished_trigger_; Trigger<Timer> timer_updated_trigger_; Trigger<Timer> timer_cancelled_trigger_; - Trigger<std::vector<Timer>> timer_tick_trigger_; + Trigger<const std::vector<Timer> &> timer_tick_trigger_; bool has_timers_{false}; bool timer_tick_running_{false}; diff --git a/tests/components/voice_assistant/common-idf.yaml b/tests/components/voice_assistant/common-idf.yaml index ab8cbf2434..8565683700 100644 --- a/tests/components/voice_assistant/common-idf.yaml +++ b/tests/components/voice_assistant/common-idf.yaml @@ -68,3 +68,24 @@ voice_assistant: - logger.log: format: "Voice assistant error - code %s, message: %s" args: [code.c_str(), message.c_str()] + on_timer_started: + - logger.log: + format: "Timer started: %s" + args: [timer.id.c_str()] + on_timer_updated: + - logger.log: + format: "Timer updated: %s" + args: [timer.id.c_str()] + on_timer_cancelled: + - logger.log: + format: "Timer cancelled: %s" + args: [timer.id.c_str()] + on_timer_finished: + - logger.log: + format: "Timer finished: %s" + args: [timer.id.c_str()] + on_timer_tick: + - lambda: |- + for (auto &timer : timers) { + ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left); + } diff --git a/tests/components/voice_assistant/common.yaml b/tests/components/voice_assistant/common.yaml index f248154b7e..d09de74396 100644 --- a/tests/components/voice_assistant/common.yaml +++ b/tests/components/voice_assistant/common.yaml @@ -58,3 +58,24 @@ voice_assistant: - logger.log: format: "Voice assistant error - code %s, message: %s" args: [code.c_str(), message.c_str()] + on_timer_started: + - logger.log: + format: "Timer started: %s" + args: [timer.id.c_str()] + on_timer_updated: + - logger.log: + format: "Timer updated: %s" + args: [timer.id.c_str()] + on_timer_cancelled: + - logger.log: + format: "Timer cancelled: %s" + args: [timer.id.c_str()] + on_timer_finished: + - logger.log: + format: "Timer finished: %s" + args: [timer.id.c_str()] + on_timer_tick: + - lambda: |- + for (auto &timer : timers) { + ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left); + } From dbf202bf0de5beb0cd09661d424961b810f2db08 Mon Sep 17 00:00:00 2001 From: tronikos <tronikos@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:57:36 -0800 Subject: [PATCH 175/251] Add get_away and get_on in WaterHeaterCall and deprecate get_state (#13891) --- esphome/components/water_heater/water_heater.h | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 93fcf5f401..070ae99575 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -90,9 +90,22 @@ class WaterHeaterCall { float get_target_temperature_low() const { return this->target_temperature_low_; } float get_target_temperature_high() const { return this->target_temperature_high_; } /// Get state flags value + ESPDEPRECATED("get_state() is deprecated, use get_away() and get_on() instead. (Removed in 2026.8.0)", "2026.2.0") uint32_t get_state() const { return this->state_; } - /// Get mask of state flags that are being changed - uint32_t get_state_mask() const { return this->state_mask_; } + + optional<bool> get_away() const { + if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { + return (this->state_ & WATER_HEATER_STATE_AWAY) != 0; + } + return {}; + } + + optional<bool> get_on() const { + if (this->state_mask_ & WATER_HEATER_STATE_ON) { + return (this->state_ & WATER_HEATER_STATE_ON) != 0; + } + return {}; + } protected: void validate_(); From b2b9e0cb0ae69b9d9415790e637172996a2cb2ef Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 9 Feb 2026 22:00:08 +0100 Subject: [PATCH 176/251] [nrf52,zigee] print reporting status (#13890) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/zigbee/zigbee_zephyr.cpp | 29 +++++++++++++++++++++ esphome/components/zigbee/zigbee_zephyr.h | 1 + 2 files changed, 30 insertions(+) diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index 4763943e88..65639de61b 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -3,6 +3,7 @@ #include "esphome/core/log.h" #include <zephyr/settings/settings.h> #include <zephyr/storage/flash_map.h> +#include "esphome/core/hal.h" extern "C" { #include <zboss_api.h> @@ -223,6 +224,7 @@ void ZigbeeComponent::dump_config() { get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf, zb_get_pan_id()); + dump_reporting_(); } static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { @@ -244,6 +246,33 @@ void ZigbeeComponent::factory_reset() { ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0); } +void ZigbeeComponent::dump_reporting_() { +#ifdef ESPHOME_LOG_HAS_VERBOSE + auto now = millis(); + bool first = true; + for (zb_uint8_t j = 0; j < ZCL_CTX().device_ctx->ep_count; j++) { + if (ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info) { + zb_zcl_reporting_info_t *rep_info = ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info; + for (zb_uint8_t i = 0; i < ZCL_CTX().device_ctx->ep_desc_list[j]->rep_info_count; i++) { + if (!first) { + ESP_LOGV(TAG, ""); + } + first = false; + ESP_LOGV(TAG, "Endpoint: %d, cluster_id %d, attr_id %d, flags %d, report in %ums", rep_info->ep, + rep_info->cluster_id, rep_info->attr_id, rep_info->flags, + ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED) + ? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now + : 0); + ESP_LOGV(TAG, "Min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds", + rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval, + rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval); + rep_info++; + } + } + } +#endif +} + } // namespace esphome::zigbee extern "C" void zboss_signal_handler(zb_uint8_t param) { diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index 05895e8e61..bd4b092ad5 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -87,6 +87,7 @@ class ZigbeeComponent : public Component { #ifdef USE_ZIGBEE_WIPE_ON_BOOT void erase_flash_(int area); #endif + void dump_reporting_(); std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{}; CallbackManager<void()> join_cb_; Trigger<> join_trigger_; From 8f74b027b48e102f96551a6a6b8d0857ce72fc44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:40:32 -0600 Subject: [PATCH 177/251] Bump setuptools from 80.10.2 to 82.0.0 (#13897) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1bd43ea2f1..339bc65eed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.10.2", "wheel>=0.43,<0.47"] +requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"] build-backend = "setuptools.build_meta" [project] From 475db750e08ebc6c76f4a05695f66c49bca3efce Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:41:16 -0500 Subject: [PATCH 178/251] [uart] Change available() return type from int to size_t (#13893) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bl0942/bl0942.cpp | 6 +++--- esphome/components/tormatic/tormatic_cover.cpp | 2 +- esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.cpp | 6 +++--- esphome/components/uart/uart_component.h | 2 +- .../components/uart/uart_component_esp8266.cpp | 15 +++++++++------ esphome/components/uart/uart_component_esp8266.h | 4 ++-- .../components/uart/uart_component_esp_idf.cpp | 2 +- esphome/components/uart/uart_component_esp_idf.h | 2 +- esphome/components/uart/uart_component_host.cpp | 7 ++++--- esphome/components/uart/uart_component_host.h | 2 +- .../components/uart/uart_component_libretiny.cpp | 2 +- .../components/uart/uart_component_libretiny.h | 2 +- esphome/components/uart/uart_component_rp2040.cpp | 2 +- esphome/components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 4 ++-- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 2 +- esphome/components/weikai/weikai.h | 2 +- tests/components/uart/common.h | 2 +- 21 files changed, 38 insertions(+), 34 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 95dd689b07..b408c5549c 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200; void BL0942::loop() { DataPacket buffer; - int avail = this->available(); + size_t avail = this->available(); if (!avail) { return; } - if (static_cast<size_t>(avail) < sizeof(buffer)) { + if (avail < sizeof(buffer)) { if (!this->rx_start_) { this->rx_start_ = millis(); } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail); + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail); this->read_array((uint8_t *) &buffer, avail); this->rx_start_ = 0; } diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index ef93964a28..be412d62a8 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() { // Read a GateStatus from the unit. The unit only sends messages in response to // status requests or commands, so a message needs to be sent first. optional<GateStatus> Tormatic::read_gate_status_() { - if (this->available() < static_cast<int>(sizeof(MessageHeader))) { + if (this->available() < sizeof(MessageHeader)) { return {}; } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 72c282f1c4..bb91e5cd7c 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -43,7 +43,7 @@ class UARTDevice { return res; } - int available() { return this->parent_->available(); } + size_t available() { return this->parent_->available(); } void flush() { this->parent_->flush(); } diff --git a/esphome/components/uart/uart_component.cpp b/esphome/components/uart/uart_component.cpp index 30fc208fc9..762e56c399 100644 --- a/esphome/components/uart/uart_component.cpp +++ b/esphome/components/uart/uart_component.cpp @@ -5,13 +5,13 @@ namespace esphome::uart { static const char *const TAG = "uart"; bool UARTComponent::check_read_timeout_(size_t len) { - if (this->available() >= int(len)) + if (this->available() >= len) return true; uint32_t start_time = millis(); - while (this->available() < int(len)) { + while (this->available() < len) { if (millis() - start_time > 100) { - ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); + ESP_LOGE(TAG, "Reading from UART timed out at byte %zu!", this->available()); return false; } yield(); diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index ea6e1562f4..b6ffbbd51f 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -69,7 +69,7 @@ class UARTComponent { // Pure virtual method to return the number of bytes available for reading. // @return Number of available bytes. - virtual int available() = 0; + virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. virtual void flush() = 0; diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 504d494e2e..3ebf381c84 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -206,7 +206,7 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { #endif return true; } -int ESP8266UartComponent::available() { +size_t ESP8266UartComponent::available() { if (this->hw_serial_ != nullptr) { return this->hw_serial_->available(); } else { @@ -329,11 +329,14 @@ uint8_t ESP8266SoftwareSerial::peek_byte() { void ESP8266SoftwareSerial::flush() { // Flush is a NO-OP with software serial, all bytes are written immediately. } -int ESP8266SoftwareSerial::available() { - int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); - if (avail < 0) - return avail + this->rx_buffer_size_; - return avail; +size_t ESP8266SoftwareSerial::available() { + // Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR. + // When in >= out, data is contiguous: [out..in). + // When in < out, data wraps: [out..buf_size) + [0..in). + size_t in = this->rx_in_pos_; + if (in >= this->rx_out_pos_) + return in - this->rx_out_pos_; + return this->rx_buffer_size_ - this->rx_out_pos_ + in; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index e33dd00644..e84cbe386d 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -23,7 +23,7 @@ class ESP8266SoftwareSerial { void write_byte(uint8_t data); - int available(); + size_t available(); protected: static void gpio_intr(ESP8266SoftwareSerial *arg); @@ -57,7 +57,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 90997787aa..19b9a4077f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -338,7 +338,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { return read_len == (int32_t) length_to_read; } -int IDFUARTComponent::available() { +size_t IDFUARTComponent::available() { size_t available = 0; esp_err_t err; diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index bd6d0c792e..1ecb02d7ab 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -22,7 +22,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 69b24607d1..0e5ef3c6bd 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -265,7 +265,7 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) { return true; } -int HostUartComponent::available() { +size_t HostUartComponent::available() { if (this->file_descriptor_ == -1) { return 0; } @@ -275,9 +275,10 @@ int HostUartComponent::available() { this->update_error_(strerror(errno)); return 0; } + size_t result = available; if (this->has_peek_) - available++; - return available; + result++; + return result; }; void HostUartComponent::flush() { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index a4a6946c0c..89b951093b 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -17,7 +17,7 @@ class HostUartComponent : public UARTComponent, public Component { void write_array(const uint8_t *data, size_t len) override; bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 863732c88d..cb4465068d 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -169,7 +169,7 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { return true; } -int LibreTinyUARTComponent::available() { return this->serial_->available(); } +size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } void LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index ec13e7da5a..31f082d31e 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -21,7 +21,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 5799d26a54..0c6834055c 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -186,7 +186,7 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { #endif return true; } -int RP2040UartComponent::available() { return this->serial_->available(); } +size_t RP2040UartComponent::available() { return this->serial_->available(); } void RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index d626d11a2e..4ca58e8dc6 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -24,7 +24,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 065d7282d5..ddcc65232d 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -81,7 +81,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC void write_array(const uint8_t *data, size_t len) override; bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; protected: diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index 224d6e3ab1..d33fb80f78 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -318,12 +318,12 @@ bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) { return bytes_read == original_len; } -int USBCDCACMInstance::available() { +size_t USBCDCACMInstance::available() { UBaseType_t waiting = 0; if (this->usb_rx_ringbuf_ != nullptr) { vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); } - return static_cast<int>(waiting) + (this->has_peek_ ? 1 : 0); + return waiting + (this->has_peek_ ? 1 : 0); } void USBCDCACMInstance::flush() { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 96c17bd155..94e6120457 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -97,7 +97,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon bool peek_byte(uint8_t *data) override; ; bool read_array(uint8_t *data, size_t len) override; - int available() override { return static_cast<int>(this->input_buffer_.get_available()); } + size_t available() override { return this->input_buffer_.get_available(); } void flush() override {} void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index d0a8f8366b..1d835daf1e 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -401,7 +401,7 @@ bool WeikaiChannel::peek_byte(uint8_t *buffer) { return this->receive_buffer_.peek(*buffer); } -int WeikaiChannel::available() { +size_t WeikaiChannel::available() { size_t available = this->receive_buffer_.count(); if (!available) available = xfer_fifo_to_buffer_(); diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 4440d9414e..43c3a1e4f4 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -374,7 +374,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @brief Returns the number of bytes in the receive buffer /// @return the number of bytes available in the receiver fifo - int available() override; + size_t available() override; /// @brief Flush the output fifo. /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index 5597b86410..1f9bfa15e7 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -29,7 +29,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); - MOCK_METHOD(int, available, (), (override)); + MOCK_METHOD(size_t, available, (), (override)); MOCK_METHOD(void, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; From 7c1327f96ae904641800650deb4c95348fc23fd4 Mon Sep 17 00:00:00 2001 From: George Joseph <gtjoseph@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:44:47 -0700 Subject: [PATCH 179/251] [mipi_dsi] Add WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD 3.4C and 4C (#13840) --- .../components/mipi_dsi/models/waveshare.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index f83cacc5a3..bf4f9063bb 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -120,3 +120,101 @@ DriverChip( (0xB2, 0x10), ], ) + +DriverChip( + "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C", + height=800, + width=800, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=24, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00), + (0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A), + (0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x00), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18), + (0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F), + (0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30), + (0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31), + (0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25), + (0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C), + (0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F), + (0xE0, 0x02), # select page 2 + (0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A), + (0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57), + (0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F), + (0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45), + (0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77), + (0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F), + (0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09), + (0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03), + (0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06), + (0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F), + (0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F), + (0xE0, 0x02), # select page 2 + (0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02), + (0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73), + (0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88), + (0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43), + (0xE0, 0x00), # select userpage + ], +) + +DriverChip( + "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C", + height=720, + width=720, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=24, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00), + (0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A), + (0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x04), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18), + (0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F), + (0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30), + (0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31), + (0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25), + (0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C), + (0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F), + (0xE0, 0x02), # select page 2 + (0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A), + (0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57), + (0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F), + (0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45), + (0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77), + (0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F), + (0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09), + (0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03), + (0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06), + (0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F), + (0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F), + (0xE0, 0x02), # select page 2 + (0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02), + (0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73), + (0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88), + (0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43), + (0xE0, 0x00), # select userpage + ] +) From 3767c5ec91c7ab483a8a68982b82cdd028cc5546 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 16:48:08 -0600 Subject: [PATCH 180/251] [scheduler] Make core timer ID collisions impossible with type-safe internal IDs (#13882) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> From dacc557a16232b4e51e47609fa94ce7f66808c70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 18:15:48 -0600 Subject: [PATCH 181/251] [uart] Convert parity_to_str to PROGMEM_STRING_TABLE (#13805) --- esphome/components/uart/uart.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 6cfd6537a5..337fa352c1 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -3,12 +3,16 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include <cinttypes> namespace esphome::uart { static const char *const TAG = "uart"; +// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD +PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN"); + void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, uint8_t data_bits) { if (this->parent_->get_baud_rate() != baud_rate) { @@ -30,16 +34,7 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART } const LogString *parity_to_str(UARTParityOptions parity) { - switch (parity) { - case UART_CONFIG_PARITY_NONE: - return LOG_STR("NONE"); - case UART_CONFIG_PARITY_EVEN: - return LOG_STR("EVEN"); - case UART_CONFIG_PARITY_ODD: - return LOG_STR("ODD"); - default: - return LOG_STR("UNKNOWN"); - } + return UARTParityStrings::get_log_str(static_cast<uint8_t>(parity), UARTParityStrings::LAST_INDEX); } } // namespace esphome::uart From 78df8be31fd061cba7a09179db45977aba08e6e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 18:16:27 -0600 Subject: [PATCH 182/251] [logger] Resolve thread name once and pass through logging chain (#13836) --- esphome/components/logger/logger.cpp | 43 ++++--- esphome/components/logger/logger.h | 114 +++++++++--------- .../logger/task_log_buffer_esp32.cpp | 3 +- .../components/logger/task_log_buffer_esp32.h | 2 +- .../logger/task_log_buffer_host.cpp | 12 +- .../components/logger/task_log_buffer_host.h | 3 +- .../logger/task_log_buffer_libretiny.cpp | 3 +- .../logger/task_log_buffer_libretiny.h | 2 +- 8 files changed, 93 insertions(+), 89 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 4cbd4f1bf1..bb20c403e5 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -36,8 +36,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch #endif // Fast path: main thread, no recursion (99.9% of all logs) + // Pass nullptr for thread_name since we already know this is the main task if (is_main_task && !this->main_task_recursion_guard_) [[likely]] { - this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args); + this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr); return; } @@ -47,21 +48,23 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch } // Non-main thread handling (~0.1% of logs) + // Resolve thread name once and pass it through the logging chain. + // ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle() + // (we already have the handle from the main task check above). + // Host: pass a stack buffer for pthread_getname_np to write into. #if defined(USE_ESP32) || defined(USE_LIBRETINY) - this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task); + const char *thread_name = get_thread_name_(current_task); #else // USE_HOST - this->log_vprintf_non_main_thread_(level, tag, line, format, args); + char thread_name_buf[THREAD_NAME_BUF_SIZE]; + const char *thread_name = this->get_thread_name_(thread_name_buf); #endif + this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name); } // Handles non-main thread logging only // Kept separate from hot path to improve instruction cache performance -#if defined(USE_ESP32) || defined(USE_LIBRETINY) void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, - TaskHandle_t current_task) { -#else // USE_HOST -void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) { -#endif + const char *thread_name) { // Check if already in recursion for this non-main thread/task if (this->is_non_main_task_recursive_()) { return; @@ -73,12 +76,8 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li bool message_sent = false; #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main threads/tasks, queue the message for callbacks -#if defined(USE_ESP32) || defined(USE_LIBRETINY) message_sent = - this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args); -#else // USE_HOST - message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args); -#endif + this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args); if (message_sent) { // Enable logger loop to process the buffered message // This is safe to call from any context including ISRs @@ -101,19 +100,27 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li #endif char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name); this->write_to_console_(buf); } // RAII guard automatically resets on return } #else -// Implementation for all other platforms (single-task, no threading) +// Implementation for single-task platforms (ESP8266, RP2040, Zephyr) +// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path. +// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking. +// Not a problem in practice yet since Zephyr has no API support (logs are console-only). void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; - - this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); +#ifdef USE_ZEPHYR + char tmp[MAX_POINTER_REPRESENTATION]; + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, + this->get_thread_name_(tmp)); +#else // Other single-task platforms don't have thread names, so pass nullptr + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr); +#endif } #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY @@ -129,7 +136,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas if (level > this->level_for(tag) || global_recursion_guard_) return; - this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr); } #endif // USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 1678fed5f5..ecf032ee0e 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,6 +2,7 @@ #include <cstdarg> #include <map> +#include <span> #include <type_traits> #if defined(USE_ESP32) || defined(USE_HOST) #include <pthread.h> @@ -124,6 +125,10 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Stack buffer size for retrieving thread/task names from the OS +// macOS allows up to 64 bytes, Linux up to 16 +static constexpr size_t THREAD_NAME_BUF_SIZE = 64; + // Buffer wrapper for log formatting functions struct LogBuffer { char *data; @@ -408,34 +413,24 @@ class Logger : public Component { #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) // Handles non-main thread logging only (~0.1% of calls) -#if defined(USE_ESP32) || defined(USE_LIBRETINY) - // ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice + // thread_name is resolved by the caller from the task handle, avoiding redundant lookups void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, - TaskHandle_t current_task); -#else // USE_HOST - // Host: No task handle parameter needed (not used in send_message_thread_safe) - void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args); -#endif + const char *thread_name); #endif void process_messages_(); void write_msg_(const char *msg, uint16_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator + // thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, - va_list args, LogBuffer &buf) { -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) - buf.write_header(level, tag, line, this->get_thread_name_()); -#elif defined(USE_ZEPHYR) - char tmp[MAX_POINTER_REPRESENTATION]; - buf.write_header(level, tag, line, this->get_thread_name_(tmp)); -#else - buf.write_header(level, tag, line, nullptr); -#endif + va_list args, LogBuffer &buf, const char *thread_name) { + buf.write_header(level, tag, line, thread_name); buf.format_body(format, args); } #ifdef USE_STORE_LOG_STR_IN_FLASH // Format a log message with flash string format and write it to a buffer with header, footer, and null terminator + // ESP8266-only (single-task), thread_name is always nullptr inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args, LogBuffer &buf) { @@ -466,9 +461,10 @@ class Logger : public Component { // Helper to format and send a log message to both console and listeners // Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings + // thread_name: name of the calling thread/task, or nullptr for main task template<typename FormatType> inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, - FormatType format, va_list args) { + FormatType format, va_list args, const char *thread_name) { RecursionGuard guard(recursion_guard); LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; #ifdef USE_STORE_LOG_STR_IN_FLASH @@ -477,7 +473,7 @@ class Logger : public Component { } else #endif { - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name); } this->notify_listeners_(level, tag, buf); this->write_log_buffer_to_console_(buf); @@ -565,37 +561,57 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) - const char *HOT get_thread_name_( -#ifdef USE_ZEPHYR - char *buff + // --- get_thread_name_ overloads (per-platform) --- + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls + // when the caller already has the handle (e.g. from the main task check in log_vprintf_) + const char *get_thread_name_(TaskHandle_t task) { + if (task == this->main_task_) { + return nullptr; // Main task + } +#if defined(USE_ESP32) + return pcTaskGetName(task); +#elif defined(USE_LIBRETINY) + return pcTaskGetTaskName(task); #endif - ) { -#ifdef USE_ZEPHYR + } + + // Convenience overload - gets the current task handle and delegates + const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); } + +#elif defined(USE_HOST) + // Takes a caller-provided buffer for the thread name (stack-allocated for thread safety) + const char *HOT get_thread_name_(std::span<char> buff) { + pthread_t current_thread = pthread_self(); + if (pthread_equal(current_thread, main_thread_)) { + return nullptr; // Main thread + } + // For non-main threads, get the thread name into the caller-provided buffer + if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) { + return buff.data(); + } + return nullptr; + } + +#elif defined(USE_ZEPHYR) + const char *HOT get_thread_name_(std::span<char> buff) { k_tid_t current_task = k_current_get(); -#else - TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); -#endif if (current_task == main_task_) { return nullptr; // Main task - } else { -#if defined(USE_ESP32) - return pcTaskGetName(current_task); -#elif defined(USE_LIBRETINY) - return pcTaskGetTaskName(current_task); -#elif defined(USE_ZEPHYR) - const char *name = k_thread_name_get(current_task); - if (name) { - // zephyr print task names only if debug component is present - return name; - } - std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task); - return buff; -#endif } + const char *name = k_thread_name_get(current_task); + if (name) { + // zephyr print task names only if debug component is present + return name; + } + std::snprintf(buff.data(), buff.size(), "%p", current_task); + return buff.data(); } #endif + // --- Non-main task recursion guards (per-platform) --- + #if defined(USE_ESP32) || defined(USE_HOST) // RAII guard for non-main task recursion using pthread TLS class NonMainTaskRecursionGuard { @@ -635,22 +651,6 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -#ifdef USE_HOST - const char *HOT get_thread_name_() { - pthread_t current_thread = pthread_self(); - if (pthread_equal(current_thread, main_thread_)) { - return nullptr; // Main thread - } - // For non-main threads, return the thread name - // We store it in thread-local storage to avoid allocation - static thread_local char thread_name_buf[32]; - if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) { - return thread_name_buf; - } - return nullptr; - } -#endif - #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { diff --git a/esphome/components/logger/task_log_buffer_esp32.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp index b9dfe45b7f..56c0a4ae2d 100644 --- a/esphome/components/logger/task_log_buffer_esp32.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) { last_processed_counter_ = message_counter_.load(std::memory_order_relaxed); } -bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) va_list args_copy; @@ -95,7 +95,6 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin // Store the thread name now instead of waiting until main loop processing // This avoids crashes if the task completes or is deleted between when this message // is enqueued and when it's processed by the main loop - const char *thread_name = pcTaskGetName(task_handle); if (thread_name != nullptr) { strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination diff --git a/esphome/components/logger/task_log_buffer_esp32.h b/esphome/components/logger/task_log_buffer_esp32.h index fde9bd60d5..6c1bafaeba 100644 --- a/esphome/components/logger/task_log_buffer_esp32.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -58,7 +58,7 @@ class TaskLogBuffer { void release_message_main_loop(void *token); // Thread-safe - send a message to the ring buffer from any thread - bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args); // Check if there are messages ready to be processed using an atomic counter for performance diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp index 0660aeb061..676686304a 100644 --- a/esphome/components/logger/task_log_buffer_host.cpp +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) { } } -bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, - va_list args) { +bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { // Acquire a slot int slot_index = this->acquire_write_slot_(); if (slot_index < 0) { @@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, msg.tag = tag; msg.line = line; - // Get thread name using pthread - char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE]; - // pthread_getname_np works the same on Linux and macOS - if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) { - strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1); + // Store the thread name now to avoid crashes if thread exits before processing + if (thread_name != nullptr) { + strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1); msg.thread_name[sizeof(msg.thread_name) - 1] = '\0'; } else { msg.thread_name[0] = '\0'; diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h index d421d50ec6..f8e4ee7bee 100644 --- a/esphome/components/logger/task_log_buffer_host.h +++ b/esphome/components/logger/task_log_buffer_host.h @@ -86,7 +86,8 @@ class TaskLogBufferHost { // Thread-safe - send a message to the buffer from any thread // Returns true if message was queued, false if buffer is full - bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args); + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args); // Check if there are messages ready to be processed inline bool HOT has_messages() const { diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index 580066e621..5a22857dcb 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -101,7 +101,7 @@ void TaskLogBufferLibreTiny::release_message_main_loop() { } bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, - TaskHandle_t task_handle, const char *format, va_list args) { + const char *thread_name, const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) va_list args_copy; va_copy(args_copy, args); @@ -162,7 +162,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char msg->line = line; // Store the thread name now to avoid crashes if task is deleted before processing - const char *thread_name = pcTaskGetTaskName(task_handle); if (thread_name != nullptr) { strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index bf6b2d2fa4..bf315e828a 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -70,7 +70,7 @@ class TaskLogBufferLibreTiny { void release_message_main_loop(); // Thread-safe - send a message to the buffer from any thread - bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args); // Fast check using volatile counter - no lock needed From bcd4a9fc39a3a35e4ea58d28f2bfc9495e7790bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 18:20:53 -0600 Subject: [PATCH 183/251] [pylontech] Batch UART reads to reduce loop overhead (#13824) --- esphome/components/pylontech/pylontech.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 1dc7caaf16..d724253256 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -56,17 +56,23 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - if (this->available() > 0) { + int avail = this->available(); + if (avail > 0) { // pylontech sends a lot of data very suddenly // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow - uint8_t data; int recv = 0; - while (this->available() > 0) { - if (this->read_byte(&data)) { - buffer_[buffer_index_write_] += (char) data; - recv++; - if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) || - buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + recv += to_read; + + for (size_t i = 0; i < to_read; i++) { + buffer_[buffer_index_write_] += (char) buf[i]; + if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { // complete line received buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; } From 2edfcf278fb8cb549b175701b4a70fe96c742eda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 18:21:10 -0600 Subject: [PATCH 184/251] [hlk_fm22x] Replace per-cycle vector allocation with member buffer (#13859) --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 62 +++++++++++++--------- esphome/components/hlk_fm22x/hlk_fm22x.h | 10 ++-- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index c0f14c7105..18d26f057a 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -1,20 +1,16 @@ #include "hlk_fm22x.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#include <array> #include <cinttypes> namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; -// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) -static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; - void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); this->set_enrolling_(false); - while (this->available()) { + while (this->available() > 0) { this->read(); } this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); @@ -35,7 +31,7 @@ void HlkFm22xComponent::update() { } void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { - if (name.length() > 31) { + if (name.length() > HLK_FM22X_NAME_SIZE - 1) { ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); return; } @@ -88,7 +84,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da } this->wait_cycles_ = 0; this->active_command_ = command; - while (this->available()) + while (this->available() > 0) this->read(); this->write((uint8_t) (START_CODE >> 8)); this->write((uint8_t) (START_CODE & 0xFF)); @@ -137,17 +133,24 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; - std::vector<uint8_t> data; - data.reserve(length); + if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { + ESP_LOGE(TAG, "Response too large: %u bytes", length); + // Discard exactly the remaining payload and checksum for this frame + for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i) + this->read(); + return; + } + for (uint16_t idx = 0; idx < length; ++idx) { byte = this->read(); checksum ^= byte; - data.push_back(byte); + this->recv_buf_[idx] = byte; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; - ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size())); + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, + format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length)); #endif byte = this->read(); @@ -157,10 +160,10 @@ void HlkFm22xComponent::recv_command_() { } switch (response_type) { case HlkFm22xResponseType::NOTE: - this->handle_note_(data); + this->handle_note_(this->recv_buf_.data(), length); break; case HlkFm22xResponseType::REPLY: - this->handle_reply_(data); + this->handle_reply_(this->recv_buf_.data(), length); break; default: ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); @@ -168,11 +171,15 @@ void HlkFm22xComponent::recv_command_() { } } -void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) { +void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) { + if (length < 1) { + ESP_LOGE(TAG, "Empty note data"); + return; + } switch (data[0]) { case HlkFm22xNoteType::FACE_STATE: - if (data.size() < 17) { - ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); + if (length < 17) { + ESP_LOGE(TAG, "Invalid face note data size: %zu", length); break; } { @@ -209,9 +216,13 @@ void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) { } } -void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) { +void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { auto expected = this->active_command_; this->active_command_ = HlkFm22xCommand::NONE; + if (length < 2) { + ESP_LOGE(TAG, "Reply too short: %zu bytes", length); + return; + } if (data[0] != (uint8_t) expected) { ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); return; @@ -238,16 +249,20 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) { } switch (expected) { case HlkFm22xCommand::VERIFY: { + if (length < 4 + HLK_FM22X_NAME_SIZE) { + ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length); + break; + } int16_t face_id = ((int16_t) data[2] << 8) | data[3]; - std::string name(data.begin() + 4, data.begin() + 36); - ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); + const char *name_ptr = reinterpret_cast<const char *>(data + 4); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr); if (this->last_face_id_sensor_ != nullptr) { this->last_face_id_sensor_->publish_state(face_id); } if (this->last_face_name_text_sensor_ != nullptr) { - this->last_face_name_text_sensor_->publish_state(name); + this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE); } - this->face_scan_matched_callback_.call(face_id, name); + this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE)); break; } case HlkFm22xCommand::ENROLL: { @@ -266,9 +281,8 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) { this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); break; case HlkFm22xCommand::GET_VERSION: - if (this->version_text_sensor_ != nullptr) { - std::string version(data.begin() + 2, data.end()); - this->version_text_sensor_->publish_state(version); + if (this->version_text_sensor_ != nullptr && length > 2) { + this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2); } this->defer([this]() { this->get_face_count_(); }); break; diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 9c981d3c44..0ea4636281 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -7,12 +7,15 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/uart/uart.h" +#include <array> #include <utility> -#include <vector> namespace esphome::hlk_fm22x { static const uint16_t START_CODE = 0xEFAA; +static constexpr size_t HLK_FM22X_NAME_SIZE = 32; +// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36 +static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; enum HlkFm22xCommand { NONE = 0x00, RESET = 0x10, @@ -118,10 +121,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { void get_face_count_(); void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); void recv_command_(); - void handle_note_(const std::vector<uint8_t> &data); - void handle_reply_(const std::vector<uint8_t> &data); + void handle_note_(const uint8_t *data, size_t length); + void handle_reply_(const uint8_t *data, size_t length); void set_enrolling_(bool enrolling); + std::array<uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE> recv_buf_; HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; uint16_t wait_cycles_ = 0; sensor::Sensor *face_count_sensor_{nullptr}; From 57b85a840017e6aa8662c1a0228bcb50052a597a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 18:24:20 -0600 Subject: [PATCH 185/251] [dlms_meter] Batch UART reads to reduce per-loop overhead (#13828) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/dlms_meter/dlms_meter.cpp | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index 6aa465143e..11d05b3a08 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -28,15 +28,28 @@ void DlmsMeterComponent::dump_config() { void DlmsMeterComponent::loop() { // Read while data is available, netznoe uses two frames so allow 2x max frame length - while (this->available()) { - if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) { + size_t avail = this->available(); + if (avail > 0) { + size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size(); + if (remaining == 0) { ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes"); - break; + } else { + // Read all available bytes in batches to reduce UART call overhead. + // Cap reads to remaining buffer capacity. + if (avail > remaining) { + avail = remaining; + } + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read); + this->last_read_ = millis(); + } } - uint8_t c; - this->read_byte(&c); - this->receive_buffer_.push_back(c); - this->last_read_ = millis(); } if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) { From 01a90074ba94e9661aa077ba272bc6e2d350ee51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 18:25:34 -0600 Subject: [PATCH 186/251] [ld2420] Batch UART reads to reduce loop overhead (#13821) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ld2420/ld2420.cpp | 22 ++++++++++++++++++++-- esphome/components/ld2420/ld2420.h | 2 ++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 10c623bce0..69b69f4a61 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -335,9 +335,10 @@ void LD2420Component::revert_config_action() { void LD2420Component::loop() { // If there is a active send command do not process it here, the send command call will handle it. - while (!this->cmd_active_ && this->available()) { - this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH); + if (this->cmd_active_) { + return; } + this->read_batch_(this->buffer_data_); } void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) { @@ -539,6 +540,23 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { } } +void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) { + // Read all available bytes in batches to reduce UART call overhead. + size_t avail = this->available(); + uint8_t buf[MAX_LINE_LENGTH]; + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + + for (size_t i = 0; i < to_read; i++) { + this->readline_(buf[i], buffer.data(), buffer.size()); + } + } +} + void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND]; this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH]; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 50ddf45264..6d81f86497 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -4,6 +4,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include <span> #ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" #endif @@ -165,6 +166,7 @@ class LD2420Component : public Component, public uart::UARTDevice { void handle_energy_mode_(uint8_t *buffer, int len); void handle_ack_data_(uint8_t *buffer, int len); void readline_(int rx_data, uint8_t *buffer, int len); + void read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer); void set_calibration_(bool state) { this->calibration_ = state; }; bool get_calibration_() { return this->calibration_; }; From 097901e9c89e284e424bb51eb6cc504a07ba261b Mon Sep 17 00:00:00 2001 From: Sean Kelly <xconverge@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:30:37 -0800 Subject: [PATCH 187/251] [aqi] Fix AQI calculation for specific pm2.5 or pm10 readings (#13770) --- esphome/components/aqi/aqi_calculator.h | 38 +++++++++++++++++++----- esphome/components/aqi/caqi_calculator.h | 30 ++++++++++++++++--- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index 993504c1e9..d624af0432 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include <algorithm> #include <cmath> #include <limits> #include "abstract_aqi_calculator.h" @@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast<uint16_t>(std::lround(aqi)); } protected: @@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f}, - {35.5f, 55.4f}, {55.5f, 125.4f}, - {125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}}; + static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 9.1f}, + {9.1f, 35.5f}, + {35.5f, 55.5f}, + {55.5f, 125.5f}, + {125.5f, 225.5f}, + {225.5f, std::numeric_limits<float>::max()} + // clang-format on + }; - static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f}, - {155.0f, 254.0f}, {255.0f, 354.0f}, - {355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}}; + static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 55.0f}, + {55.0f, 155.0f}, + {155.0f, 255.0f}, + {255.0f, 355.0f}, + {355.0f, 425.0f}, + {425.0f, std::numeric_limits<float>::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index d2ec4bb98f..fe2efe7059 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include <algorithm> #include <cmath> #include <limits> #include "abstract_aqi_calculator.h" @@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast<uint16_t>(std::lround(aqi)); } protected: @@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { - {0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}}; + // clang-format off + {0.0f, 15.1f}, + {15.1f, 30.1f}, + {30.1f, 55.1f}, + {55.1f, 110.1f}, + {110.1f, std::numeric_limits<float>::max()} + // clang-format on + }; static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { - {0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}}; + // clang-format off + {0.0f, 25.1f}, + {25.1f, 50.1f}, + {50.1f, 90.1f}, + {90.1f, 180.1f}, + {180.1f, std::numeric_limits<float>::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } From 87ac263264b02033638abbed3d661ff33ff7bffd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Feb 2026 18:32:52 -0600 Subject: [PATCH 188/251] [dsmr] Batch UART reads to reduce per-loop overhead (#13826) --- esphome/components/dsmr/dsmr.cpp | 243 +++++++++++++++++-------------- esphome/components/dsmr/dsmr.h | 1 + 2 files changed, 137 insertions(+), 107 deletions(-) diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index c78d37bf5e..20fbd20cd6 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() { this->start_requesting_data_(); } if (!this->requesting_data_) { - while (this->available()) { - this->read(); - } + this->drain_rx_buffer_(); } } return this->requesting_data_; @@ -115,138 +113,169 @@ void Dsmr::stop_requesting_data_() { } else { ESP_LOGV(TAG, "Stop reading data from P1 port"); } - while (this->available()) { - this->read(); - } + this->drain_rx_buffer_(); this->requesting_data_ = false; } } +void Dsmr::drain_rx_buffer_() { + uint8_t buf[64]; + int avail; + while ((avail = this->available()) > 0) { + if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) { + break; + } + } +} + void Dsmr::reset_telegram_() { this->header_found_ = false; this->footer_found_ = false; this->bytes_read_ = 0; this->crypt_bytes_read_ = 0; this->crypt_telegram_len_ = 0; - this->last_read_time_ = 0; } void Dsmr::receive_telegram_() { while (this->available_within_timeout_()) { - const char c = this->read(); + // Read all available bytes in batches to reduce UART call overhead. + uint8_t buf[64]; + int avail = this->available(); + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) + return; + avail -= to_read; - // Find a new telegram header, i.e. forward slash. - if (c == '/') { - ESP_LOGV(TAG, "Header of telegram found"); - this->reset_telegram_(); - this->header_found_ = true; - } - if (!this->header_found_) - continue; + for (size_t i = 0; i < to_read; i++) { + const char c = static_cast<char>(buf[i]); - // Check for buffer overflow. - if (this->bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } + // Find a new telegram header, i.e. forward slash. + if (c == '/') { + ESP_LOGV(TAG, "Header of telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + if (!this->header_found_) + continue; - // Some v2.2 or v3 meters will send a new value which starts with '(' - // in a new line, while the value belongs to the previous ObisId. For - // proper parsing, remove these new line characters. - if (c == '(') { - while (true) { - auto previous_char = this->telegram_[this->bytes_read_ - 1]; - if (previous_char == '\n' || previous_char == '\r') { - this->bytes_read_--; - } else { - break; + // Check for buffer overflow. + if (this->bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); + return; + } + + // Some v2.2 or v3 meters will send a new value which starts with '(' + // in a new line, while the value belongs to the previous ObisId. For + // proper parsing, remove these new line characters. + if (c == '(') { + while (true) { + auto previous_char = this->telegram_[this->bytes_read_ - 1]; + if (previous_char == '\n' || previous_char == '\r') { + this->bytes_read_--; + } else { + break; + } + } + } + + // Store the byte in the buffer. + this->telegram_[this->bytes_read_] = c; + this->bytes_read_++; + + // Check for a footer, i.e. exclamation mark, followed by a hex checksum. + if (c == '!') { + ESP_LOGV(TAG, "Footer of telegram found"); + this->footer_found_ = true; + continue; + } + // Check for the end of the hex checksum, i.e. a newline. + if (this->footer_found_ && c == '\n') { + // Parse the telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; } } } - - // Store the byte in the buffer. - this->telegram_[this->bytes_read_] = c; - this->bytes_read_++; - - // Check for a footer, i.e. exclamation mark, followed by a hex checksum. - if (c == '!') { - ESP_LOGV(TAG, "Footer of telegram found"); - this->footer_found_ = true; - continue; - } - // Check for the end of the hex checksum, i.e. a newline. - if (this->footer_found_ && c == '\n') { - // Parse the telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; - } } } void Dsmr::receive_encrypted_telegram_() { while (this->available_within_timeout_()) { - const char c = this->read(); + // Read all available bytes in batches to reduce UART call overhead. + uint8_t buf[64]; + int avail = this->available(); + while (avail > 0) { + size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) + return; + avail -= to_read; - // Find a new telegram start byte. - if (!this->header_found_) { - if ((uint8_t) c != 0xDB) { - continue; + for (size_t i = 0; i < to_read; i++) { + const char c = static_cast<char>(buf[i]); + + // Find a new telegram start byte. + if (!this->header_found_) { + if ((uint8_t) c != 0xDB) { + continue; + } + ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + + // Check for buffer overflow. + if (this->crypt_bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); + return; + } + + // Store the byte in the buffer. + this->crypt_telegram_[this->crypt_bytes_read_] = c; + this->crypt_bytes_read_++; + + // Read the length of the incoming encrypted telegram. + if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { + // Complete header + data bytes + this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); + ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); + } + + // Check for the end of the encrypted telegram. + if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { + continue; + } + ESP_LOGV(TAG, "End of encrypted telegram found"); + + // Decrypt the encrypted telegram. + GCM<AES128> *gcmaes128{new GCM<AES128>()}; + gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); + // the iv is 8 bytes of the system title + 4 bytes frame counter + // system title is at byte 2 and frame counter at byte 15 + for (int i = 10; i < 14; i++) + this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); + gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_), + // the ciphertext start at byte 18 + &this->crypt_telegram_[18], + // cipher size + this->crypt_bytes_read_ - 17); + delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) + + this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); + ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); + ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); + + // Parse the decrypted telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; } - ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); - this->reset_telegram_(); - this->header_found_ = true; } - - // Check for buffer overflow. - if (this->crypt_bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } - - // Store the byte in the buffer. - this->crypt_telegram_[this->crypt_bytes_read_] = c; - this->crypt_bytes_read_++; - - // Read the length of the incoming encrypted telegram. - if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { - // Complete header + data bytes - this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); - ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); - } - - // Check for the end of the encrypted telegram. - if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { - continue; - } - ESP_LOGV(TAG, "End of encrypted telegram found"); - - // Decrypt the encrypted telegram. - GCM<AES128> *gcmaes128{new GCM<AES128>()}; - gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); - // the iv is 8 bytes of the system title + 4 bytes frame counter - // system title is at byte 2 and frame counter at byte 15 - for (int i = 10; i < 14; i++) - this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; - constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); - gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_), - // the ciphertext start at byte 18 - &this->crypt_telegram_[18], - // cipher size - this->crypt_bytes_read_ - 17); - delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - - this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); - ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); - ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); - - // Parse the decrypted telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; } } diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index b7e05a22b3..fafcf62b87 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -85,6 +85,7 @@ class Dsmr : public Component, public uart::UARTDevice { void receive_telegram_(); void receive_encrypted_telegram_(); void reset_telegram_(); + void drain_rx_buffer_(); /// Wait for UART data to become available within the read timeout. /// From dcbb0204794d672fcfc66f4174bb4ca0a1e9dc9f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:02:41 -0500 Subject: [PATCH 189/251] [uart] Fix available() return type to size_t across components (#13898) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/cse7766/cse7766.cpp | 6 +++--- esphome/components/dfplayer/dfplayer.cpp | 4 ++-- esphome/components/dsmr/dsmr.cpp | 12 ++++++------ esphome/components/ld2410/ld2410.cpp | 4 ++-- esphome/components/ld2412/ld2412.cpp | 4 ++-- esphome/components/ld2450/ld2450.cpp | 4 ++-- esphome/components/modbus/modbus.cpp | 4 ++-- esphome/components/nextion/nextion.cpp | 4 ++-- esphome/components/pipsolar/pipsolar.cpp | 8 ++++---- esphome/components/pylontech/pylontech.cpp | 4 ++-- esphome/components/rd03d/rd03d.cpp | 4 ++-- esphome/components/rf_bridge/rf_bridge.cpp | 4 ++-- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 4 ++-- esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp | 4 ++-- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 4 ++-- esphome/components/tuya/tuya.cpp | 4 ++-- 16 files changed, 39 insertions(+), 39 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 45abd3ca3d..7ffdf757a0 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -16,8 +16,8 @@ void CSE7766Component::loop() { } // Early return prevents updating last_transmission_ when no data is available. - int avail = this->available(); - if (avail <= 0) { + size_t avail = this->available(); + if (avail == 0) { return; } @@ -27,7 +27,7 @@ void CSE7766Component::loop() { // At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call. uint8_t buf[CSE7766_RAW_DATA_SIZE]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 48c06be558..79f8fd03c3 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { void DFPlayer::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 20fbd20cd6..baf7f59314 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() { void Dsmr::drain_rx_buffer_() { uint8_t buf[64]; - int avail; + size_t avail; while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { break; } } @@ -140,9 +140,9 @@ void Dsmr::receive_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; @@ -206,9 +206,9 @@ void Dsmr::receive_encrypted_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index b57b1d9978..95a04f768a 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() { void LD2410Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index f8ceee78eb..95e19e0d5f 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() { void LD2412Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 38ba0d7f96..b04b509a16 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -277,10 +277,10 @@ void LD2450Component::dump_config() { void LD2450Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index c1f5635028..d40343db33 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -20,10 +20,10 @@ void Modbus::loop() { const uint32_t now = App.get_loop_component_start_time(); // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 56bbc840fb..9f1ce47837 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) { void Nextion::process_serial_() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index d7b37f6130..e6831ad19e 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -14,9 +14,9 @@ void Pipsolar::setup() { void Pipsolar::empty_uart_buffer_() { uint8_t buf[64]; - int avail; + size_t avail; while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { break; } } @@ -97,10 +97,10 @@ void Pipsolar::loop() { } if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index d724253256..7eb89d5b32 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -56,14 +56,14 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - int avail = this->available(); + size_t avail = this->available(); if (avail > 0) { // pylontech sends a lot of data very suddenly // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow int recv = 0; uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index e4dbdf41cb..d47347fcfa 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -82,10 +82,10 @@ void RD03DComponent::dump_config() { void RD03DComponent::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index e33c13aafe..d8c148145c 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -136,10 +136,10 @@ void RFBridgeComponent::loop() { this->last_bridge_byte_ = now; } - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 3f2103b401..99d519b434 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -107,10 +107,10 @@ void MR24HPC1Component::update_() { // main loop void MR24HPC1Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index d95e13241d..12f188fe03 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -31,10 +31,10 @@ void MR60BHA2Component::dump_config() { // main loop void MR60BHA2Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 441ee2b5c2..5d571618d3 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -50,10 +50,10 @@ void MR60FDA2Component::setup() { // main loop void MR60FDA2Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 9ee4c09b86..a1acbf2f56 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -32,10 +32,10 @@ void Tuya::setup() { void Tuya::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } From b97a728cf1e8b35c0f9e60ca9c5f7a50fdd5c224 Mon Sep 17 00:00:00 2001 From: Cody Cutrer <cody@cutrer.us> Date: Mon, 9 Feb 2026 20:40:44 -0700 Subject: [PATCH 190/251] [ld2450] add on_data callback (#13601) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/ld2450/__init__.py | 13 ++++++++++++- esphome/components/ld2450/ld2450.cpp | 6 ++++++ esphome/components/ld2450/ld2450.h | 12 ++++++++++++ tests/components/ld2450/common.yaml | 3 +++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index bd6d697c90..5854a5794c 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -1,7 +1,8 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_THROTTLE +from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -11,6 +12,8 @@ MULTI_CONF = True ld2450_ns = cg.esphome_ns.namespace("ld2450") LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) +LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template()) + CONF_LD2450_ID = "ld2450_id" CONFIG_SCHEMA = cv.All( @@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_THROTTLE): cv.invalid( f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), + cv.Optional(CONF_ON_DATA): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger), + } + ), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -45,3 +53,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) + for conf in config.get(CONF_ON_DATA, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index b04b509a16..1ea5c18271 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -413,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() { this->set_timeout(1500, [this]() { this->read_all_info(); }); } +void LD2450Component::add_on_data_callback(std::function<void()> &&callback) { + this->data_callback_.add(std::move(callback)); +} + // Send command with values to LD2450 void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { ESP_LOGV(TAG, "Sending COMMAND %02X", command); @@ -613,6 +617,8 @@ void LD2450Component::handle_periodic_data_() { this->still_presence_millis_ = App.get_loop_component_start_time(); } #endif + + this->data_callback_.call(); } bool LD2450Component::handle_ack_data_() { diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index b94c3cac37..fe69cd81d0 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -141,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice { int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2); + /// Add a callback that will be called after each successfully processed periodic data frame. + void add_on_data_callback(std::function<void()> &&callback); + protected: void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void set_config_mode_(bool enable); @@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice { #ifdef USE_TEXT_SENSOR std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{}; #endif + + LazyCallbackManager<void()> data_callback_; +}; + +class LD2450DataTrigger : public Trigger<> { + public: + explicit LD2450DataTrigger(LD2450Component *parent) { + parent->add_on_data_callback([this]() { this->trigger(); }); + } }; } // namespace esphome::ld2450 diff --git a/tests/components/ld2450/common.yaml b/tests/components/ld2450/common.yaml index cfa3c922fc..617228ca34 100644 --- a/tests/components/ld2450/common.yaml +++ b/tests/components/ld2450/common.yaml @@ -1,5 +1,8 @@ ld2450: - id: ld2450_radar + on_data: + then: + - logger.log: "LD2450 Radar Data Received" button: - platform: ld2450 From 5caed68cd9a9a36e9d3fb805bb2e464dcfe18e73 Mon Sep 17 00:00:00 2001 From: tronikos <tronikos@users.noreply.github.com> Date: Tue, 10 Feb 2026 03:36:56 -0800 Subject: [PATCH 191/251] [api] Deprecate WATER_HEATER_COMMAND_HAS_STATE (#13892) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/api/api.proto | 4 +++- esphome/components/api/api_connection.cpp | 6 +++++- esphome/components/api/api_pb2.h | 2 ++ esphome/components/api/api_pb2_dump.cpp | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d25934c60b..18dac6a2d1 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1155,9 +1155,11 @@ enum WaterHeaterCommandHasField { WATER_HEATER_COMMAND_HAS_NONE = 0; WATER_HEATER_COMMAND_HAS_MODE = 1; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2; - WATER_HEATER_COMMAND_HAS_STATE = 4; + WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true]; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16; + WATER_HEATER_COMMAND_HAS_ON_STATE = 32; + WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64; } message WaterHeaterCommandRequest { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ddc24a7e2c..c00f413e67 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1343,8 +1343,12 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH) call.set_target_temperature_high(msg.target_temperature_high); - if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) { + if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) || + (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) { call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0); + } + if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) || + (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) { call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0); } call.perform(); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 15819da172..d001f869c5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -147,6 +147,8 @@ enum WaterHeaterCommandHasField : uint32_t { WATER_HEATER_COMMAND_HAS_STATE = 4, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16, + WATER_HEATER_COMMAND_HAS_ON_STATE = 32, + WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64, }; #ifdef USE_NUMBER enum NumberMode : uint32_t { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index f1e3bdcafe..73690610ed 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -385,6 +385,10 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; + case enums::WATER_HEATER_COMMAND_HAS_ON_STATE: + return "WATER_HEATER_COMMAND_HAS_ON_STATE"; + case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE: + return "WATER_HEATER_COMMAND_HAS_AWAY_STATE"; default: return "UNKNOWN"; } From 1c3af302991d9e907b5df7161f61a5b8dd959805 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:45:31 +0000 Subject: [PATCH 192/251] Bump aioesphomeapi from 43.14.0 to 44.0.0 (#13906) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1771867535..b8adc22013 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 -aioesphomeapi==43.14.0 +aioesphomeapi==44.0.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From e85a022c775176936ad6938edb6b1d6d41b12388 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:49:59 +0000 Subject: [PATCH 193/251] Bump esphome-dashboard from 20260110.0 to 20260210.0 (#13905) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b8adc22013..a0a29ad30a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.19 esptool==5.1.0 click==8.1.7 -esphome-dashboard==20260110.0 +esphome-dashboard==20260210.0 aioesphomeapi==44.0.0 zeroconf==0.148.0 puremagic==1.30 From e3141211c3ce78767569b39896a52b75efa4796e Mon Sep 17 00:00:00 2001 From: tronikos <tronikos@users.noreply.github.com> Date: Tue, 10 Feb 2026 04:45:18 -0800 Subject: [PATCH 194/251] [water_heater] Add On/Off and Away mode support to template platform (#13839) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../template/water_heater/__init__.py | 30 +++++++ .../template/water_heater/automation.h | 11 ++- .../water_heater/template_water_heater.cpp | 35 +++++++- .../water_heater/template_water_heater.h | 4 + tests/components/template/common-base.yaml | 6 ++ .../fixtures/water_heater_template.yaml | 15 ++++ .../integration/test_water_heater_template.py | 89 ++++++++++++++----- 7 files changed, 164 insertions(+), 26 deletions(-) diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 5f96155fbf..71f98c826a 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import water_heater import esphome.config_validation as cv from esphome.const import ( + CONF_AWAY, CONF_ID, CONF_MODE, CONF_OPTIMISTIC, @@ -18,6 +19,7 @@ from esphome.types import ConfigType from .. import template_ns CONF_CURRENT_TEMPERATURE = "current_temperature" +CONF_IS_ON = "is_on" TemplateWaterHeater = template_ns.class_( "TemplateWaterHeater", cg.Component, water_heater.WaterHeater @@ -51,6 +53,8 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( water_heater.validate_water_heater_mode ), + cv.Optional(CONF_AWAY): cv.returning_lambda, + cv.Optional(CONF_IS_ON): cv.returning_lambda, } ) .extend(cv.COMPONENT_SCHEMA) @@ -98,6 +102,22 @@ async def to_code(config: ConfigType) -> None: if CONF_SUPPORTED_MODES in config: cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_AWAY in config: + template_ = await cg.process_lambda( + config[CONF_AWAY], + [], + return_type=cg.optional.template(bool), + ) + cg.add(var.set_away_lambda(template_)) + + if CONF_IS_ON in config: + template_ = await cg.process_lambda( + config[CONF_IS_ON], + [], + return_type=cg.optional.template(bool), + ) + cg.add(var.set_is_on_lambda(template_)) + @automation.register_action( "water_heater.template.publish", @@ -110,6 +130,8 @@ async def to_code(config: ConfigType) -> None: cv.Optional(CONF_MODE): cv.templatable( water_heater.validate_water_heater_mode ), + cv.Optional(CONF_AWAY): cv.templatable(cv.boolean), + cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean), } ), ) @@ -134,4 +156,12 @@ async def water_heater_template_publish_to_code( template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode) cg.add(var.set_mode(template_)) + if CONF_AWAY in config: + template_ = await cg.templatable(config[CONF_AWAY], args, bool) + cg.add(var.set_away(template_)) + + if CONF_IS_ON in config: + template_ = await cg.templatable(config[CONF_IS_ON], args, bool) + cg.add(var.set_is_on(template_)) + return var diff --git a/esphome/components/template/water_heater/automation.h b/esphome/components/template/water_heater/automation.h index 3dad2b85ae..d19542db41 100644 --- a/esphome/components/template/water_heater/automation.h +++ b/esphome/components/template/water_heater/automation.h @@ -11,12 +11,15 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T TEMPLATABLE_VALUE(float, current_temperature) TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode) + TEMPLATABLE_VALUE(bool, away) + TEMPLATABLE_VALUE(bool, is_on) void play(const Ts &...x) override { if (this->current_temperature_.has_value()) { this->parent_->set_current_temperature(this->current_temperature_.value(x...)); } - bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value(); + bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value() || this->away_.has_value() || + this->is_on_.has_value(); if (needs_call) { auto call = this->parent_->make_call(); if (this->target_temperature_.has_value()) { @@ -25,6 +28,12 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T if (this->mode_.has_value()) { call.set_mode(this->mode_.value(x...)); } + if (this->away_.has_value()) { + call.set_away(this->away_.value(x...)); + } + if (this->is_on_.has_value()) { + call.set_on(this->is_on_.value(x...)); + } call.perform(); } else { this->parent_->publish_state(); diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index c354deee0e..57c76286a0 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -17,7 +17,7 @@ void TemplateWaterHeater::setup() { } } if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() && - !this->mode_f_.has_value()) + !this->mode_f_.has_value() && !this->away_f_.has_value() && !this->is_on_f_.has_value()) this->disable_loop(); } @@ -32,6 +32,12 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { if (this->target_temperature_f_.has_value()) { traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE); } + if (this->away_f_.has_value()) { + traits.set_supports_away_mode(true); + } + if (this->is_on_f_.has_value()) { + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF); + } return traits; } @@ -62,6 +68,22 @@ void TemplateWaterHeater::loop() { } } + auto away = this->away_f_.call(); + if (away.has_value()) { + if (*away != this->is_away()) { + this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *away); + changed = true; + } + } + + auto is_on = this->is_on_f_.call(); + if (is_on.has_value()) { + if (*is_on != this->is_on()) { + this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *is_on); + changed = true; + } + } + if (changed) { this->publish_state(); } @@ -90,6 +112,17 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { } } + if (call.get_away().has_value()) { + if (this->optimistic_) { + this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *call.get_away()); + } + } + if (call.get_on().has_value()) { + if (this->optimistic_) { + this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *call.get_on()); + } + } + this->set_trigger_.trigger(); if (this->optimistic_) { diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index 22173209aa..045a142e40 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -24,6 +24,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { this->target_temperature_f_.set(std::forward<F>(f)); } template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); } + template<typename F> void set_away_lambda(F &&f) { this->away_f_.set(std::forward<F>(f)); } + template<typename F> void set_is_on_lambda(F &&f) { this->is_on_f_.set(std::forward<F>(f)); } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } @@ -49,6 +51,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { TemplateLambda<float> current_temperature_f_; TemplateLambda<float> target_temperature_f_; TemplateLambda<water_heater::WaterHeaterMode> mode_f_; + TemplateLambda<bool> away_f_; + TemplateLambda<bool> is_on_f_; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; water_heater::WaterHeaterModeMask supported_modes_; bool optimistic_{true}; diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index b8742f8c7b..e9ddfcf43e 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -13,6 +13,8 @@ esphome: id: template_water_heater target_temperature: 50.0 mode: ECO + away: false + is_on: true # Templated - water_heater.template.publish: @@ -20,6 +22,8 @@ esphome: current_temperature: !lambda "return 45.0;" target_temperature: !lambda "return 55.0;" mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;" + away: !lambda "return true;" + is_on: !lambda "return false;" # Test C++ API: set_template() with stateless lambda (no captures) # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. @@ -414,6 +418,8 @@ water_heater: current_temperature: !lambda "return 42.0f;" target_temperature: !lambda "return 60.0f;" mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" + away: !lambda "return false;" + is_on: !lambda "return true;" supported_modes: - "OFF" - ECO diff --git a/tests/integration/fixtures/water_heater_template.yaml b/tests/integration/fixtures/water_heater_template.yaml index 1aaded1991..0c82ff68ce 100644 --- a/tests/integration/fixtures/water_heater_template.yaml +++ b/tests/integration/fixtures/water_heater_template.yaml @@ -4,6 +4,14 @@ host: api: logger: +globals: + - id: global_away + type: bool + initial_value: "false" + - id: global_is_on + type: bool + initial_value: "true" + water_heater: - platform: template id: test_boiler @@ -11,6 +19,8 @@ water_heater: optimistic: true current_temperature: !lambda "return 45.0f;" target_temperature: !lambda "return 60.0f;" + away: !lambda "return id(global_away);" + is_on: !lambda "return id(global_is_on);" # Note: No mode lambda - we want optimistic mode changes to stick # A mode lambda would override mode changes in loop() supported_modes: @@ -22,3 +32,8 @@ water_heater: min_temperature: 30.0 max_temperature: 85.0 target_temperature_step: 0.5 + set_action: + - lambda: |- + // Sync optimistic state back to globals so lambdas reflect the change + id(global_away) = id(test_boiler).is_away(); + id(global_is_on) = id(test_boiler).is_on(); diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index 6b4a685d0d..096d4c8461 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -5,7 +5,13 @@ from __future__ import annotations import asyncio import aioesphomeapi -from aioesphomeapi import WaterHeaterInfo, WaterHeaterMode, WaterHeaterState +from aioesphomeapi import ( + WaterHeaterFeature, + WaterHeaterInfo, + WaterHeaterMode, + WaterHeaterState, + WaterHeaterStateFlag, +) import pytest from .state_utils import InitialStateHelper @@ -22,18 +28,25 @@ async def test_water_heater_template( loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: states: dict[int, aioesphomeapi.EntityState] = {} - gas_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future() - eco_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future() + state_future: asyncio.Future[WaterHeaterState] | None = None def on_state(state: aioesphomeapi.EntityState) -> None: states[state.key] = state - if isinstance(state, WaterHeaterState): - # Wait for GAS mode - if state.mode == WaterHeaterMode.GAS and not gas_mode_future.done(): - gas_mode_future.set_result(state) - # Wait for ECO mode (we start at OFF, so test transitioning to ECO) - elif state.mode == WaterHeaterMode.ECO and not eco_mode_future.done(): - eco_mode_future.set_result(state) + if ( + isinstance(state, WaterHeaterState) + and state_future is not None + and not state_future.done() + ): + state_future.set_result(state) + + async def wait_for_state(timeout: float = 5.0) -> WaterHeaterState: + """Wait for next water heater state change.""" + nonlocal state_future + state_future = loop.create_future() + try: + return await asyncio.wait_for(state_future, timeout) + finally: + state_future = None # Get entities and set up state synchronization entities, services = await client.list_entities_services() @@ -89,24 +102,52 @@ async def test_water_heater_template( f"Expected target temp 60.0, got {initial_state.target_temperature}" ) + # Verify supported features: away mode and on/off (fixture has away + is_on lambdas) + assert ( + test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE + ) != 0, "Expected SUPPORTS_AWAY_MODE in supported_features" + assert ( + test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_ON_OFF + ) != 0, "Expected SUPPORTS_ON_OFF in supported_features" + + # Verify initial state: on (is_on lambda returns true), not away (away lambda returns false) + assert (initial_state.state & WaterHeaterStateFlag.ON) != 0, ( + "Expected initial state to include ON flag" + ) + assert (initial_state.state & WaterHeaterStateFlag.AWAY) == 0, ( + "Expected initial state to not include AWAY flag" + ) + + # Test turning on away mode + client.water_heater_command(test_water_heater.key, away=True) + away_on_state = await wait_for_state() + assert (away_on_state.state & WaterHeaterStateFlag.AWAY) != 0 + # ON flag should still be set (is_on lambda returns true) + assert (away_on_state.state & WaterHeaterStateFlag.ON) != 0 + + # Test turning off away mode + client.water_heater_command(test_water_heater.key, away=False) + away_off_state = await wait_for_state() + assert (away_off_state.state & WaterHeaterStateFlag.AWAY) == 0 + assert (away_off_state.state & WaterHeaterStateFlag.ON) != 0 + + # Test turning off (on=False) + client.water_heater_command(test_water_heater.key, on=False) + off_state = await wait_for_state() + assert (off_state.state & WaterHeaterStateFlag.ON) == 0 + assert (off_state.state & WaterHeaterStateFlag.AWAY) == 0 + + # Test turning back on (on=True) + client.water_heater_command(test_water_heater.key, on=True) + on_state = await wait_for_state() + assert (on_state.state & WaterHeaterStateFlag.ON) != 0 + # Test changing to GAS mode client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS) - - try: - gas_state = await asyncio.wait_for(gas_mode_future, timeout=5.0) - except TimeoutError: - pytest.fail("GAS mode change not received within 5 seconds") - - assert isinstance(gas_state, WaterHeaterState) + gas_state = await wait_for_state() assert gas_state.mode == WaterHeaterMode.GAS # Test changing to ECO mode (from GAS) client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO) - - try: - eco_state = await asyncio.wait_for(eco_mode_future, timeout=5.0) - except TimeoutError: - pytest.fail("ECO mode change not received within 5 seconds") - - assert isinstance(eco_state, WaterHeaterState) + eco_state = await wait_for_state() assert eco_state.mode == WaterHeaterMode.ECO From d4ccc64dc05da9ebf8fbba72b02cf3443585f37e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 08:55:59 -0600 Subject: [PATCH 195/251] [http_request] Fix IDF chunked response completion detection (#13886) --- .../components/http_request/http_request.h | 46 ++++++++++++++-- .../http_request/http_request_idf.cpp | 53 +++++++++++++------ .../http_request/http_request_idf.h | 1 + 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index c88360ca78..a427cc4a05 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -103,6 +103,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st * - ESP-IDF: blocking reads, 0 only returned when all content read * - Arduino: non-blocking, 0 means "no data yet" or "all content read" * + * Chunked responses that complete in a reasonable time work correctly on both + * platforms. The limitation below applies only to *streaming* chunked + * responses where data arrives slowly over a long period. + * + * Streaming chunked responses are NOT supported (all platforms): + * The read helpers (http_read_loop_result, http_read_fully) block the main + * event loop until all response data is received. For streaming responses + * where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy), + * this starves the event loop on both ESP-IDF and Arduino. If data arrives + * just often enough to avoid the caller's timeout, the loop runs + * indefinitely. If data stops entirely, ESP-IDF fails with + * -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with + * delay(1) until the caller's timeout fires. Supporting streaming requires + * a non-blocking incremental read pattern that yields back to the event + * loop between chunks. Components that need streaming should use + * esp_http_client directly on a separate FreeRTOS task with + * esp_http_client_is_complete_data_received() for completion detection + * (see audio_reader.cpp for an example). + * + * Chunked transfer encoding - platform differences: + * - ESP-IDF HttpContainer: + * HttpContainerIDF overrides is_read_complete() to call + * esp_http_client_is_complete_data_received(), which is the + * authoritative completion check for both chunked and non-chunked + * transfers. When esp_http_client_read() returns 0 for a completed + * chunked response, read() returns 0 and is_read_complete() returns + * true, so callers get COMPLETE from http_read_loop_result(). + * + * - Arduino HttpContainer: + * Chunked responses are decoded internally (see + * HttpContainerArduino::read_chunked_()). When the final chunk arrives, + * is_chunked_ is cleared and content_length is set to bytes_read_. + * Completion is then detected via is_read_complete(), and a subsequent + * read() returns 0 to indicate "all content read" (not + * HTTP_ERROR_CONNECTION_CLOSED). + * * Use the helper functions below instead of checking return values directly: * - http_read_loop_result(): for manual loops with per-chunk processing * - http_read_fully(): for simple "read N bytes into buffer" operations @@ -204,9 +240,13 @@ class HttpContainer : public Parented<HttpRequestComponent> { size_t get_bytes_read() const { return this->bytes_read_; } - /// Check if all expected content has been read - /// For chunked responses, returns false (completion detected via read() returning error/EOF) - bool is_read_complete() const { + /// Check if all expected content has been read. + /// Base implementation handles non-chunked responses and status-code-based no-body checks. + /// Platform implementations may override for chunked completion detection: + /// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked. + /// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final + /// chunk, after which the base implementation detects completion. + virtual bool is_read_complete() const { // Per RFC 9112, these responses have no body: // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index bd12b7d123..486984a694 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -218,32 +218,50 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c return container; } +bool HttpContainerIDF::is_read_complete() const { + // Base class handles no-body status codes and non-chunked content_length completion + if (HttpContainer::is_read_complete()) { + return true; + } + // For chunked responses, use the authoritative ESP-IDF completion check + return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_); +} + // ESP-IDF HTTP read implementation (blocking mode) // // WARNING: Return values differ from BSD sockets! See http_request.h for full documentation. // // esp_http_client_read() in blocking mode returns: // > 0: bytes read -// 0: connection closed (end of stream) +// 0: all chunked data received (is_chunk_complete true) or connection closed +// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet // < 0: error // // We normalize to HttpContainer::read() contract: // > 0: bytes read -// 0: all content read (only returned when content_length is known and fully read) +// 0: all content read (for both content_length-based and chunked completion) // < 0: error/connection closed // // Note on chunked transfer encoding: // esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header). -// We handle this by skipping the content_length check when content_length is 0, -// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF -// by returning 0. +// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls +// esp_http_client_is_complete_data_received() to distinguish successful completion from +// connection errors. Callers use http_read_loop_result() which checks is_read_complete() +// to return COMPLETE for successful chunked EOF. +// +// Streaming chunked responses are not supported (see http_request.h for details). +// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN +// after its internal transport timeout (configured via timeout_ms) expires. +// This is passed through as a negative return value, which callers treat as an error. int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content (non-chunked only) - // For chunked responses (content_length == 0), esp_http_client_read() handles EOF - if (this->is_read_complete()) { + // Check if we've already read all expected content (non-chunked and no-body only). + // Use the base class check here, NOT the override: esp_http_client_is_complete_data_received() + // returns true as soon as all data arrives from the network, but data may still be in + // the client's internal buffer waiting to be consumed by esp_http_client_read(). + if (HttpContainer::is_read_complete()) { return 0; // All content read successfully } @@ -258,15 +276,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return read_len_or_error; } - // esp_http_client_read() returns 0 in two cases: - // 1. Known content_length: connection closed before all data received (error) - // 2. Chunked encoding (content_length == 0): end of stream reached (EOF) - // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct. - // For case 2, 0 indicates that all chunked data has already been delivered - // in previous successful read() calls, so treating this as a closed - // connection does not cause any loss of response data. + // esp_http_client_read() returns 0 when: + // - Known content_length: connection closed before all data received (error) + // - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF) + // + // Return 0 in both cases. Callers use http_read_loop_result() which calls + // is_read_complete() to distinguish these: + // - Chunked complete: is_read_complete() returns true (via + // esp_http_client_is_complete_data_received()), caller gets COMPLETE + // - Non-chunked incomplete: is_read_complete() returns false, caller + // eventually gets TIMEOUT (since no more data arrives) if (read_len_or_error == 0) { - return HTTP_ERROR_CONNECTION_CLOSED; + return 0; } // Negative value - error, return the actual error code for debugging diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index ad11811a8f..2a130eae58 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -16,6 +16,7 @@ class HttpContainerIDF : public HttpContainer { HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {} int read(uint8_t *buf, size_t max_len) override; void end() override; + bool is_read_complete() const override; /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt(); From 298efb53400852a09e2f8bdf16feac9a58422059 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 10 Feb 2026 08:56:31 -0600 Subject: [PATCH 196/251] [resampler] Refactor for stability and to support Sendspin (#12254) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../components/resampler/speaker/__init__.py | 20 +- .../resampler/speaker/resampler_speaker.cpp | 247 +++++++++++++----- .../resampler/speaker/resampler_speaker.h | 24 +- 3 files changed, 204 insertions(+), 87 deletions(-) diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 7036862d14..4e4705a889 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import audio, esp32, speaker +from esphome.components import audio, esp32, socket, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -34,7 +34,7 @@ def _set_stream_limits(config): return config -def _validate_audio_compatability(config): +def _validate_audio_compatibility(config): inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config) @@ -73,10 +73,13 @@ CONFIG_SCHEMA = cv.All( ) -FINAL_VALIDATE_SCHEMA = _validate_audio_compatability +FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility async def to_code(config): + # Enable wake_loop_threadsafe for immediate command processing from other tasks + socket.require_wake_loop_threadsafe() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await speaker.register_speaker(var, config) @@ -86,12 +89,11 @@ async def to_code(config): cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) - if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM): - cg.add(var.set_task_stack_in_psram(task_stack_in_psram)) - if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]: - esp32.add_idf_sdkconfig_option( - "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True - ) + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index ad61aca084..74420f906a 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -4,6 +4,8 @@ #include "esphome/components/audio/audio_resampler.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -17,13 +19,17 @@ static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; -static const uint32_t TASK_DELAY_MS = 20; static const uint32_t TASK_STACK_SIZE = 3072; +static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000; + static const char *const TAG = "resampler_speaker"; enum ResamplingEventGroupBits : uint32_t { - COMMAND_STOP = (1 << 0), // stops the resampler task + COMMAND_STOP = (1 << 0), // signals stop request + COMMAND_START = (1 << 1), // signals start request + COMMAND_FINISH = (1 << 2), // signals finish request (graceful stop) + TASK_COMMAND_STOP = (1 << 5), // signals the task to stop STATE_STARTING = (1 << 10), STATE_RUNNING = (1 << 11), STATE_STOPPING = (1 << 12), @@ -34,9 +40,16 @@ enum ResamplingEventGroupBits : uint32_t { ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; +void ResamplerSpeaker::dump_config() { + ESP_LOGCONFIG(TAG, + "Resampler Speaker:\n" + " Target Bits Per Sample: %u\n" + " Target Sample Rate: %" PRIu32 " Hz", + this->target_bits_per_sample_, this->target_sample_rate_); +} + void ResamplerSpeaker::setup() { this->event_group_ = xEventGroupCreate(); - if (this->event_group_ == nullptr) { ESP_LOGE(TAG, "Failed to create event group"); this->mark_failed(); @@ -55,81 +68,155 @@ void ResamplerSpeaker::setup() { this->audio_output_callback_(new_frames, write_timestamp); } }); + + // Start with loop disabled since no task is running and no commands are pending + this->disable_loop(); } void ResamplerSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); + // Process commands with priority: STOP > FINISH > START + // This ensures stop commands take precedence over conflicting start commands + if (event_group_bits & ResamplingEventGroupBits::COMMAND_STOP) { + if (this->state_ == speaker::STATE_RUNNING || this->state_ == speaker::STATE_STARTING) { + // Clear STOP, START, and FINISH bits - stop takes precedence + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP | + ResamplingEventGroupBits::COMMAND_START | + ResamplingEventGroupBits::COMMAND_FINISH); + this->waiting_for_output_ = false; + this->enter_stopping_state_(); + } else if (this->state_ == speaker::STATE_STOPPED) { + // Already stopped, just clear the command bits + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP | + ResamplingEventGroupBits::COMMAND_START | + ResamplingEventGroupBits::COMMAND_FINISH); + } + // Leave bits set if STATE_STOPPING - will be processed once stopped + } else if (event_group_bits & ResamplingEventGroupBits::COMMAND_FINISH) { + if (this->state_ == speaker::STATE_RUNNING) { + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH); + this->output_speaker_->finish(); + } else if (this->state_ == speaker::STATE_STOPPED) { + // Already stopped, just clear the command bit + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH); + } + // Leave bit set if transitioning states - will be processed once state allows + } else if (event_group_bits & ResamplingEventGroupBits::COMMAND_START) { + if (this->state_ == speaker::STATE_STOPPED) { + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START); + this->state_ = speaker::STATE_STARTING; + } else if (this->state_ == speaker::STATE_RUNNING) { + // Already running, just clear the command bit + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START); + } + // Leave bit set if transitioning states - will be processed once state allows + } + + // Re-read bits after command processing (enter_stopping_state_ may have set task bits) + event_group_bits = xEventGroupGetBits(this->event_group_); + if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) { - ESP_LOGD(TAG, "Starting resampler task"); + ESP_LOGD(TAG, "Starting"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING); } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); + this->status_set_error(LOG_STR("Not enough memory")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); + this->status_set_error(LOG_STR("Unsupported stream")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { - this->status_set_error(LOG_STR("Resampler task failed")); + this->status_set_error(LOG_STR("Resampler failure")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); } if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) { - ESP_LOGD(TAG, "Started resampler task"); + ESP_LOGV(TAG, "Started"); this->status_clear_error(); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING); } if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) { - ESP_LOGD(TAG, "Stopping resampler task"); + ESP_LOGV(TAG, "Stopping"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) { - if (this->delete_task_() == ESP_OK) { - ESP_LOGD(TAG, "Stopped resampler task"); - xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); - } + this->delete_task_(); + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); } switch (this->state_) { case speaker::STATE_STARTING: { - esp_err_t err = this->start_(); - if (err == ESP_OK) { - this->status_clear_error(); - this->state_ = speaker::STATE_RUNNING; - } else { - switch (err) { - case ESP_ERR_INVALID_STATE: - this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); - break; - case ESP_ERR_NO_MEM: - this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); - default: - this->status_set_error(LOG_STR("Failed to start resampler")); - break; + if (!this->waiting_for_output_) { + esp_err_t err = this->start_(); + if (err == ESP_OK) { + this->callback_remainder_ = 0; // reset callback remainder + this->status_clear_error(); + this->waiting_for_output_ = true; + this->state_start_ms_ = App.get_loop_component_start_time(); + } else { + this->set_start_error_(err); + this->waiting_for_output_ = false; + this->enter_stopping_state_(); + } + } else { + if (this->output_speaker_->is_running()) { + this->state_ = speaker::STATE_RUNNING; + this->waiting_for_output_ = false; + } else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) { + // Timed out waiting for the output speaker to start + this->waiting_for_output_ = false; + this->enter_stopping_state_(); } - - this->state_ = speaker::STATE_STOPPING; } break; } case speaker::STATE_RUNNING: if (this->output_speaker_->is_stopped()) { - this->state_ = speaker::STATE_STOPPING; + this->enter_stopping_state_(); + } + break; + case speaker::STATE_STOPPING: { + if ((this->output_speaker_->get_pause_state()) || + ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS)) { + // If output speaker is paused or stopping timeout exceeded, force stop + this->output_speaker_->stop(); } + if (this->output_speaker_->is_stopped() && (this->task_handle_ == nullptr)) { + // Only transition to stopped state once the output speaker and resampler task are fully stopped + this->waiting_for_output_ = false; + this->state_ = speaker::STATE_STOPPED; + } break; - case speaker::STATE_STOPPING: - this->stop_(); - this->state_ = speaker::STATE_STOPPED; - break; + } case speaker::STATE_STOPPED: + event_group_bits = xEventGroupGetBits(this->event_group_); + if (event_group_bits == 0) { + // No pending events, disable loop to save CPU cycles + this->disable_loop(); + } + break; + } +} + +void ResamplerSpeaker::set_start_error_(esp_err_t err) { + switch (err) { + case ESP_ERR_INVALID_STATE: + this->status_set_error(LOG_STR("Task failed to start")); + break; + case ESP_ERR_NO_MEM: + this->status_set_error(LOG_STR("Not enough memory")); + break; + default: + this->status_set_error(LOG_STR("Failed to start")); break; } } @@ -143,16 +230,33 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) { bytes_written = this->output_speaker_->play(data, length, ticks_to_wait); } else { - if (this->ring_buffer_.use_count() == 1) { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + if (temp_ring_buffer) { + // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); + } else { + // Delay to avoid repeatedly hammering while waiting for the speaker to start + vTaskDelay(ticks_to_wait); } } return bytes_written; } -void ResamplerSpeaker::start() { this->state_ = speaker::STATE_STARTING; } +void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { + this->enable_loop_soon_any_context(); + uint32_t event_bits = xEventGroupGetBits(this->event_group_); + if (!(event_bits & command_bit)) { + xEventGroupSetBits(this->event_group_, command_bit); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + if (wake_loop) { + App.wake_loop_threadsafe(); + } +#endif + } +} + +void ResamplerSpeaker::start() { this->send_command_(ResamplingEventGroupBits::COMMAND_START, true); } esp_err_t ResamplerSpeaker::start_() { this->target_stream_info_ = audio::AudioStreamInfo( @@ -185,7 +289,7 @@ esp_err_t ResamplerSpeaker::start_task_() { } if (this->task_handle_ == nullptr) { - this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this, + this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this, RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_); } @@ -196,43 +300,47 @@ esp_err_t ResamplerSpeaker::start_task_() { return ESP_OK; } -void ResamplerSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; } +void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::COMMAND_STOP); } -void ResamplerSpeaker::stop_() { +void ResamplerSpeaker::enter_stopping_state_() { + this->state_ = speaker::STATE_STOPPING; + this->state_start_ms_ = App.get_loop_component_start_time(); if (this->task_handle_ != nullptr) { - xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP); + xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP); } this->output_speaker_->stop(); } -esp_err_t ResamplerSpeaker::delete_task_() { - if (!this->task_created_) { +void ResamplerSpeaker::delete_task_() { + if (this->task_handle_ != nullptr) { + // Delete the suspended task + vTaskDelete(this->task_handle_); this->task_handle_ = nullptr; - - if (this->task_stack_buffer_ != nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } - - this->task_stack_buffer_ = nullptr; - } - - return ESP_OK; } - return ESP_ERR_INVALID_STATE; + if (this->task_stack_buffer_ != nullptr) { + // Deallocate the task stack buffer + if (this->task_stack_in_psram_) { + RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL); + stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); + } else { + RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL); + stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); + } + + this->task_stack_buffer_ = nullptr; + } } -void ResamplerSpeaker::finish() { this->output_speaker_->finish(); } +void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); } bool ResamplerSpeaker::has_buffered_data() const { bool has_ring_buffer_data = false; - if (this->requires_resampling_() && (this->ring_buffer_.use_count() > 0)) { - has_ring_buffer_data = (this->ring_buffer_.lock()->available() > 0); + if (this->requires_resampling_()) { + std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + if (temp_ring_buffer) { + has_ring_buffer_data = (temp_ring_buffer->available() > 0); + } } return (has_ring_buffer_data || this->output_speaker_->has_buffered_data()); } @@ -253,9 +361,8 @@ bool ResamplerSpeaker::requires_resampling_() const { } void ResamplerSpeaker::resample_task(void *params) { - ResamplerSpeaker *this_resampler = (ResamplerSpeaker *) params; + ResamplerSpeaker *this_resampler = static_cast<ResamplerSpeaker *>(params); - this_resampler->task_created_ = true; xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING); std::unique_ptr<audio::AudioResampler> resampler = @@ -269,7 +376,7 @@ void ResamplerSpeaker::resample_task(void *params) { std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); - if (temp_ring_buffer.use_count() == 0) { + if (!temp_ring_buffer) { err = ESP_ERR_NO_MEM; } else { this_resampler->ring_buffer_ = temp_ring_buffer; @@ -291,7 +398,7 @@ void ResamplerSpeaker::resample_task(void *params) { while (err == ESP_OK) { uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); - if (event_bits & ResamplingEventGroupBits::COMMAND_STOP) { + if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) { break; } @@ -310,8 +417,8 @@ void ResamplerSpeaker::resample_task(void *params) { xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING); resampler.reset(); xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED); - this_resampler->task_created_ = false; - vTaskDelete(nullptr); + + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } } // namespace resampler diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index 810087ab7f..c1ebd7e7b5 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -8,14 +8,16 @@ #include "esphome/core/component.h" -#include <freertos/event_groups.h> #include <freertos/FreeRTOS.h> +#include <freertos/event_groups.h> namespace esphome { namespace resampler { class ResamplerSpeaker : public Component, public speaker::Speaker { public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + void dump_config() override; void setup() override; void loop() override; @@ -65,13 +67,18 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { /// ESP_ERR_INVALID_STATE if the task wasn't created esp_err_t start_task_(); - /// @brief Stops the output speaker. If the resampling task is running, it sends the stop command. - void stop_(); + /// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is + /// running, and stops the output speaker. + void enter_stopping_state_(); - /// @brief Deallocates the task stack and resets the pointers. - /// @return ESP_OK if successful - /// ESP_ERR_INVALID_STATE if the task hasn't stopped itself - esp_err_t delete_task_(); + /// @brief Sets the appropriate status error based on the start failure reason. + void set_start_error_(esp_err_t err); + + /// @brief Deletes the resampler task if suspended, deallocates the task stack, and resets the related pointers. + void delete_task_(); + + /// @brief Sends a command via event group bits, enables the loop, and optionally wakes the main loop. + void send_command_(uint32_t command_bit, bool wake_loop = false); inline bool requires_resampling_() const; static void resample_task(void *params); @@ -83,7 +90,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { speaker::Speaker *output_speaker_{nullptr}; bool task_stack_in_psram_{false}; - bool task_created_{false}; + bool waiting_for_output_{false}; TaskHandle_t task_handle_{nullptr}; StaticTask_t task_stack_; @@ -98,6 +105,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { uint32_t target_sample_rate_; uint32_t buffer_duration_ms_; + uint32_t state_start_ms_{0}; uint64_t callback_remainder_{0}; }; From 13a124c86d3c6944d4fb27462c4a98a2bc553881 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:10:27 -0500 Subject: [PATCH 197/251] [pulse_counter] Migrate from legacy PCNT API to new ESP-IDF 5.x API (#13904) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 1 + esphome/components/hlw8012/sensor.py | 5 +- .../pulse_counter/pulse_counter_sensor.cpp | 113 ++++++++++-------- .../pulse_counter/pulse_counter_sensor.h | 21 ++-- esphome/components/pulse_counter/sensor.py | 5 +- 5 files changed, 72 insertions(+), 73 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6f5011246c..8c052acc87 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -135,6 +135,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_i2s", # I2S driver - only needed by i2s_audio component "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM + "esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 4727877633..1d793ac6b1 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -94,10 +94,7 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): if CORE.is_esp32: - # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) - # HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h - # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) - include_builtin_idf_component("driver") + include_builtin_idf_component("esp_driver_pcnt") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index c0d74cef4a..ef4cc980f6 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -1,6 +1,11 @@ #include "pulse_counter_sensor.h" #include "esphome/core/log.h" +#ifdef HAS_PCNT +#include <esp_private/esp_clk.h> +#include <hal/pcnt_ll.h> +#endif + namespace esphome { namespace pulse_counter { @@ -56,103 +61,107 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() { #ifdef HAS_PCNT bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { - static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; - static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0; this->pin = pin; this->pin->setup(); - this->pcnt_unit = next_pcnt_unit; - this->pcnt_channel = next_pcnt_channel; - next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); - if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) { - next_pcnt_unit = PCNT_UNIT_0; - next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1); + + pcnt_unit_config_t unit_config = { + .low_limit = INT16_MIN, + .high_limit = INT16_MAX, + .flags = {.accum_count = true}, + }; + esp_err_t error = pcnt_new_unit(&unit_config, &this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Creating PCNT unit failed: %s", esp_err_to_name(error)); + return false; } - ESP_LOGCONFIG(TAG, - " PCNT Unit Number: %u\n" - " PCNT Channel Number: %u", - this->pcnt_unit, this->pcnt_channel); + pcnt_chan_config_t chan_config = { + .edge_gpio_num = this->pin->get_pin(), + .level_gpio_num = -1, + }; + error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Creating PCNT channel failed: %s", esp_err_to_name(error)); + return false; + } - pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS; + pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD; + pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD; switch (this->rising_edge_mode) { case PULSE_COUNTER_DISABLE: - rising = PCNT_COUNT_DIS; + rising = PCNT_CHANNEL_EDGE_ACTION_HOLD; break; case PULSE_COUNTER_INCREMENT: - rising = PCNT_COUNT_INC; + rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE; break; case PULSE_COUNTER_DECREMENT: - rising = PCNT_COUNT_DEC; + rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE; break; } switch (this->falling_edge_mode) { case PULSE_COUNTER_DISABLE: - falling = PCNT_COUNT_DIS; + falling = PCNT_CHANNEL_EDGE_ACTION_HOLD; break; case PULSE_COUNTER_INCREMENT: - falling = PCNT_COUNT_INC; + falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE; break; case PULSE_COUNTER_DECREMENT: - falling = PCNT_COUNT_DEC; + falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE; break; } - pcnt_config_t pcnt_config = { - .pulse_gpio_num = this->pin->get_pin(), - .ctrl_gpio_num = PCNT_PIN_NOT_USED, - .lctrl_mode = PCNT_MODE_KEEP, - .hctrl_mode = PCNT_MODE_KEEP, - .pos_mode = rising, - .neg_mode = falling, - .counter_h_lim = 0, - .counter_l_lim = 0, - .unit = this->pcnt_unit, - .channel = this->pcnt_channel, - }; - esp_err_t error = pcnt_unit_config(&pcnt_config); + error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling); if (error != ESP_OK) { - ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error)); return false; } if (this->filter_us != 0) { - uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u); - ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val); - error = pcnt_set_filter_value(this->pcnt_unit, filter_val); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + pcnt_glitch_filter_config_t filter_config = { + .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), + }; + error = pcnt_unit_set_glitch_filter(this->pcnt_unit, &filter_config); if (error != ESP_OK) { - ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error)); - return false; - } - error = pcnt_filter_enable(this->pcnt_unit); - if (error != ESP_OK) { - ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Setting PCNT glitch filter failed: %s", esp_err_to_name(error)); return false; } } - error = pcnt_counter_pause(this->pcnt_unit); + error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN); if (error != ESP_OK) { - ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error)); return false; } - error = pcnt_counter_clear(this->pcnt_unit); + error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX); if (error != ESP_OK) { - ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error)); return false; } - error = pcnt_counter_resume(this->pcnt_unit); + + error = pcnt_unit_enable(this->pcnt_unit); if (error != ESP_OK) { - ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error)); + ESP_LOGE(TAG, "Enabling PCNT unit failed: %s", esp_err_to_name(error)); + return false; + } + error = pcnt_unit_clear_count(this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Clearing PCNT unit failed: %s", esp_err_to_name(error)); + return false; + } + error = pcnt_unit_start(this->pcnt_unit); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Starting PCNT unit failed: %s", esp_err_to_name(error)); return false; } return true; } pulse_counter_t HwPulseCounterStorage::read_raw_value() { - pulse_counter_t counter; - pcnt_get_counter_value(this->pcnt_unit, &counter); - pulse_counter_t ret = counter - this->last_value; - this->last_value = counter; + int count; + pcnt_unit_get_count(this->pcnt_unit, &count); + pulse_counter_t ret = count - this->last_value; + this->last_value = count; return ret; } #endif // HAS_PCNT diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index f906e9e5cb..a7913d5d66 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -6,14 +6,13 @@ #include <cinttypes> -// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h) -// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the -// "driver" IDF component dependency. See: -// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6 -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) -#include <driver/pcnt.h> +#if defined(USE_ESP32) +#include <soc/soc_caps.h> +#ifdef SOC_PCNT_SUPPORTED +#include <driver/pulse_cnt.h> #define HAS_PCNT -#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#endif // SOC_PCNT_SUPPORTED +#endif // USE_ESP32 namespace esphome { namespace pulse_counter { @@ -24,11 +23,7 @@ enum PulseCounterCountMode { PULSE_COUNTER_DECREMENT, }; -#ifdef HAS_PCNT -using pulse_counter_t = int16_t; -#else // HAS_PCNT using pulse_counter_t = int32_t; -#endif // HAS_PCNT struct PulseCounterStorageBase { virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0; @@ -58,8 +53,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase { bool pulse_counter_setup(InternalGPIOPin *pin) override; pulse_counter_t read_raw_value() override; - pcnt_unit_t pcnt_unit; - pcnt_channel_t pcnt_channel; + pcnt_unit_handle_t pcnt_unit{nullptr}; + pcnt_channel_handle_t pcnt_channel{nullptr}; }; #endif // HAS_PCNT diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 65be5ee793..0124463567 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -129,10 +129,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): use_pcnt = config.get(CONF_USE_PCNT) if CORE.is_esp32 and use_pcnt: - # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) - # Provides driver/pcnt.h header for hardware pulse counter API - # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) - include_builtin_idf_component("driver") + include_builtin_idf_component("esp_driver_pcnt") var = await sensor.new_sensor(config, use_pcnt) await cg.register_component(var, config) From 03b41855f5137bf54adc859084baf5a1171ad92a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:03:26 -0500 Subject: [PATCH 198/251] [esp32_hosted] Bump esp_wifi_remote and esp_hosted versions (#13911) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32_hosted/__init__.py | 4 ++-- esphome/idf_component.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 170f436f02..287c780769 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -95,9 +95,9 @@ async def to_code(config): framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" if framework_ver >= cv.Version(5, 5, 0): - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4") + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index b4a2c9b909..f39ea9b3ae 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: espressif/mdns: version: 1.9.1 espressif/esp_wifi_remote: - version: 1.2.4 + version: 1.3.2 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: @@ -18,7 +18,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.9.3 + version: 2.11.5 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From c4b109eebd223ea73736bd85d972362fb5c81ad0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:09:56 -0500 Subject: [PATCH 199/251] [esp32_rmt_led_strip, remote_receiver, pulse_counter] Replace hardcoded clock frequencies with runtime queries (#13908) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../esp32_rmt_led_strip/led_strip.cpp | 23 +++++++++++-------- .../pulse_counter/pulse_counter_sensor.cpp | 6 +++-- .../remote_receiver/remote_receiver_esp32.cpp | 11 ++++----- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 4ca0b998b1..8bb5cbb62e 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -7,22 +7,25 @@ #include "esphome/core/log.h" #include <esp_attr.h> +#include <esp_clk_tree.h> namespace esphome { namespace esp32_rmt_led_strip { static const char *const TAG = "esp32_rmt_led_strip"; -#ifdef USE_ESP32_VARIANT_ESP32H2 -static const uint32_t RMT_CLK_FREQ = 32000000; -static const uint8_t RMT_CLK_DIV = 1; -#else -static const uint32_t RMT_CLK_FREQ = 80000000; -static const uint8_t RMT_CLK_DIV = 2; -#endif - static const size_t RMT_SYMBOLS_PER_BYTE = 8; +// Query the RMT default clock source frequency. This varies by variant: +// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2. +// Worst-case reset time is WS2811 at 300µs = 24000 ticks at 80MHz, well within +// the 15-bit rmt_symbol_word_t duration field max of 32767. +static uint32_t rmt_resolution_hz() { + uint32_t freq; + esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); + return freq; +} + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free, rmt_symbol_word_t *symbols, bool *done, void *arg) { @@ -92,7 +95,7 @@ void ESP32RMTLEDStripLightOutput::setup() { rmt_tx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); channel.clk_src = RMT_CLK_SRC_DEFAULT; - channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV; + channel.resolution_hz = rmt_resolution_hz(); channel.gpio_num = gpio_num_t(this->pin_); channel.mem_block_symbols = this->rmt_symbols_; channel.trans_queue_depth = 1; @@ -137,7 +140,7 @@ void ESP32RMTLEDStripLightOutput::setup() { void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) { - float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; + float ratio = (float) rmt_resolution_hz() / 1e09f; // 0-bit this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high); diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ef4cc980f6..8ac5a28d8f 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT -#include <esp_private/esp_clk.h> +#include <esp_clk_tree.h> #include <hal/pcnt_ll.h> #endif @@ -117,7 +117,9 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + uint32_t apb_freq; + esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index eda8365169..f95244ea45 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -3,15 +3,11 @@ #ifdef USE_ESP32 #include <driver/gpio.h> +#include <esp_clk_tree.h> namespace esphome::remote_receiver { static const char *const TAG = "remote_receiver.esp32"; -#ifdef USE_ESP32_VARIANT_ESP32H2 -static const uint32_t RMT_CLK_FREQ = 32000000; -#else -static const uint32_t RMT_CLK_FREQ = 80000000; -#endif static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; @@ -98,7 +94,10 @@ void RemoteReceiverComponent::setup() { } uint32_t event_size = sizeof(rmt_rx_done_event_data_t); - uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000); + uint32_t rmt_freq; + esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, + &rmt_freq); + uint32_t max_filter_ns = UINT8_MAX * 1000u / (rmt_freq / 1000000); memset(&this->store_.config, 0, sizeof(this->store_.config)); this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns); this->store_.config.signal_range_max_ns = this->idle_us_ * 1000; From b8ec3aab1d2cc36a96902aa39cae9331a02f1f93 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:16:25 -0500 Subject: [PATCH 200/251] [ci] Pin ESP-IDF version for Arduino framework builds (#13909) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .clang-tidy.hash | 2 +- platformio.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 63ddbbac05..37f82e2755 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced +8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695 diff --git a/platformio.ini b/platformio.ini index d198862a25..94b1f1a727 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,6 +136,7 @@ extends = common:arduino 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.6/esp32-core-3.3.6.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = From 2585779f11daab1b41fd333382c0f2dcd8bd174e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 12:23:16 -0600 Subject: [PATCH 201/251] [api] Remove duplicate peername storage to save RAM (#13540) --- esphome/components/api/api_connection.cpp | 30 ++++++++++++------- esphome/components/api/api_connection.h | 6 ++-- esphome/components/api/api_frame_helper.cpp | 18 +++++++++-- esphome/components/api/api_frame_helper.h | 9 +++--- .../components/api/api_frame_helper_noise.cpp | 7 ++++- .../api/api_frame_helper_plaintext.cpp | 7 ++++- esphome/components/api/api_server.cpp | 8 +++-- .../voice_assistant/voice_assistant.cpp | 6 ++-- 8 files changed, 65 insertions(+), 26 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c00f413e67..e51689fd07 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -133,8 +133,8 @@ void APIConnection::start() { return; } // Initialize client name with peername (IP address) until Hello message provides actual name - const char *peername = this->helper_->get_client_peername(); - this->helper_->set_client_name(peername, strlen(peername)); + char peername[socket::SOCKADDR_STR_LEN]; + this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername)); } APIConnection::~APIConnection() { @@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) { void APIConnection::loop() { if (this->flags_.next_close) { - // requested a disconnect - this->helper_->close(); + // requested a disconnect - don't close socket here, let APIServer::loop() do it + // so getpeername() still works for the disconnect trigger this->flags_.remove = true; return; } @@ -293,7 +293,8 @@ bool APIConnection::send_disconnect_response_() { return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); } void APIConnection::on_disconnect_response() { - this->helper_->close(); + // Don't close socket here, let APIServer::loop() do it + // so getpeername() still works for the disconnect trigger this->flags_.remove = true; } @@ -1469,8 +1470,11 @@ void APIConnection::complete_authentication_() { this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), - std::string(this->helper_->get_client_peername())); + { + char peername[socket::SOCKADDR_STR_LEN]; + this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), + std::string(this->helper_->get_peername_to(peername))); + } #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1489,8 +1493,9 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size()); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; + char peername[socket::SOCKADDR_STR_LEN]; ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), - this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_); + this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; @@ -1838,7 +1843,8 @@ void APIConnection::on_no_setup_connection() { this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); } void APIConnection::on_fatal_error() { - this->helper_->close(); + // Don't close socket here - keep it open so getpeername() works for logging + // Socket will be closed when client is removed from the list in APIServer::loop() this->flags_.remove = true; } @@ -2195,12 +2201,14 @@ void APIConnection::process_state_subscriptions_() { #endif // USE_API_HOMEASSISTANT_STATES void APIConnection::log_client_(int level, const LogString *message) { + char peername[socket::SOCKADDR_STR_LEN]; esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(), - this->helper_->get_client_peername(), LOG_STR_ARG(message)); + this->helper_->get_peername_to(peername), LOG_STR_ARG(message)); } void APIConnection::log_warning_(const LogString *message, APIError err) { - ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(), + char peername[socket::SOCKADDR_STR_LEN]; + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername), LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 7f738a9bfd..abcb162865 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -276,8 +276,10 @@ class APIConnection final : public APIServerConnectionBase { bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; const char *get_name() const { return this->helper_->get_client_name(); } - /// Get peer name (IP address) - cached at connection init time - const char *get_peername() const { return this->helper_->get_client_peername(); } + /// Get peer name (IP address) into caller-provided buffer, returns buf for convenience + const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const { + return this->helper_->get_peername_to(buf); + } protected: // Helper function to handle authentication completion diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index dd44fe9e17..e432a976b0 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper"; static constexpr size_t API_MAX_LOG_BYTES = 168; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + do { \ + char peername_buf[socket::SOCKADDR_STR_LEN]; \ + this->get_peername_to(peername_buf); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \ + } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) #endif @@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() { return APIError::OK; // All buffers sent successfully } +const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const { + if (this->socket_) { + this->socket_->getpeername_to(buf); + } else { + buf[0] = '\0'; + } + return buf.data(); +} + APIError APIFrameHelper::init_common_() { if (state_ != State::INITIALIZE || this->socket_ == nullptr) { HELPER_LOG("Bad state for init %d", (int) state_); return APIError::BAD_STATE; } - // Cache peername now while socket is valid - needed for error logging after socket failure - this->socket_->getpeername_to(this->client_peername_); int err = this->socket_->setblocking(false); if (err != 0) { state_ = State::FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index f311e34fd7..03f3814bb9 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -90,8 +90,9 @@ class APIFrameHelper { // Get client name (null-terminated) const char *get_client_name() const { return this->client_name_; } - // Get client peername/IP (null-terminated, cached at init time for availability after socket failure) - const char *get_client_peername() const { return this->client_peername_; } + // Get client peername/IP into caller-provided buffer (fetches on-demand from socket) + // Returns pointer to buf for convenience in printf-style calls + const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const; // Set client name from buffer with length (truncates if needed) void set_client_name(const char *name, size_t len) { size_t copy_len = std::min(len, sizeof(this->client_name_) - 1); @@ -105,6 +106,8 @@ class APIFrameHelper { bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { + if (state_ == State::CLOSED) + return APIError::OK; // Already closed state_ = State::CLOSED; int err = this->socket_->close(); if (err == -1) @@ -231,8 +234,6 @@ class APIFrameHelper { // Client name buffer - stores name from Hello message or initial peername char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; - // Cached peername/IP address - captured at init time for availability after socket failure - char client_peername_[socket::SOCKADDR_STR_LEN]{}; // Group smaller types together uint16_t rx_buf_len_ = 0; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 4a9257231d..c1641b398a 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") static constexpr size_t API_MAX_LOG_BYTES = 168; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + do { \ + char peername_buf[socket::SOCKADDR_STR_LEN]; \ + this->get_peername_to(peername_buf); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \ + } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) #endif diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 3dfd683929..ed3cc8934e 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext"; static constexpr size_t API_MAX_LOG_BYTES = 168; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + do { \ + char peername_buf[socket::SOCKADDR_STR_LEN]; \ + this->get_peername_to(peername_buf); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \ + } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index c56449455d..53b41a5c14 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -192,11 +192,15 @@ void APIServer::loop() { ESP_LOGV(TAG, "Remove connection %s", client->get_name()); #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Save client info before removal for the trigger + // Save client info before closing socket and removal for the trigger + char peername_buf[socket::SOCKADDR_STR_LEN]; std::string client_name(client->get_name()); - std::string client_peername(client->get_peername()); + std::string client_peername(client->get_peername_to(peername_buf)); #endif + // Close socket now (was deferred from on_fatal_error to allow getpeername) + client->helper_->close(); + // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { std::swap(this->clients_[client_index], this->clients_.back()); diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 6267d97480..641d4d6ff8 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -430,12 +430,14 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr } if (this->api_client_ != nullptr) { + char current_peername[socket::SOCKADDR_STR_LEN]; + char new_peername[socket::SOCKADDR_STR_LEN]; ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant\n" "Current client: %s (%s)\n" "New client: %s (%s)", - this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(), - client->get_peername()); + this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(), + client->get_peername_to(new_peername)); return; } From eea7e9edffe8bdf321c3e8042c0c7614b926b16b Mon Sep 17 00:00:00 2001 From: Jas Strong <jasmine@electronpusher.org> Date: Thu, 5 Feb 2026 00:06:52 -0800 Subject: [PATCH 202/251] [rd03d] Revert incorrect field order swap (#13769) Co-authored-by: jas <jas@asspa.in> --- esphome/components/rd03d/rd03d.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index ba05abe8e0..090e4dcf32 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -132,18 +132,15 @@ void RD03DComponent::process_frame_() { // Header is 4 bytes, each target is 8 bytes uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); - // Extract raw bytes for this target - // Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution, - // actual radar output has Resolution before Speed (verified empirically - - // stationary targets were showing non-zero speed with original field order) + // Extract raw bytes for this target (per datasheet Table 5-2: X, Y, Speed, Resolution) uint8_t x_low = this->buffer_[offset + 0]; uint8_t x_high = this->buffer_[offset + 1]; uint8_t y_low = this->buffer_[offset + 2]; uint8_t y_high = this->buffer_[offset + 3]; - uint8_t res_low = this->buffer_[offset + 4]; - uint8_t res_high = this->buffer_[offset + 5]; - uint8_t speed_low = this->buffer_[offset + 6]; - uint8_t speed_high = this->buffer_[offset + 7]; + uint8_t speed_low = this->buffer_[offset + 4]; + uint8_t speed_high = this->buffer_[offset + 5]; + uint8_t res_low = this->buffer_[offset + 6]; + uint8_t res_high = this->buffer_[offset + 7]; // Decode values per RD-03D format int16_t x = decode_value(x_low, x_high); From 9eee4c992450bcfaec1e7516f96ccacad7b09a4f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:00:16 -0500 Subject: [PATCH 203/251] [core] Add capacity check to register_component_ (#13778) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/core/application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 55eb25ce09..76ce976131 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -81,6 +81,10 @@ void Application::register_component_(Component *comp) { return; } } + if (this->components_.size() >= ESPHOME_COMPONENT_COUNT) { + ESP_LOGE(TAG, "Cannot register component %s - at capacity!", LOG_STR_ARG(comp->get_component_log_str())); + return; + } this->components_.push_back(comp); } void Application::setup() { From 438a0c428985b9d8c37ac2e036d7a955809a8793 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:09:48 -0500 Subject: [PATCH 204/251] [ota] Fix CLI upload option shown when only http_request platform configured (#13784) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --- esphome/__main__.py | 9 +++- tests/unit_tests/test_main.py | 85 ++++++++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 6cec481abc..e49a1eea9d 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -287,8 +287,13 @@ def has_api() -> bool: def has_ota() -> bool: - """Check if OTA is available.""" - return CONF_OTA in CORE.config + """Check if OTA upload is available (requires platform: esphome).""" + if CONF_OTA not in CORE.config: + return False + return any( + ota_item.get(CONF_PLATFORM) == CONF_ESPHOME + for ota_item in CORE.config[CONF_OTA] + ) def has_mqtt_ip_lookup() -> bool: diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3268f7ee87..c9aa446323 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -32,6 +32,7 @@ from esphome.__main__ import ( has_mqtt_ip_lookup, has_mqtt_logging, has_non_ip_address, + has_ota, has_resolvable_address, mqtt_get_ip, run_esphome, @@ -332,7 +333,9 @@ def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None: def test_choose_upload_log_host_with_ota_list() -> None: """Test with OTA as the only item in the list.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=["OTA"], @@ -345,7 +348,7 @@ def test_choose_upload_log_host_with_ota_list() -> None: @pytest.mark.usefixtures("mock_has_mqtt_logging") def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None: """Test with OTA list falling back to MQTT when no address.""" - setup_core(config={CONF_OTA: {}, "mqtt": {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], "mqtt": {}}) result = choose_upload_log_host( default=["OTA"], @@ -408,7 +411,9 @@ def test_choose_upload_log_host_with_serial_device_with_ports( def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None: """Test OTA device when OTA is configured.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default="OTA", @@ -475,7 +480,9 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None: @pytest.mark.usefixtures("mock_choose_prompt") def test_choose_upload_log_host_multiple_devices() -> None: """Test with multiple devices including special identifiers.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")] @@ -514,7 +521,9 @@ def test_choose_upload_log_host_no_defaults_with_serial_ports( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_no_defaults_with_ota() -> None: """Test interactive mode with OTA option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) with patch( "esphome.__main__.choose_prompt", return_value="192.168.1.100" @@ -575,7 +584,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -604,7 +617,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -632,7 +649,9 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_check_default_matches() -> None: """Test when check_default matches an available option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=None, @@ -704,7 +723,10 @@ def test_choose_upload_log_host_mixed_resolved_unresolved() -> None: def test_choose_upload_log_host_ota_both_conditions() -> None: """Test OTA device when both OTA and API are configured and enabled.""" - setup_core(config={CONF_OTA: {}, CONF_API: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}}, + address="192.168.1.100", + ) result = choose_upload_log_host( default="OTA", @@ -719,7 +741,7 @@ def test_choose_upload_log_host_ota_ip_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -744,7 +766,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -769,7 +791,7 @@ def test_choose_upload_log_host_ota_ip_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -794,7 +816,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -817,7 +839,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: @pytest.mark.usefixtures("mock_no_mqtt_logging") def test_choose_upload_log_host_no_address_with_ota_config() -> None: """Test OTA device when OTA is configured but no address is set.""" - setup_core(config={CONF_OTA: {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) with pytest.raises( EsphomeError, match="All specified devices .* could not be resolved" @@ -1532,10 +1554,43 @@ def test_has_mqtt() -> None: assert has_mqtt() is False # Test with other components but no MQTT - setup_core(config={CONF_API: {}, CONF_OTA: {}}) + setup_core(config={CONF_API: {}, CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) assert has_mqtt() is False +def test_has_ota() -> None: + """Test has_ota function. + + The has_ota function should only return True when OTA is configured + with platform: esphome, not when only platform: http_request is configured. + This is because CLI OTA upload only works with the esphome platform. + """ + # Test with OTA esphome platform configured + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) + assert has_ota() is True + + # Test with OTA http_request platform only (should return False) + # This is the bug scenario from issue #13783 + setup_core(config={CONF_OTA: [{CONF_PLATFORM: "http_request"}]}) + assert has_ota() is False + + # Test without OTA configured + setup_core(config={}) + assert has_ota() is False + + # Test with multiple OTA platforms including esphome + setup_core( + config={ + CONF_OTA: [{CONF_PLATFORM: "http_request"}, {CONF_PLATFORM: CONF_ESPHOME}] + } + ) + assert has_ota() is True + + # Test with empty OTA list + setup_core(config={CONF_OTA: []}) + assert has_ota() is False + + def test_get_port_type() -> None: """Test get_port_type function.""" From c1455ccc29815b20aa814c82b14cf23c2fac8ca1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Feb 2026 22:19:20 +0100 Subject: [PATCH 205/251] [dashboard] Close WebSocket after process exit to prevent zombie connections (#13834) --- esphome/dashboard/web_server.py | 1 + tests/dashboard/test_web_server.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index f94d8eea22..da50279864 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -317,6 +317,7 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl # Check if the proc was not forcibly closed _LOGGER.info("Process exited with return code %s", returncode) self.write_message({"event": "exit", "code": returncode}) + self.close() def on_close(self) -> None: # Check if proc exists (if 'start' has been run) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 10ca6061e6..7642876ee5 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -29,7 +29,7 @@ from esphome.dashboard.entries import ( bool_to_entry_state, ) from esphome.dashboard.models import build_importable_device_dict -from esphome.dashboard.web_server import DashboardSubscriber +from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket from esphome.zeroconf import DiscoveredImport from .common import get_fixture_path @@ -1654,3 +1654,25 @@ async def test_websocket_check_origin_multiple_trusted_domains( assert data["event"] == "initial_state" finally: ws.close() + + +def test_proc_on_exit_calls_close() -> None: + """Test _proc_on_exit sends exit event and closes the WebSocket.""" + handler = Mock(spec=EsphomeCommandWebSocket) + handler._is_closed = False + + EsphomeCommandWebSocket._proc_on_exit(handler, 0) + + handler.write_message.assert_called_once_with({"event": "exit", "code": 0}) + handler.close.assert_called_once() + + +def test_proc_on_exit_skips_when_already_closed() -> None: + """Test _proc_on_exit does nothing when WebSocket is already closed.""" + handler = Mock(spec=EsphomeCommandWebSocket) + handler._is_closed = True + + EsphomeCommandWebSocket._proc_on_exit(handler, 0) + + handler.write_message.assert_not_called() + handler.close.assert_not_called() From a5dc4b0fce2c0b2b279abb6cc8eaa49b642d19d4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Feb 2026 18:52:05 +0100 Subject: [PATCH 206/251] [nrf52,logger] fix printk (#13874) --- esphome/components/logger/logger_zephyr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 41f53beec0..c0fb5c502b 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { #ifdef CONFIG_PRINTK // Requires the debug component and an active SWD connection. // It is used for pyocd rtt -t nrf52840 - k_str_out(const_cast<char *>(msg), len); + printk("%.*s", static_cast<int>(len), msg); #endif if (this->uart_dev_ == nullptr) { return; From 0b047c334d3142753169ccb308ae2dda0a7c32b6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:53:43 +1100 Subject: [PATCH 207/251] [lvgl] Fix crash with unconfigured `top_layer` (#13846) --- esphome/components/lvgl/schemas.py | 1 + tests/components/lvgl/test.host.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 45d933c00e..2aeeedbd10 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -436,6 +436,7 @@ def container_schema(widget_type: WidgetType, extras=None): schema = schema.extend(widget_type.schema) def validator(value): + value = value or {} return append_layout_schema(schema, value)(value) return validator diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 00a8cd8c01..f84156c9d8 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -20,6 +20,8 @@ lvgl: - id: lvgl_0 default_font: space16 displays: sdl0 + top_layer: + - id: lvgl_1 displays: sdl1 on_idle: From 1f761902b6f17984216b7a690f0bb68fc3d2afee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:49:58 -0500 Subject: [PATCH 208/251] [esp32] Set UV_CACHE_DIR inside data dir so Clean All clears it (#13888) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7fb708dea4..3a330a3722 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1026,6 +1026,10 @@ async def to_code(config): Path(__file__).parent / "iram_fix.py.script", ) + # Set the uv cache inside the data dir so "Clean All" clears it. + # Avoids persistent corrupted cache from mid-stream download failures. + os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache")) + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_platformio_option("framework", "espidf") cg.add_build_flag("-DUSE_ESP_IDF") From 1a6c67f92ef4d46ed4134d2e16a102e96d5159a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:45:03 -0600 Subject: [PATCH 209/251] [ssd1306_base] Move switch tables to PROGMEM with lookup tables (#13814) --- .../components/ssd1306_base/ssd1306_base.cpp | 136 ++++++++---------- .../components/ssd1306_base/ssd1306_base.h | 5 +- .../components/ssd1306_i2c/ssd1306_i2c.cpp | 2 +- .../components/ssd1306_spi/ssd1306_spi.cpp | 2 +- 4 files changed, 65 insertions(+), 80 deletions(-) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index e0e7f94ce0..be99bd93da 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -1,6 +1,7 @@ #include "ssd1306_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace ssd1306_base { @@ -40,6 +41,55 @@ static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC; static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD; +// Verify first enum value and table sizes match SSD1306_MODEL_COUNT +static_assert(SSD1306_MODEL_128_32 == 0, "SSD1306Model enum must start at 0"); + +// PROGMEM lookup table indexed by SSD1306Model enum (width, height per model) +struct ModelDimensions { + uint8_t width; + uint8_t height; +}; +static const ModelDimensions MODEL_DIMS[] PROGMEM = { + {128, 32}, // SSD1306_MODEL_128_32 + {128, 64}, // SSD1306_MODEL_128_64 + {96, 16}, // SSD1306_MODEL_96_16 + {64, 48}, // SSD1306_MODEL_64_48 + {64, 32}, // SSD1306_MODEL_64_32 + {72, 40}, // SSD1306_MODEL_72_40 + {128, 32}, // SH1106_MODEL_128_32 + {128, 64}, // SH1106_MODEL_128_64 + {96, 16}, // SH1106_MODEL_96_16 + {64, 48}, // SH1106_MODEL_64_48 + {64, 128}, // SH1107_MODEL_128_64 (note: width is 64, height is 128) + {128, 128}, // SH1107_MODEL_128_128 + {128, 32}, // SSD1305_MODEL_128_32 + {128, 64}, // SSD1305_MODEL_128_64 +}; + +// clang-format off +PROGMEM_STRING_TABLE(ModelStrings, + "SSD1306 128x32", // SSD1306_MODEL_128_32 + "SSD1306 128x64", // SSD1306_MODEL_128_64 + "SSD1306 96x16", // SSD1306_MODEL_96_16 + "SSD1306 64x48", // SSD1306_MODEL_64_48 + "SSD1306 64x32", // SSD1306_MODEL_64_32 + "SSD1306 72x40", // SSD1306_MODEL_72_40 + "SH1106 128x32", // SH1106_MODEL_128_32 + "SH1106 128x64", // SH1106_MODEL_128_64 + "SH1106 96x16", // SH1106_MODEL_96_16 + "SH1106 64x48", // SH1106_MODEL_64_48 + "SH1107 128x64", // SH1107_MODEL_128_64 + "SH1107 128x128", // SH1107_MODEL_128_128 + "SSD1305 128x32", // SSD1305_MODEL_128_32 + "SSD1305 128x64", // SSD1305_MODEL_128_64 + "Unknown" // fallback +); +// clang-format on +static_assert(sizeof(MODEL_DIMS) / sizeof(MODEL_DIMS[0]) == SSD1306_MODEL_COUNT, + "MODEL_DIMS must have one entry per SSD1306Model"); +static_assert(ModelStrings::COUNT == SSD1306_MODEL_COUNT + 1, + "ModelStrings must have one entry per SSD1306Model plus fallback"); + void SSD1306::setup() { this->init_internal_(this->get_buffer_length_()); @@ -146,6 +196,7 @@ void SSD1306::setup() { break; case SH1107_MODEL_128_64: case SH1107_MODEL_128_128: + case SSD1306_MODEL_COUNT: // Not used, but prevents build warning break; } @@ -274,54 +325,14 @@ void SSD1306::turn_off() { this->is_on_ = false; } int SSD1306::get_height_internal() { - switch (this->model_) { - case SH1107_MODEL_128_64: - case SH1107_MODEL_128_128: - return 128; - case SSD1306_MODEL_128_32: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_128_32: - case SSD1305_MODEL_128_32: - return 32; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1305_MODEL_128_64: - return 64; - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - return 16; - case SSD1306_MODEL_64_48: - case SH1106_MODEL_64_48: - return 48; - case SSD1306_MODEL_72_40: - return 40; - default: - return 0; - } + if (this->model_ >= SSD1306_MODEL_COUNT) + return 0; + return progmem_read_byte(&MODEL_DIMS[this->model_].height); } int SSD1306::get_width_internal() { - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1305_MODEL_128_32: - case SSD1305_MODEL_128_64: - case SH1107_MODEL_128_128: - return 128; - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - return 96; - case SSD1306_MODEL_64_48: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_64_48: - case SH1107_MODEL_128_64: - return 64; - case SSD1306_MODEL_72_40: - return 72; - default: - return 0; - } + if (this->model_ >= SSD1306_MODEL_COUNT) + return 0; + return progmem_read_byte(&MODEL_DIMS[this->model_].width); } size_t SSD1306::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; @@ -361,37 +372,8 @@ void SSD1306::init_reset_() { this->reset_pin_->digital_write(true); } } -const char *SSD1306::model_str_() { - switch (this->model_) { - case SSD1306_MODEL_128_32: - return "SSD1306 128x32"; - case SSD1306_MODEL_128_64: - return "SSD1306 128x64"; - case SSD1306_MODEL_64_32: - return "SSD1306 64x32"; - case SSD1306_MODEL_96_16: - return "SSD1306 96x16"; - case SSD1306_MODEL_64_48: - return "SSD1306 64x48"; - case SSD1306_MODEL_72_40: - return "SSD1306 72x40"; - case SH1106_MODEL_128_32: - return "SH1106 128x32"; - case SH1106_MODEL_128_64: - return "SH1106 128x64"; - case SH1106_MODEL_96_16: - return "SH1106 96x16"; - case SH1106_MODEL_64_48: - return "SH1106 64x48"; - case SH1107_MODEL_128_64: - return "SH1107 128x64"; - case SSD1305_MODEL_128_32: - return "SSD1305 128x32"; - case SSD1305_MODEL_128_64: - return "SSD1305 128x64"; - default: - return "Unknown"; - } +const LogString *SSD1306::model_str_() { + return ModelStrings::get_log_str(static_cast<uint8_t>(this->model_), ModelStrings::LAST_INDEX); } } // namespace ssd1306_base diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index a573437386..3cc795a323 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -22,6 +22,9 @@ enum SSD1306Model { SH1107_MODEL_128_128, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, + // When adding a new model, add it before SSD1306_MODEL_COUNT and update + // MODEL_DIMS and ModelStrings tables in ssd1306_base.cpp + SSD1306_MODEL_COUNT, // must be last }; class SSD1306 : public display::DisplayBuffer { @@ -70,7 +73,7 @@ class SSD1306 : public display::DisplayBuffer { int get_height_internal() override; int get_width_internal() override; size_t get_buffer_length_(); - const char *model_str_(); + const LogString *model_str_(); SSD1306Model model_{SSD1306_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 47a21a8ff4..e1f6e91243 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -28,7 +28,7 @@ void I2CSSD1306::dump_config() { " Offset X: %d\n" " Offset Y: %d\n" " Inverted Color: %s", - this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), + LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), this->offset_x_, this->offset_y_, YESNO(this->invert_)); LOG_I2C_DEVICE(this); LOG_PIN(" Reset Pin: ", this->reset_pin_); diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index db28dfc564..af9a17c8ab 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -24,7 +24,7 @@ void SPISSD1306::dump_config() { " Offset X: %d\n" " Offset Y: %d\n" " Inverted Color: %s", - this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), + LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), this->offset_x_, this->offset_y_, YESNO(this->invert_)); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); From 4168e8c30d6cf7f6d7e572e827c1851b956853f3 Mon Sep 17 00:00:00 2001 From: Sean Kelly <xconverge@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:30:37 -0800 Subject: [PATCH 210/251] [aqi] Fix AQI calculation for specific pm2.5 or pm10 readings (#13770) --- esphome/components/aqi/aqi_calculator.h | 38 +++++++++++++++++++----- esphome/components/aqi/caqi_calculator.h | 30 ++++++++++++++++--- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index 993504c1e9..d624af0432 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include <algorithm> #include <cmath> #include <limits> #include "abstract_aqi_calculator.h" @@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast<uint16_t>(std::lround(aqi)); } protected: @@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f}, - {35.5f, 55.4f}, {55.5f, 125.4f}, - {125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}}; + static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 9.1f}, + {9.1f, 35.5f}, + {35.5f, 55.5f}, + {55.5f, 125.5f}, + {125.5f, 225.5f}, + {225.5f, std::numeric_limits<float>::max()} + // clang-format on + }; - static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f}, - {155.0f, 254.0f}, {255.0f, 354.0f}, - {355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}}; + static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 55.0f}, + {55.0f, 155.0f}, + {155.0f, 255.0f}, + {255.0f, 355.0f}, + {355.0f, 425.0f}, + {425.0f, std::numeric_limits<float>::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index d2ec4bb98f..fe2efe7059 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include <algorithm> #include <cmath> #include <limits> #include "abstract_aqi_calculator.h" @@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast<uint16_t>(std::lround(aqi)); } protected: @@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { - {0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}}; + // clang-format off + {0.0f, 15.1f}, + {15.1f, 30.1f}, + {30.1f, 55.1f}, + {55.1f, 110.1f}, + {110.1f, std::numeric_limits<float>::max()} + // clang-format on + }; static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { - {0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}}; + // clang-format off + {0.0f, 25.1f}, + {25.1f, 50.1f}, + {50.1f, 90.1f}, + {90.1f, 180.1f}, + {180.1f, std::numeric_limits<float>::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } From a99f75ca7102036544a3285f615a830fb7b01975 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 Feb 2026 08:45:06 +1300 Subject: [PATCH 211/251] Bump version to 2026.1.5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 140ac06565..356739412b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.1.4 +PROJECT_NUMBER = 2026.1.5 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 862c7e37e6..e5c1162834 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.1.4" +__version__ = "2026.1.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From c03abcdb861da71e2f51c596e8f46179a2124d87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:53:53 -0600 Subject: [PATCH 212/251] [http_request] Reduce heap allocations in update check by parsing JSON directly from buffer (#13588) --- .../update/http_request_update.cpp | 24 +++++++------------ esphome/components/json/json_util.cpp | 7 +++++- esphome/components/json/json_util.h | 2 ++ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c63e55d159..85609bd31f 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -90,16 +90,14 @@ void HttpRequestUpdate::update_task(void *params) { UPDATE_RETURN; } size_t read_index = container->get_bytes_read(); + size_t content_length = container->content_length; + + container->end(); + container.reset(); // Release ownership of the container's shared_ptr bool valid = false; - { // Ensures the response string falls out of scope and deallocates before the task ends - std::string response((char *) data, read_index); - allocator.deallocate(data, container->content_length); - - container->end(); - container.reset(); // Release ownership of the container's shared_ptr - - valid = json::parse_json(response, [this_update](JsonObject root) -> bool { + { // Scope to ensure JsonDocument is destroyed before deallocating buffer + valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool { if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || !root[ESPHOME_F("builds")].is<JsonArray>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); @@ -137,6 +135,7 @@ void HttpRequestUpdate::update_task(void *params) { return false; }); } + allocator.deallocate(data, content_length); if (!valid) { ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); @@ -157,17 +156,12 @@ void HttpRequestUpdate::update_task(void *params) { } } - { // Ensures the current version string falls out of scope and deallocates before the task ends - std::string current_version; #ifdef ESPHOME_PROJECT_VERSION - current_version = ESPHOME_PROJECT_VERSION; + this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION; #else - current_version = ESPHOME_VERSION; + this_update->update_info_.current_version = ESPHOME_VERSION; #endif - this_update->update_info_.current_version = current_version; - } - bool trigger_update_available = false; if (this_update->update_info_.latest_version.empty() || diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 869d29f92e..69f8bfc61a 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -25,8 +25,13 @@ std::string build_json(const json_build_t &f) { } bool parse_json(const std::string &data, const json_parse_t &f) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f); +} + +bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size()); + JsonDocument doc = parse_json(data, len); if (doc.overflowed() || doc.isNull()) return false; return f(doc.as<JsonObject>()); diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 91cc84dc14..ca074926bd 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -50,6 +50,8 @@ std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); +/// Parse JSON from raw bytes and run the provided json parse function if it's valid. +bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f); /// Parse a JSON string and return the root JsonDocument (or an unbound object on error) JsonDocument parse_json(const uint8_t *data, size_t len); From 727bb27611e185fecaf83bd3945ac8aebf420a55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:54:07 -0600 Subject: [PATCH 213/251] [bmp3xx_base/bmp581_base] Convert oversampling and IIR filter strings to PROGMEM_STRING_TABLE (#13808) --- .../components/bmp3xx_base/bmp3xx_base.cpp | 47 ++++------------- .../components/bmp581_base/bmp581_base.cpp | 51 ++++--------------- 2 files changed, 20 insertions(+), 78 deletions(-) diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index d7d9972170..c781252de3 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -6,8 +6,9 @@ */ #include "bmp3xx_base.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include <cinttypes> namespace esphome { @@ -26,46 +27,18 @@ static const LogString *chip_type_to_str(uint8_t chip_type) { } } +// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32 +PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", ""); + static const LogString *oversampling_to_str(Oversampling oversampling) { - switch (oversampling) { - case Oversampling::OVERSAMPLING_NONE: - return LOG_STR("None"); - case Oversampling::OVERSAMPLING_X2: - return LOG_STR("2x"); - case Oversampling::OVERSAMPLING_X4: - return LOG_STR("4x"); - case Oversampling::OVERSAMPLING_X8: - return LOG_STR("8x"); - case Oversampling::OVERSAMPLING_X16: - return LOG_STR("16x"); - case Oversampling::OVERSAMPLING_X32: - return LOG_STR("32x"); - default: - return LOG_STR(""); - } + return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX); } +// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128 +PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *iir_filter_to_str(IIRFilter filter) { - switch (filter) { - case IIRFilter::IIR_FILTER_OFF: - return LOG_STR("OFF"); - case IIRFilter::IIR_FILTER_2: - return LOG_STR("2x"); - case IIRFilter::IIR_FILTER_4: - return LOG_STR("4x"); - case IIRFilter::IIR_FILTER_8: - return LOG_STR("8x"); - case IIRFilter::IIR_FILTER_16: - return LOG_STR("16x"); - case IIRFilter::IIR_FILTER_32: - return LOG_STR("32x"); - case IIRFilter::IIR_FILTER_64: - return LOG_STR("64x"); - case IIRFilter::IIR_FILTER_128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX); } void BMP3XXComponent::setup() { diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index 67c6771862..c4a96ebc39 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -11,57 +11,26 @@ */ #include "bmp581_base.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::bmp581_base { static const char *const TAG = "bmp581"; +// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128 +PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *oversampling_to_str(Oversampling oversampling) { - switch (oversampling) { - case Oversampling::OVERSAMPLING_NONE: - return LOG_STR("None"); - case Oversampling::OVERSAMPLING_X2: - return LOG_STR("2x"); - case Oversampling::OVERSAMPLING_X4: - return LOG_STR("4x"); - case Oversampling::OVERSAMPLING_X8: - return LOG_STR("8x"); - case Oversampling::OVERSAMPLING_X16: - return LOG_STR("16x"); - case Oversampling::OVERSAMPLING_X32: - return LOG_STR("32x"); - case Oversampling::OVERSAMPLING_X64: - return LOG_STR("64x"); - case Oversampling::OVERSAMPLING_X128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX); } +// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128 +PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *iir_filter_to_str(IIRFilter filter) { - switch (filter) { - case IIRFilter::IIR_FILTER_OFF: - return LOG_STR("OFF"); - case IIRFilter::IIR_FILTER_2: - return LOG_STR("2x"); - case IIRFilter::IIR_FILTER_4: - return LOG_STR("4x"); - case IIRFilter::IIR_FILTER_8: - return LOG_STR("8x"); - case IIRFilter::IIR_FILTER_16: - return LOG_STR("16x"); - case IIRFilter::IIR_FILTER_32: - return LOG_STR("32x"); - case IIRFilter::IIR_FILTER_64: - return LOG_STR("64x"); - case IIRFilter::IIR_FILTER_128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX); } void BMP581Component::dump_config() { From 2a6d9d632505b5ccfef1678bdfe69f30c9d9b8df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:54:22 -0600 Subject: [PATCH 214/251] [mqtt] Avoid heap allocation in on_log by using const char* publish overload (#13809) --- esphome/components/mqtt/mqtt_client.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index d503461257..90b423c386 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -170,10 +170,8 @@ void MQTTClientComponent::send_device_info_() { void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { (void) tag; if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = std::string(message, message_len), - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); + this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos, + this->log_message_.retain); } } #endif From 86feb4e27a3f4943197b19485fb7f83c64300269 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:54:37 -0600 Subject: [PATCH 215/251] [rtttl] Convert state_to_string to PROGMEM_STRING_TABLE (#13807) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index c179282c50..5a64f37da4 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -2,6 +2,7 @@ #include <cmath> #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace rtttl { @@ -375,22 +376,13 @@ void Rtttl::loop() { } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback +PROGMEM_STRING_TABLE(RtttlStateStrings, "STATE_STOPPED", "STATE_INIT", "STATE_STARTING", "STATE_RUNNING", + "STATE_STOPPING", "UNKNOWN"); + static const LogString *state_to_string(State state) { - switch (state) { - case STATE_STOPPED: - return LOG_STR("STATE_STOPPED"); - case STATE_STARTING: - return LOG_STR("STATE_STARTING"); - case STATE_RUNNING: - return LOG_STR("STATE_RUNNING"); - case STATE_STOPPING: - return LOG_STR("STATE_STOPPING"); - case STATE_INIT: - return LOG_STR("STATE_INIT"); - default: - return LOG_STR("UNKNOWN"); - } -}; + return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX); +} #endif void Rtttl::set_state_(State state) { From 5365faa8773c889de4e31c7eac16a6db23b760ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:54:48 -0600 Subject: [PATCH 216/251] [debug] Move ESP8266 switch tables to flash with PROGMEM_STRING_TABLE (#13813) --- esphome/components/debug/debug_esp8266.cpp | 68 +++++++++++----------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index a4b6468b49..1a07ec4f3a 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -1,6 +1,7 @@ #include "debug_component.h" #ifdef USE_ESP8266 #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include <Esp.h> extern "C" { @@ -19,27 +20,38 @@ namespace debug { static const char *const TAG = "debug"; +// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback +// clang-format off +PROGMEM_STRING_TABLE(ResetReasonStrings, + "Power On", // 0 = REASON_DEFAULT_RST + "Hardware Watchdog", // 1 = REASON_WDT_RST + "Exception", // 2 = REASON_EXCEPTION_RST + "Software Watchdog", // 3 = REASON_SOFT_WDT_RST + "Software/System restart", // 4 = REASON_SOFT_RESTART + "Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE + "External System", // 6 = REASON_EXT_SYS_RST + "Unknown" // 7 = fallback +); +// clang-format on +static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices"); +static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices"); +static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices"); +static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices"); +static_assert(REASON_SOFT_RESTART == 4, "Reset reason enum values must match table indices"); +static_assert(REASON_DEEP_SLEEP_AWAKE == 5, "Reset reason enum values must match table indices"); +static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices"); + +// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback +PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN"); +static_assert(FM_QIO == 0, "Flash mode enum values must match table indices"); +static_assert(FM_QOUT == 1, "Flash mode enum values must match table indices"); +static_assert(FM_DIO == 2, "Flash mode enum values must match table indices"); +static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices"); + // Get reset reason string from reason code (no heap allocation) // Returns LogString* pointing to flash (PROGMEM) on ESP8266 static const LogString *get_reset_reason_str(uint32_t reason) { - switch (reason) { - case REASON_DEFAULT_RST: - return LOG_STR("Power On"); - case REASON_WDT_RST: - return LOG_STR("Hardware Watchdog"); - case REASON_EXCEPTION_RST: - return LOG_STR("Exception"); - case REASON_SOFT_WDT_RST: - return LOG_STR("Software Watchdog"); - case REASON_SOFT_RESTART: - return LOG_STR("Software/System restart"); - case REASON_DEEP_SLEEP_AWAKE: - return LOG_STR("Deep-Sleep Wake"); - case REASON_EXT_SYS_RST: - return LOG_STR("External System"); - default: - return LOG_STR("Unknown"); - } + return ResetReasonStrings::get_log_str(static_cast<uint8_t>(reason), ResetReasonStrings::LAST_INDEX); } // Size for core version hex buffer @@ -92,23 +104,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - const LogString *flash_mode; - switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) - case FM_QIO: - flash_mode = LOG_STR("QIO"); - break; - case FM_QOUT: - flash_mode = LOG_STR("QOUT"); - break; - case FM_DIO: - flash_mode = LOG_STR("DIO"); - break; - case FM_DOUT: - flash_mode = LOG_STR("DOUT"); - break; - default: - flash_mode = LOG_STR("UNKNOWN"); - } + const LogString *flash_mode = FlashModeStrings::get_log_str( + static_cast<uint8_t>(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance) + FlashModeStrings::LAST_INDEX); uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance) ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, From e2fad9a6c911b6c60663db91a9cf30d8ab4ee991 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:55:01 -0600 Subject: [PATCH 217/251] [sprinkler] Convert state and request origin strings to PROGMEM_STRING_TABLE (#13806) --- esphome/components/sprinkler/sprinkler.cpp | 42 ++++++---------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index eae6ecbf31..9e423c1760 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -4,6 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include <cinttypes> #include <utility> @@ -1544,42 +1545,19 @@ void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) { ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name)); } +// Request origin strings indexed by SprinklerValveRunRequestOrigin enum (0-2): USER, CYCLE, QUEUE +PROGMEM_STRING_TABLE(SprinklerRequestOriginStrings, "USER", "CYCLE", "QUEUE", "UNKNOWN"); + const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { - switch (origin) { - case USER: - return LOG_STR("USER"); - - case CYCLE: - return LOG_STR("CYCLE"); - - case QUEUE: - return LOG_STR("QUEUE"); - - default: - return LOG_STR("UNKNOWN"); - } + return SprinklerRequestOriginStrings::get_log_str(static_cast<uint8_t>(origin), + SprinklerRequestOriginStrings::LAST_INDEX); } +// Sprinkler state strings indexed by SprinklerState enum (0-4): IDLE, STARTING, ACTIVE, STOPPING, BYPASS +PROGMEM_STRING_TABLE(SprinklerStateStrings, "IDLE", "STARTING", "ACTIVE", "STOPPING", "BYPASS", "UNKNOWN"); + const LogString *Sprinkler::state_as_str_(SprinklerState state) { - switch (state) { - case IDLE: - return LOG_STR("IDLE"); - - case STARTING: - return LOG_STR("STARTING"); - - case ACTIVE: - return LOG_STR("ACTIVE"); - - case STOPPING: - return LOG_STR("STOPPING"); - - case BYPASS: - return LOG_STR("BYPASS"); - - default: - return LOG_STR("UNKNOWN"); - } + return SprinklerStateStrings::get_log_str(static_cast<uint8_t>(state), SprinklerStateStrings::LAST_INDEX); } void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { From c65d3a0072d9d7e2202b6d657c41d035b2a9fa90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:55:16 -0600 Subject: [PATCH 218/251] [mqtt] Add zero-allocation topic getters to MQTT_COMPONENT_CUSTOM_TOPIC macro (#13811) --- esphome/components/mqtt/mqtt_climate.cpp | 34 +++++++++++++----------- esphome/components/mqtt/mqtt_component.h | 5 ++++ esphome/components/mqtt/mqtt_cover.cpp | 6 ++--- esphome/components/mqtt/mqtt_fan.cpp | 9 ++++--- esphome/components/mqtt/mqtt_valve.cpp | 4 +-- 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 158cfb5552..81b2e0e8db 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -300,9 +300,11 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); + // Reusable stack buffer for topic construction (avoids heap allocation per publish) + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; // mode bool success = true; - if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode))) + if (!this->publish(this->get_mode_state_topic_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode))) success = false; int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -311,68 +313,70 @@ bool MQTTClimateComponent::publish_state_() { if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) && !std::isnan(this->device_->current_temperature)) { len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy); - if (!this->publish(this->get_current_temperature_state_topic(), payload, len)) + if (!this->publish(this->get_current_temperature_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy); - if (!this->publish(this->get_target_temperature_low_state_topic(), payload, len)) + if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len)) success = false; len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy); - if (!this->publish(this->get_target_temperature_high_state_topic(), payload, len)) + if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len)) success = false; } else { len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy); - if (!this->publish(this->get_target_temperature_state_topic(), payload, len)) + if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) && !std::isnan(this->device_->current_humidity)) { len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0); - if (!this->publish(this->get_current_humidity_state_topic(), payload, len)) + if (!this->publish(this->get_current_humidity_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) && !std::isnan(this->device_->target_humidity)) { len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0); - if (!this->publish(this->get_target_humidity_state_topic(), payload, len)) + if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { if (this->device_->has_custom_preset()) { - if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset())) + if (!this->publish(this->get_preset_state_topic_to(topic_buf), this->device_->get_custom_preset().c_str())) success = false; } else if (this->device_->preset.has_value()) { - if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value()))) + if (!this->publish(this->get_preset_state_topic_to(topic_buf), + climate_preset_to_mqtt_str(this->device_->preset.value()))) success = false; - } else if (!this->publish(this->get_preset_state_topic(), "")) { + } else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) { success = false; } } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action))) + if (!this->publish(this->get_action_state_topic_to(topic_buf), climate_action_to_mqtt_str(this->device_->action))) success = false; } if (traits.get_supports_fan_modes()) { if (this->device_->has_custom_fan_mode()) { - if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode())) + if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), this->device_->get_custom_fan_mode().c_str())) success = false; } else if (this->device_->fan_mode.has_value()) { - if (!this->publish(this->get_fan_mode_state_topic(), + if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value()))) success = false; - } else if (!this->publish(this->get_fan_mode_state_topic(), "")) { + } else if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), "")) { success = false; } } if (traits.get_supports_swing_modes()) { - if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) + if (!this->publish(this->get_swing_mode_state_topic_to(topic_buf), + climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) success = false; } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index eb98158647..853712940a 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -59,6 +59,11 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b \ public: \ void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \ + StringRef get_##name##_##type##_topic_to(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { \ + if (!this->custom_##name##_##type##_topic_.empty()) \ + return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \ + return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \ + } \ std::string get_##name##_##type##_topic() const { \ if (this->custom_##name##_##type##_topic_.empty()) \ return this->get_default_topic_for_(#name "/" #type); \ diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 50e68ecbcc..c21af413ed 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -112,19 +112,19 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } bool MQTTCoverComponent::publish_state() { auto traits = this->cover_->get_traits(); bool success = true; + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (traits.get_supports_position()) { char pos[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0); - if (!this->publish(this->get_position_state_topic(), pos, len)) + if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len)) success = false; } if (traits.get_supports_tilt()) { char pos[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0); - if (!this->publish(this->get_tilt_state_topic(), pos, len)) + if (!this->publish(this->get_tilt_state_topic_to(topic_buf), pos, len)) success = false; } - char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (!this->publish(this->get_state_topic_to_(topic_buf), cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, traits.get_supports_position()))) diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 84d51895c5..ae2b8c4600 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -173,19 +173,20 @@ bool MQTTFanComponent::publish_state() { this->publish(this->get_state_topic_to_(topic_buf), state_s); bool failed = false; if (this->state_->get_traits().supports_direction()) { - bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction)); + bool success = this->publish(this->get_direction_state_topic_to(topic_buf), + fan_direction_to_mqtt_str(this->state_->direction)); failed = failed || !success; } if (this->state_->get_traits().supports_oscillation()) { - bool success = - this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating)); + bool success = this->publish(this->get_oscillation_state_topic_to(topic_buf), + fan_oscillation_to_mqtt_str(this->state_->oscillating)); failed = failed || !success; } auto traits = this->state_->get_traits(); if (traits.supports_speed()) { char buf[12]; 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); + bool success = this->publish(this->get_speed_level_state_topic_to(topic_buf), buf, len); failed = failed || !success; } return !failed; diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 16e25f6a8a..2b9f02858b 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -87,13 +87,13 @@ bool MQTTValveComponent::send_initial_state() { return this->publish_state(); } bool MQTTValveComponent::publish_state() { auto traits = this->valve_->get_traits(); bool success = true; + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (traits.get_supports_position()) { char pos[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0); - if (!this->publish(this->get_position_state_topic(), pos, len)) + if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len)) success = false; } - char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (!this->publish(this->get_state_topic_to_(topic_buf), valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position, traits.get_supports_position()))) From 868a2151e3ef63606e933257e49200216fe50eb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 13:56:12 -0600 Subject: [PATCH 219/251] [web_server_idf] Reduce heap allocations by using stack buffers (#13549) --- .../web_server_idf/web_server_idf.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ac7f99bef7..0dd1948dcc 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -344,14 +344,15 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw memcpy(user_info + user_len + 1, password, pass_len); user_info[user_info_len] = '\0'; - size_t n = 0, out; - esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast<const uint8_t *>(user_info), user_info_len); - - auto digest = std::unique_ptr<char[]>(new char[n + 1]); - esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out, + // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes + // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety + constexpr size_t max_digest_len = 350; + char digest[max_digest_len]; + size_t out; + esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out, reinterpret_cast<const uint8_t *>(user_info), user_info_len); - return strcmp(digest.get(), auth_str + auth_prefix_len) == 0; + return strcmp(digest, auth_str + auth_prefix_len) == 0; } void AsyncWebServerRequest::requestAuthentication(const char *realm) const { @@ -861,12 +862,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } }); - // Process data - std::unique_ptr<char[]> buffer(new char[MULTIPART_CHUNK_SIZE]); + // Process data - use stack buffer to avoid heap allocation + char buffer[MULTIPART_CHUNK_SIZE]; size_t bytes_since_yield = 0; for (size_t remaining = r->content_len; remaining > 0;) { - int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE)); + int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE)); if (recv_len <= 0) { httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, @@ -874,7 +875,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; } - if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) { + if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) { ESP_LOGW(TAG, "Multipart parser error"); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); return ESP_FAIL; From d152438335c21a1590da26f0a3b3fd19d3d480aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 14:07:09 -0600 Subject: [PATCH 220/251] [libretiny] Update LibreTiny to v1.12.1 (#13851) --- .clang-tidy.hash | 2 +- esphome/components/bk72xx/boards.py | 23 +- esphome/components/libretiny/__init__.py | 10 +- esphome/components/ln882x/boards.py | 126 ++++--- esphome/components/rtl87xx/boards.py | 420 +++++++++++++++++++++-- platformio.ini | 2 +- 6 files changed, 474 insertions(+), 109 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 37f82e2755..3ffe32af88 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695 +74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7 diff --git a/esphome/components/bk72xx/boards.py b/esphome/components/bk72xx/boards.py index 3850dbe266..4bee69fe6d 100644 --- a/esphome/components/bk72xx/boards.py +++ b/esphome/components/bk72xx/boards.py @@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "cbu": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7231t-qfn32-tuya": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7231n-qfn32-tuya": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = { }, "cb3s": { "WIRE1_SCL": 20, - "WIRE1_SDA_0": 21, - "WIRE1_SDA_1": 21, + "WIRE1_SDA": 21, "SERIAL1_RX": 10, "SERIAL1_TX": 11, "SERIAL2_TX": 0, @@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7252": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "cb3se": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE2_SCL": 0, "WIRE2_SDA": 1, "SERIAL1_RX": 10, diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 553179beec..01445da7ee 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -193,14 +193,14 @@ def _notify_old_style(config): # The dev and latest branches will be at *least* this version, which is what matters. # Use GitHub releases directly to avoid PlatformIO moderation delays. ARDUINO_VERSIONS = { - "dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"), + "dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"), "latest": ( - cv.Version(1, 11, 0), - "https://github.com/libretiny-eu/libretiny.git#v1.11.0", + cv.Version(1, 12, 1), + "https://github.com/libretiny-eu/libretiny.git#v1.12.1", ), "recommended": ( - cv.Version(1, 11, 0), - "https://github.com/libretiny-eu/libretiny.git#v1.11.0", + cv.Version(1, 12, 1), + "https://github.com/libretiny-eu/libretiny.git#v1.12.1", ), } diff --git a/esphome/components/ln882x/boards.py b/esphome/components/ln882x/boards.py index 600371951d..df44419ed2 100644 --- a/esphome/components/ln882x/boards.py +++ b/esphome/components/ln882x/boards.py @@ -154,28 +154,26 @@ LN882X_BOARD_PINS = { "A7": 21, }, "wb02a": { - "WIRE0_SCL_0": 7, - "WIRE0_SCL_1": 5, + "WIRE0_SCL_0": 1, + "WIRE0_SCL_1": 2, "WIRE0_SCL_2": 3, - "WIRE0_SCL_3": 10, - "WIRE0_SCL_4": 2, - "WIRE0_SCL_5": 1, - "WIRE0_SCL_6": 4, - "WIRE0_SCL_7": 5, - "WIRE0_SCL_8": 9, - "WIRE0_SCL_9": 24, - "WIRE0_SCL_10": 25, - "WIRE0_SDA_0": 7, - "WIRE0_SDA_1": 5, + "WIRE0_SCL_3": 4, + "WIRE0_SCL_4": 5, + "WIRE0_SCL_5": 7, + "WIRE0_SCL_6": 9, + "WIRE0_SCL_7": 10, + "WIRE0_SCL_8": 24, + "WIRE0_SCL_9": 25, + "WIRE0_SDA_0": 1, + "WIRE0_SDA_1": 2, "WIRE0_SDA_2": 3, - "WIRE0_SDA_3": 10, - "WIRE0_SDA_4": 2, - "WIRE0_SDA_5": 1, - "WIRE0_SDA_6": 4, - "WIRE0_SDA_7": 5, - "WIRE0_SDA_8": 9, - "WIRE0_SDA_9": 24, - "WIRE0_SDA_10": 25, + "WIRE0_SDA_3": 4, + "WIRE0_SDA_4": 5, + "WIRE0_SDA_5": 7, + "WIRE0_SDA_6": 9, + "WIRE0_SDA_7": 10, + "WIRE0_SDA_8": 24, + "WIRE0_SDA_9": 25, "SERIAL0_RX": 3, "SERIAL0_TX": 2, "SERIAL1_RX": 24, @@ -221,32 +219,32 @@ LN882X_BOARD_PINS = { "A1": 4, }, "wl2s": { - "WIRE0_SCL_0": 7, - "WIRE0_SCL_1": 12, - "WIRE0_SCL_2": 3, - "WIRE0_SCL_3": 10, - "WIRE0_SCL_4": 2, - "WIRE0_SCL_5": 0, - "WIRE0_SCL_6": 19, - "WIRE0_SCL_7": 11, - "WIRE0_SCL_8": 9, - "WIRE0_SCL_9": 24, - "WIRE0_SCL_10": 25, - "WIRE0_SCL_11": 5, - "WIRE0_SCL_12": 1, - "WIRE0_SDA_0": 7, - "WIRE0_SDA_1": 12, - "WIRE0_SDA_2": 3, - "WIRE0_SDA_3": 10, - "WIRE0_SDA_4": 2, - "WIRE0_SDA_5": 0, - "WIRE0_SDA_6": 19, - "WIRE0_SDA_7": 11, - "WIRE0_SDA_8": 9, - "WIRE0_SDA_9": 24, - "WIRE0_SDA_10": 25, - "WIRE0_SDA_11": 5, - "WIRE0_SDA_12": 1, + "WIRE0_SCL_0": 0, + "WIRE0_SCL_1": 1, + "WIRE0_SCL_2": 2, + "WIRE0_SCL_3": 3, + "WIRE0_SCL_4": 5, + "WIRE0_SCL_5": 7, + "WIRE0_SCL_6": 9, + "WIRE0_SCL_7": 10, + "WIRE0_SCL_8": 11, + "WIRE0_SCL_9": 12, + "WIRE0_SCL_10": 19, + "WIRE0_SCL_11": 24, + "WIRE0_SCL_12": 25, + "WIRE0_SDA_0": 0, + "WIRE0_SDA_1": 1, + "WIRE0_SDA_2": 2, + "WIRE0_SDA_3": 3, + "WIRE0_SDA_4": 5, + "WIRE0_SDA_5": 7, + "WIRE0_SDA_6": 9, + "WIRE0_SDA_7": 10, + "WIRE0_SDA_8": 11, + "WIRE0_SDA_9": 12, + "WIRE0_SDA_10": 19, + "WIRE0_SDA_11": 24, + "WIRE0_SDA_12": 25, "SERIAL0_RX": 3, "SERIAL0_TX": 2, "SERIAL1_RX": 24, @@ -301,24 +299,24 @@ LN882X_BOARD_PINS = { "A2": 1, }, "ln-02": { - "WIRE0_SCL_0": 11, - "WIRE0_SCL_1": 19, - "WIRE0_SCL_2": 3, - "WIRE0_SCL_3": 24, - "WIRE0_SCL_4": 2, - "WIRE0_SCL_5": 25, - "WIRE0_SCL_6": 1, - "WIRE0_SCL_7": 0, - "WIRE0_SCL_8": 9, - "WIRE0_SDA_0": 11, - "WIRE0_SDA_1": 19, - "WIRE0_SDA_2": 3, - "WIRE0_SDA_3": 24, - "WIRE0_SDA_4": 2, - "WIRE0_SDA_5": 25, - "WIRE0_SDA_6": 1, - "WIRE0_SDA_7": 0, - "WIRE0_SDA_8": 9, + "WIRE0_SCL_0": 0, + "WIRE0_SCL_1": 1, + "WIRE0_SCL_2": 2, + "WIRE0_SCL_3": 3, + "WIRE0_SCL_4": 9, + "WIRE0_SCL_5": 11, + "WIRE0_SCL_6": 19, + "WIRE0_SCL_7": 24, + "WIRE0_SCL_8": 25, + "WIRE0_SDA_0": 0, + "WIRE0_SDA_1": 1, + "WIRE0_SDA_2": 2, + "WIRE0_SDA_3": 3, + "WIRE0_SDA_4": 9, + "WIRE0_SDA_5": 11, + "WIRE0_SDA_6": 19, + "WIRE0_SDA_7": 24, + "WIRE0_SDA_8": 25, "SERIAL0_RX": 3, "SERIAL0_TX": 2, "SERIAL1_RX": 24, diff --git a/esphome/components/rtl87xx/boards.py b/esphome/components/rtl87xx/boards.py index 45ef02b7e7..3a5ee853f2 100644 --- a/esphome/components/rtl87xx/boards.py +++ b/esphome/components/rtl87xx/boards.py @@ -71,6 +71,10 @@ RTL87XX_BOARDS = { "name": "WR3L Wi-Fi Module", "family": FAMILY_RTL8710B, }, + "wbru": { + "name": "WBRU Wi-Fi Module", + "family": FAMILY_RTL8720C, + }, "wr2le": { "name": "WR2LE Wi-Fi Module", "family": FAMILY_RTL8710B, @@ -83,6 +87,14 @@ RTL87XX_BOARDS = { "name": "T103_V1.0", "family": FAMILY_RTL8710B, }, + "cr3l": { + "name": "CR3L Wi-Fi Module", + "family": FAMILY_RTL8720C, + }, + "generic-rtl8720cm-4mb-1712k": { + "name": "Generic - RTL8720CM (4M/1712k)", + "family": FAMILY_RTL8720C, + }, "generic-rtl8720cf-2mb-896k": { "name": "Generic - RTL8720CF (2M/896k)", "family": FAMILY_RTL8720C, @@ -103,6 +115,10 @@ RTL87XX_BOARDS = { "name": "WR2L Wi-Fi Module", "family": FAMILY_RTL8710B, }, + "wbr1": { + "name": "WBR1 Wi-Fi Module", + "family": FAMILY_RTL8720C, + }, "wr1": { "name": "WR1 Wi-Fi Module", "family": FAMILY_RTL8710B, @@ -119,10 +135,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, @@ -230,10 +246,10 @@ RTL87XX_BOARD_PINS = { "A1": 41, }, "wbr3": { - "WIRE0_SCL_0": 11, - "WIRE0_SCL_1": 2, - "WIRE0_SCL_2": 19, - "WIRE0_SCL_3": 15, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SCL_3": 19, "WIRE0_SDA_0": 3, "WIRE0_SDA_1": 12, "WIRE0_SDA_2": 16, @@ -242,10 +258,10 @@ RTL87XX_BOARD_PINS = { "SERIAL0_TX_0": 11, "SERIAL0_TX_1": 14, "SERIAL1_CTS": 4, - "SERIAL1_RX_0": 2, - "SERIAL1_RX_1": 0, - "SERIAL1_TX_0": 3, - "SERIAL1_TX_1": 1, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, "SERIAL2_CTS": 19, "SERIAL2_RX": 15, "SERIAL2_TX": 16, @@ -296,6 +312,12 @@ RTL87XX_BOARD_PINS = { }, "generic-rtl8710bn-2mb-468k": { "SPI0_CS": 19, + "SPI0_FCS": 6, + "SPI0_FD0": 9, + "SPI0_FD1": 7, + "SPI0_FD2": 8, + "SPI0_FD3": 11, + "SPI0_FSCK": 10, "SPI0_MISO": 22, "SPI0_MOSI": 23, "SPI0_SCK": 18, @@ -396,10 +418,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, @@ -463,10 +485,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, @@ -714,6 +736,12 @@ RTL87XX_BOARD_PINS = { }, "generic-rtl8710bn-2mb-788k": { "SPI0_CS": 19, + "SPI0_FCS": 6, + "SPI0_FD0": 9, + "SPI0_FD1": 7, + "SPI0_FD2": 8, + "SPI0_FD3": 11, + "SPI0_FSCK": 10, "SPI0_MISO": 22, "SPI0_MOSI": 23, "SPI0_SCK": 18, @@ -807,6 +835,12 @@ RTL87XX_BOARD_PINS = { }, "generic-rtl8710bx-4mb-980k": { "SPI0_CS": 19, + "SPI0_FCS": 6, + "SPI0_FD0": 9, + "SPI0_FD1": 7, + "SPI0_FD2": 8, + "SPI0_FD3": 11, + "SPI0_FSCK": 10, "SPI0_MISO": 22, "SPI0_MOSI": 23, "SPI0_SCK": 18, @@ -957,8 +991,8 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, "WIRE0_SDA_0": 19, "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, @@ -1088,6 +1122,99 @@ RTL87XX_BOARD_PINS = { "A0": 19, "A1": 41, }, + "wbru": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 7, + "SPI0_CS_2": 15, + "SPI0_MISO_0": 10, + "SPI0_MISO_1": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 9, + "SPI0_MOSI_2": 19, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 8, + "SPI0_SCK_2": 16, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SCL_3": 19, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 16, + "WIRE0_SDA_3": 20, + "SERIAL0_CTS": 10, + "SERIAL0_RTS": 9, + "SERIAL0_RX_0": 12, + "SERIAL0_RX_1": 13, + "SERIAL0_TX_0": 11, + "SERIAL0_TX_1": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX": 3, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CS0": 7, + "CTS0": 10, + "CTS1": 4, + "CTS2": 19, + "MOSI0": 19, + "PA00": 0, + "PA0": 0, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PWM0": 0, + "PWM1": 12, + "PWM5": 17, + "PWM6": 18, + "RTS0": 9, + "RTS2": 20, + "RX2": 15, + "SCK0": 16, + "TX1": 3, + "TX2": 16, + "D0": 8, + "D1": 9, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 15, + "D6": 16, + "D7": 11, + "D8": 12, + "D9": 17, + "D10": 18, + "D11": 19, + "D12": 14, + "D13": 13, + "D14": 20, + "D15": 0, + "D16": 10, + "D17": 7, + }, "wr2le": { "MISO0": 22, "MISO1": 22, @@ -1116,21 +1243,21 @@ RTL87XX_BOARD_PINS = { "SPI0_MISO": 20, "SPI0_MOSI_0": 4, "SPI0_MOSI_1": 19, - "SPI0_SCK_0": 16, - "SPI0_SCK_1": 3, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 16, "WIRE0_SCL_0": 2, "WIRE0_SCL_1": 15, "WIRE0_SCL_2": 19, - "WIRE0_SDA_0": 20, + "WIRE0_SDA_0": 3, "WIRE0_SDA_1": 16, - "WIRE0_SDA_2": 3, + "WIRE0_SDA_2": 20, "SERIAL0_RX": 13, "SERIAL0_TX": 14, "SERIAL1_CTS": 4, - "SERIAL1_RX_0": 2, - "SERIAL1_RX_1": 0, - "SERIAL1_TX_0": 3, - "SERIAL1_TX_1": 1, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, "SERIAL2_CTS": 19, "SERIAL2_RTS": 20, "SERIAL2_RX": 15, @@ -1251,6 +1378,168 @@ RTL87XX_BOARD_PINS = { "A0": 19, "A1": 41, }, + "cr3l": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 15, + "SPI0_MISO": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 19, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 16, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 15, + "WIRE0_SCL_2": 19, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 16, + "WIRE0_SDA_2": 20, + "SERIAL0_RX": 13, + "SERIAL0_TX": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX": 2, + "SERIAL1_TX": 3, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CTS1": 4, + "CTS2": 19, + "MISO0": 20, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PWM0": 20, + "PWM5": 17, + "PWM6": 18, + "RTS2": 20, + "RX0": 13, + "RX1": 2, + "RX2": 15, + "SCL0": 19, + "SDA0": 16, + "TX0": 14, + "TX1": 3, + "TX2": 16, + "D0": 20, + "D1": 2, + "D2": 3, + "D3": 4, + "D4": 15, + "D5": 16, + "D6": 17, + "D7": 18, + "D8": 19, + "D9": 13, + "D10": 14, + }, + "generic-rtl8720cm-4mb-1712k": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 7, + "SPI0_CS_2": 15, + "SPI0_MISO_0": 10, + "SPI0_MISO_1": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 9, + "SPI0_MOSI_2": 19, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 8, + "SPI0_SCK_2": 16, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SCL_3": 19, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 16, + "WIRE0_SDA_3": 20, + "SERIAL0_CTS": 10, + "SERIAL0_RTS": 9, + "SERIAL0_RX_0": 12, + "SERIAL0_RX_1": 13, + "SERIAL0_TX_0": 11, + "SERIAL0_TX_1": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CS0": 15, + "CTS0": 10, + "CTS1": 4, + "CTS2": 19, + "MOSI0": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PA23": 23, + "PWM0": 20, + "PWM5": 17, + "PWM6": 18, + "PWM7": 23, + "RTS0": 9, + "RTS2": 20, + "RX2": 15, + "SCK0": 16, + "TX2": 16, + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 7, + "D6": 8, + "D7": 9, + "D8": 10, + "D9": 11, + "D10": 12, + "D11": 13, + "D12": 14, + "D13": 15, + "D14": 16, + "D15": 17, + "D16": 18, + "D17": 19, + "D18": 20, + "D19": 23, + }, "generic-rtl8720cf-2mb-896k": { "SPI0_CS_0": 2, "SPI0_CS_1": 7, @@ -1456,8 +1745,8 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, "WIRE0_SDA_0": 19, "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, @@ -1585,6 +1874,65 @@ RTL87XX_BOARD_PINS = { "D4": 12, "A0": 19, }, + "wbr1": { + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 16, + "SERIAL0_RX_0": 12, + "SERIAL0_RX_1": 13, + "SERIAL0_TX_0": 11, + "SERIAL0_TX_1": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CTS1": 4, + "MOSI0": 4, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA11": 11, + "PA12": 12, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PWM5": 17, + "PWM6": 18, + "PWM7": 13, + "RX2": 15, + "SCL0": 15, + "SDA0": 12, + "TX2": 16, + "D0": 14, + "D1": 13, + "D2": 2, + "D3": 3, + "D4": 16, + "D5": 4, + "D6": 11, + "D7": 15, + "D8": 12, + "D9": 17, + "D10": 18, + "D11": 0, + "D12": 1, + }, "wr1": { "SPI0_CS": 19, "SPI0_MISO": 22, @@ -1594,10 +1942,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, diff --git a/platformio.ini b/platformio.ini index 94b1f1a727..6b29daf87a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -214,7 +214,7 @@ build_unflags = ; This are common settings for the LibreTiny (all variants) using Arduino. [common:libretiny-arduino] extends = common:arduino -platform = https://github.com/libretiny-eu/libretiny.git#v1.11.0 +platform = https://github.com/libretiny-eu/libretiny.git#v1.12.1 framework = arduino lib_compat_mode = soft lib_deps = From 548b7e5dab6a92aa103c7662647b647e02d4d580 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:04:12 -0500 Subject: [PATCH 221/251] [esp32] Fix ESP32-P4 test: replace stale esp_hosted component ref (#13920) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- tests/components/esp32/test.esp32-p4-idf.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index d67787b3d5..bc054f5aee 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -6,8 +6,8 @@ esp32: type: esp-idf components: - espressif/mdns^1.8.2 - - name: espressif/esp_hosted - ref: 2.7.0 + - name: espressif/button + ref: 4.1.5 advanced: enable_idf_experimental_features: yes disable_debug_stubs: true From b4707344d322473550f8fb46024949ce57eaf033 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:44:12 -0500 Subject: [PATCH 222/251] [esp32] Upgrade uv to 0.10.1 and increase HTTP retries (#13918) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- docker/Dockerfile | 2 +- esphome/components/esp32/__init__.py | 8 -------- esphome/platformio_api.py | 2 ++ 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 348a503bc8..8ebdd1e49b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,7 +23,7 @@ RUN if command -v apk > /dev/null; then \ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 -RUN pip install --no-cache-dir -U pip uv==0.6.14 +RUN pip install --no-cache-dir -U pip uv==0.10.1 COPY requirements.txt / diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d3c343b9db..a680a78951 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1436,14 +1436,6 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) - # Set the uv cache inside the data dir so "Clean All" clears it. - # Avoids persistent corrupted cache from mid-stream download failures. - os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache")) - - # Set the uv cache inside the data dir so "Clean All" clears it. - # Avoids persistent corrupted cache from mid-stream download failures. - os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache")) - if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index e66f9a2c97..d42f89d029 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -133,6 +133,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int: ) # Suppress Python syntax warnings from third-party scripts during compilation os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") + # Increase uv retry count to handle transient network errors (default is 3) + os.environ.setdefault("UV_HTTP_RETRIES", "10") cmd = ["platformio"] + list(args) if not CORE.verbose: From 58659e4893851cfd21167a34fda25dd768c85bd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 18:48:13 -0600 Subject: [PATCH 223/251] [mdns] Throttle MDNS.update() polling on ESP8266 and RP2040 (#13917) --- esphome/components/mdns/mdns_component.h | 25 +++++++++++++++++++++--- esphome/components/mdns/mdns_esp8266.cpp | 11 ++++++++--- esphome/components/mdns/mdns_rp2040.cpp | 11 ++++++++--- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index f696cfff1c..32f8f16ec1 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -45,9 +45,28 @@ class MDNSComponent : public Component { void setup() override; void dump_config() override; -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO) - void loop() override; -#endif + // Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040). + // + // On these platforms, MDNS.update() calls _process(true) which only manages timer-driven + // state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS + // packets are handled independently via the lwIP onRx UDP callback and are NOT affected + // by how often update() is called. + // + // The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1). + // Announcement intervals are 1000ms and cache TTL checks are on the order of seconds + // to minutes. A 50ms polling interval provides sufficient resolution for all timers + // while completely removing mDNS from the per-iteration loop list. + // + // In steady state (after the ~8 second boot probe/announce phase completes), update() + // checks timers that are set to never expire, making every call pure overhead. + // + // Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this + // interval is safe in production. + // + // By using set_interval() instead of overriding loop(), the component is excluded from + // the main loop list via has_overridden_loop(), eliminating all per-iteration overhead + // including virtual dispatch. + static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50; float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } #ifdef USE_MDNS_EXTRA_SERVICES diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index dcbe5ebd52..295a408cbd 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -36,9 +36,14 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER } } -void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); } - -void MDNSComponent::loop() { MDNS.update(); } +void MDNSComponent::setup() { + this->setup_buffers_and_register_(register_esp8266); + // Schedule MDNS.update() via set_interval() instead of overriding loop(). + // This removes the component from the per-iteration loop list entirely, + // eliminating virtual dispatch overhead on every main loop cycle. + // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); +} void MDNSComponent::on_shutdown() { MDNS.close(); diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index e4a9b60cdb..05d991c1fa 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -35,9 +35,14 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV } } -void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); } - -void MDNSComponent::loop() { MDNS.update(); } +void MDNSComponent::setup() { + this->setup_buffers_and_register_(register_rp2040); + // Schedule MDNS.update() via set_interval() instead of overriding loop(). + // This removes the component from the per-iteration loop list entirely, + // eliminating virtual dispatch overhead on every main loop cycle. + // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); +} void MDNSComponent::on_shutdown() { MDNS.close(); From 42bc0994f1da8754b47255f5c4aaa848f8bfb259 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht <rupprecht.thomas@gmail.com> Date: Wed, 11 Feb 2026 04:10:29 +0100 Subject: [PATCH 224/251] [rtttl] Code Improvements (#13653) Co-authored-by: Keith Burzinski <kbx81x@gmail.com> --- esphome/components/rtttl/rtttl.cpp | 412 +++++++++++++++-------------- esphome/components/rtttl/rtttl.h | 55 ++-- 2 files changed, 239 insertions(+), 228 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 5a64f37da4..6e86405b74 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -4,28 +4,72 @@ #include "esphome/core/log.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace rtttl { +namespace esphome::rtttl { static const char *const TAG = "rtttl"; -static const uint32_t DOUBLE_NOTE_GAP_MS = 10; - // These values can also be found as constants in the Tone library (Tone.h) static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; -static const uint16_t I2S_SPEED = 1000; +#if defined(USE_OUTPUT) || defined(USE_SPEAKER) +static const uint32_t DOUBLE_NOTE_GAP_MS = 10; +#endif // USE_OUTPUT || USE_SPEAKER -#undef HALF_PI -static const double HALF_PI = 1.5707963267948966192313216916398; +#ifdef USE_SPEAKER +static const size_t SAMPLE_BUFFER_SIZE = 2048; + +struct SpeakerSample { + int8_t left{0}; + int8_t right{0}; +}; inline double deg2rad(double degrees) { static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0; return degrees * PI_ON_180; } +#endif // USE_SPEAKER + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback +PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING", + "State::STOPPING", "UNKNOWN"); + +static const LogString *state_to_string(State state) { + return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX); +} +#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + +static uint8_t note_index_from_char(char note) { + switch (note) { + case 'c': + return 1; + // 'c#': 2 + case 'd': + return 3; + // 'd#': 4 + case 'e': + return 5; + case 'f': + return 6; + // 'f#': 7 + case 'g': + return 8; + // 'g#': 9 + case 'a': + return 10; + // 'a#': 11 + // Support both 'b' (English notation for B natural) and 'h' (German notation for B natural) + case 'b': + case 'h': + return 12; + case 'p': + default: + return 0; + } +} void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, @@ -34,161 +78,34 @@ void Rtttl::dump_config() { this->gain_); } -void Rtttl::play(std::string rtttl) { - if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { - size_t pos = this->rtttl_.find(':'); - size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); - ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); - return; - } - - this->rtttl_ = std::move(rtttl); - - this->default_duration_ = 4; - this->default_octave_ = 6; - this->note_duration_ = 0; - - int bpm = 63; - uint8_t num; - - // Get name - this->position_ = this->rtttl_.find(':'); - - // it's somewhat documented to be up to 10 characters but let's be a bit flexible here - if (this->position_ == std::string::npos || this->position_ > 15) { - ESP_LOGE(TAG, "Unable to determine name; missing ':'"); - return; - } - - ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); - - // get default duration - this->position_ = this->rtttl_.find("d=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing 'd='"); - return; - } - this->position_ += 2; - num = this->get_integer_(); - if (num > 0) - this->default_duration_ = num; - - // get default octave - this->position_ = this->rtttl_.find("o=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing 'o="); - return; - } - this->position_ += 2; - num = get_integer_(); - if (num >= 3 && num <= 7) - this->default_octave_ = num; - - // get BPM - this->position_ = this->rtttl_.find("b=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing b="); - return; - } - this->position_ += 2; - num = get_integer_(); - if (num != 0) - bpm = num; - - this->position_ = this->rtttl_.find(':', this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing second ':'"); - return; - } - this->position_++; - - // BPM usually expresses the number of quarter notes per minute - this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) - - this->output_freq_ = 0; - this->last_note_ = millis(); - this->note_duration_ = 1; - -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - this->set_state_(State::STATE_INIT); - this->samples_sent_ = 0; - this->samples_count_ = 0; - } -#endif -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->set_state_(State::STATE_RUNNING); - } -#endif -} - -void Rtttl::stop() { -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - if (this->speaker_->is_running()) { - this->speaker_->stop(); - } - this->set_state_(STATE_STOPPING); - } -#endif - this->position_ = this->rtttl_.length(); - this->note_duration_ = 0; -} - -void Rtttl::finish_() { - ESP_LOGV(TAG, "Rtttl::finish_()"); -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(State::STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); - this->speaker_->finish(); - this->set_state_(State::STATE_STOPPING); - } -#endif - // Ensure no more notes are played in case finish_() is called for an error. - this->position_ = this->rtttl_.length(); - this->note_duration_ = 0; -} - void Rtttl::loop() { - if (this->state_ == State::STATE_STOPPED) { + if (this->state_ == State::STOPPED) { this->disable_loop(); return; } +#ifdef USE_OUTPUT + if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) { + return; + } +#endif // USE_OUTPUT + #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { - if (this->state_ == State::STATE_STOPPING) { + if (this->state_ == State::STOPPING) { if (this->speaker_->is_stopped()) { - this->set_state_(State::STATE_STOPPED); + this->set_state_(State::STOPPED); } else { return; } - } else if (this->state_ == State::STATE_INIT) { + } else if (this->state_ == State::INIT) { if (this->speaker_->is_stopped()) { this->speaker_->start(); - this->set_state_(State::STATE_STARTING); + this->set_state_(State::STARTING); } - } else if (this->state_ == State::STATE_STARTING) { + } else if (this->state_ == State::STARTING) { if (this->speaker_->is_running()) { - this->set_state_(State::STATE_RUNNING); + this->set_state_(State::RUNNING); } } if (!this->speaker_->is_running()) { @@ -230,19 +147,17 @@ void Rtttl::loop() { } } } -#endif -#ifdef USE_OUTPUT - if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) - return; -#endif +#endif // USE_SPEAKER + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... - while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') + while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') { this->position_++; + } // first, get note duration, if available uint8_t num = this->get_integer_(); @@ -254,35 +169,8 @@ void Rtttl::loop() { this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after } - uint8_t note; + uint8_t note = note_index_from_char(this->rtttl_[this->position_]); - switch (this->rtttl_[this->position_]) { - case 'c': - note = 1; - break; - case 'd': - note = 3; - break; - case 'e': - note = 5; - break; - case 'f': - note = 6; - break; - case 'g': - note = 8; - break; - case 'a': - note = 10; - break; - case 'h': - case 'b': - note = 12; - break; - case 'p': - default: - note = 0; - } this->position_++; // now, get optional '#' sharp @@ -292,7 +180,7 @@ void Rtttl::loop() { } // now, get scale - uint8_t scale = get_integer_(); + uint8_t scale = this->get_integer_(); if (scale == 0) { scale = this->default_octave_; } @@ -345,7 +233,8 @@ void Rtttl::loop() { this->output_->set_level(0.0); } } -#endif +#endif // USE_OUTPUT + #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { this->samples_sent_ = 0; @@ -370,20 +259,152 @@ void Rtttl::loop() { } // Convert from frequency in Hz to high and low samples in fixed point } -#endif +#endif // USE_SPEAKER this->last_note_ = millis(); } -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE -// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback -PROGMEM_STRING_TABLE(RtttlStateStrings, "STATE_STOPPED", "STATE_INIT", "STATE_STARTING", "STATE_RUNNING", - "STATE_STOPPING", "UNKNOWN"); +void Rtttl::play(std::string rtttl) { + if (this->state_ != State::STOPPED && this->state_ != State::STOPPING) { + size_t pos = this->rtttl_.find(':'); + size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); + ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); + return; + } -static const LogString *state_to_string(State state) { - return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX); + this->rtttl_ = std::move(rtttl); + + this->default_duration_ = 4; + this->default_octave_ = 6; + this->note_duration_ = 0; + + int bpm = 63; + uint8_t num; + + // Get name + this->position_ = this->rtttl_.find(':'); + + // it's somewhat documented to be up to 10 characters but let's be a bit flexible here + if (this->position_ == std::string::npos || this->position_ > 15) { + ESP_LOGE(TAG, "Unable to determine name; missing ':'"); + return; + } + + ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); + + // get default duration + this->position_ = this->rtttl_.find("d=", this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'd='"); + return; + } + this->position_ += 2; + num = this->get_integer_(); + if (num > 0) { + this->default_duration_ = num; + } + + // get default octave + this->position_ = this->rtttl_.find("o=", this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'o="); + return; + } + this->position_ += 2; + num = this->get_integer_(); + if (num >= 3 && num <= 7) { + this->default_octave_ = num; + } + + // get BPM + this->position_ = this->rtttl_.find("b=", this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing b="); + return; + } + this->position_ += 2; + num = this->get_integer_(); + if (num != 0) { + bpm = num; + } + + this->position_ = this->rtttl_.find(':', this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing second ':'"); + return; + } + this->position_++; + + // BPM usually expresses the number of quarter notes per minute + this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + + this->output_freq_ = 0; + this->last_note_ = millis(); + this->note_duration_ = 1; + +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->set_state_(State::RUNNING); + } +#endif // USE_OUTPUT + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->set_state_(State::INIT); + this->samples_sent_ = 0; + this->samples_count_ = 0; + } +#endif // USE_SPEAKER +} + +void Rtttl::stop() { +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STOPPED); + } +#endif // USE_OUTPUT + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + if (this->speaker_->is_running()) { + this->speaker_->stop(); + } + this->set_state_(State::STOPPING); + } +#endif // USE_SPEAKER + + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; +} + +void Rtttl::finish_() { + ESP_LOGV(TAG, "Rtttl::finish_()"); + +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STOPPED); + } +#endif // USE_OUTPUT + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->finish(); + this->set_state_(State::STOPPING); + } +#endif // USE_SPEAKER + + // Ensure no more notes are played in case finish_() is called for an error. + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; } -#endif void Rtttl::set_state_(State state) { State old_state = this->state_; @@ -391,15 +412,14 @@ void Rtttl::set_state_(State state) { ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), LOG_STR_ARG(state_to_string(state))); - // Clear loop_done when transitioning from STOPPED to any other state - if (state == State::STATE_STOPPED) { + // Clear loop_done when transitioning from `State::STOPPED` to any other state + if (state == State::STOPPED) { this->disable_loop(); this->on_finished_playback_callback_.call(); ESP_LOGD(TAG, "Playback finished"); - } else if (old_state == State::STATE_STOPPED) { + } else if (old_state == State::STOPPED) { this->enable_loop(); } } -} // namespace rtttl -} // namespace esphome +} // namespace esphome::rtttl diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 1e924a897c..6f5df07766 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -5,48 +5,41 @@ #ifdef USE_OUTPUT #include "esphome/components/output/float_output.h" -#endif +#endif // USE_OUTPUT #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" -#endif +#endif // USE_SPEAKER -namespace esphome { -namespace rtttl { +namespace esphome::rtttl { -enum State : uint8_t { - STATE_STOPPED = 0, - STATE_INIT, - STATE_STARTING, - STATE_RUNNING, - STATE_STOPPING, +enum class State : uint8_t { + STOPPED = 0, + INIT, + STARTING, + RUNNING, + STOPPING, }; -#ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 2048; - -struct SpeakerSample { - int8_t left{0}; - int8_t right{0}; -}; -#endif - class Rtttl : public Component { public: #ifdef USE_OUTPUT void set_output(output::FloatOutput *output) { this->output_ = output; } -#endif +#endif // USE_OUTPUT + #ifdef USE_SPEAKER void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } -#endif - float get_gain() { return gain_; } - void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); } +#endif // USE_SPEAKER + + void dump_config() override; + void loop() override; void play(std::string rtttl); void stop(); - void dump_config() override; - bool is_playing() { return this->state_ != State::STATE_STOPPED; } - void loop() override; + float get_gain() { return this->gain_; } + void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); } + + bool is_playing() { return this->state_ != State::STOPPED; } void add_on_finished_playback_callback(std::function<void()> callback) { this->on_finished_playback_callback_.add(std::move(callback)); @@ -90,12 +83,12 @@ class Rtttl : public Component { /// The gain of the output. float gain_{0.6f}; /// The current state of the RTTTL player. - State state_{State::STATE_STOPPED}; + State state_{State::STOPPED}; #ifdef USE_OUTPUT /// The output to write the sound to. output::FloatOutput *output_; -#endif +#endif // USE_OUTPUT #ifdef USE_SPEAKER /// The speaker to write the sound to. @@ -110,8 +103,7 @@ class Rtttl : public Component { int samples_count_{0}; /// The number of samples for the gap between notes. int samples_gap_{0}; - -#endif +#endif // USE_SPEAKER /// The callback to call when playback is finished. CallbackManager<void()> on_finished_playback_callback_; @@ -145,5 +137,4 @@ class FinishedPlaybackTrigger : public Trigger<> { } }; -} // namespace rtttl -} // namespace esphome +} // namespace esphome::rtttl From e3bafc1b45502971f699a5746685f2f91e6ea29a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 21:29:29 -0600 Subject: [PATCH 225/251] [esp32_ble] Extract state transitions from ESP32BLE::loop() hot path (#13903) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32_ble/ble.cpp | 70 ++++++++++++++-------------- esphome/components/esp32_ble/ble.h | 4 ++ 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 87b5e2b738..acbe9d88fc 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -369,42 +369,9 @@ bool ESP32BLE::ble_dismantle_() { } void ESP32BLE::loop() { - switch (this->state_) { - case BLE_COMPONENT_STATE_OFF: - case BLE_COMPONENT_STATE_DISABLED: - return; - case BLE_COMPONENT_STATE_DISABLE: { - ESP_LOGD(TAG, "Disabling"); - -#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - for (auto *ble_event_handler : this->ble_status_event_handlers_) { - ble_event_handler->ble_before_disabled_event_handler(); - } -#endif - - if (!ble_dismantle_()) { - ESP_LOGE(TAG, "Could not be dismantled"); - this->mark_failed(); - return; - } - this->state_ = BLE_COMPONENT_STATE_DISABLED; - return; - } - case BLE_COMPONENT_STATE_ENABLE: { - ESP_LOGD(TAG, "Enabling"); - this->state_ = BLE_COMPONENT_STATE_OFF; - - if (!ble_setup_()) { - ESP_LOGE(TAG, "Could not be set up"); - this->mark_failed(); - return; - } - - this->state_ = BLE_COMPONENT_STATE_ACTIVE; - return; - } - case BLE_COMPONENT_STATE_ACTIVE: - break; + if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) { + this->loop_handle_state_transition_not_active_(); + return; } BLEEvent *ble_event = this->ble_events_.pop(); @@ -520,6 +487,37 @@ void ESP32BLE::loop() { } } +void ESP32BLE::loop_handle_state_transition_not_active_() { + // Caller ensures state_ != ACTIVE + if (this->state_ == BLE_COMPONENT_STATE_DISABLE) { + ESP_LOGD(TAG, "Disabling"); + +#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT + for (auto *ble_event_handler : this->ble_status_event_handlers_) { + ble_event_handler->ble_before_disabled_event_handler(); + } +#endif + + if (!ble_dismantle_()) { + ESP_LOGE(TAG, "Could not be dismantled"); + this->mark_failed(); + return; + } + this->state_ = BLE_COMPONENT_STATE_DISABLED; + } else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) { + ESP_LOGD(TAG, "Enabling"); + this->state_ = BLE_COMPONENT_STATE_OFF; + + if (!ble_setup_()) { + ESP_LOGE(TAG, "Could not be set up"); + this->mark_failed(); + return; + } + + this->state_ = BLE_COMPONENT_STATE_ACTIVE; + } +} + // Helper function to load new event data based on type void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { event->load_gap_event(e, p); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 1999c870f8..f1ab81b6dc 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -155,6 +155,10 @@ class ESP32BLE : public Component { #endif static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + // Handle DISABLE and ENABLE transitions when not in the ACTIVE state. + // Other non-ACTIVE states (e.g. OFF, DISABLED) are currently treated as no-ops. + void __attribute__((noinline)) loop_handle_state_transition_not_active_(); + bool ble_setup_(); bool ble_dismantle_(); bool ble_pre_setup_(); From 5281fd32730a4669cab2706c22d625b3fc12e909 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 21:30:34 -0600 Subject: [PATCH 226/251] [api] Extract cold code from APIConnection::loop() hot path (#13901) --- esphome/components/api/api_connection.cpp | 74 ++++++++++++++--------- esphome/components/api/api_connection.h | 23 +++---- esphome/components/api/list_entities.h | 1 - esphome/components/api/subscribe_state.h | 1 - esphome/core/component_iterator.h | 1 + 5 files changed, 56 insertions(+), 44 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e51689fd07..14ccbd2248 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -219,35 +219,8 @@ void APIConnection::loop() { this->process_batch_(); } - switch (this->active_iterator_) { - case ActiveIterator::LIST_ENTITIES: - if (this->iterator_storage_.list_entities.completed()) { - this->destroy_active_iterator_(); - if (this->flags_.state_subscription) { - this->begin_iterator_(ActiveIterator::INITIAL_STATE); - } - } else { - this->process_iterator_batch_(this->iterator_storage_.list_entities); - } - break; - case ActiveIterator::INITIAL_STATE: - if (this->iterator_storage_.initial_state.completed()) { - this->destroy_active_iterator_(); - // Process any remaining batched messages immediately - if (!this->deferred_batch_.empty()) { - this->process_batch_(); - } - // Now that everything is sent, enable immediate sending for future state changes - this->flags_.should_try_send_immediately = true; - // Release excess memory from buffers that grew during initial sync - this->deferred_batch_.release_buffer(); - this->helper_->release_buffers(); - } else { - this->process_iterator_batch_(this->iterator_storage_.initial_state); - } - break; - case ActiveIterator::NONE: - break; + if (this->active_iterator_ != ActiveIterator::NONE) { + this->process_active_iterator_(); } if (this->flags_.sent_ping) { @@ -283,6 +256,49 @@ void APIConnection::loop() { #endif } +void APIConnection::process_active_iterator_() { + // Caller ensures active_iterator_ != NONE + if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) { + if (this->iterator_storage_.list_entities.completed()) { + this->destroy_active_iterator_(); + if (this->flags_.state_subscription) { + this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } + } else { + this->process_iterator_batch_(this->iterator_storage_.list_entities); + } + } else { // INITIAL_STATE + if (this->iterator_storage_.initial_state.completed()) { + this->destroy_active_iterator_(); + // Process any remaining batched messages immediately + if (!this->deferred_batch_.empty()) { + this->process_batch_(); + } + // Now that everything is sent, enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); + } else { + this->process_iterator_batch_(this->iterator_storage_.initial_state); + } + } +} + +void APIConnection::process_iterator_batch_(ComponentIterator &iterator) { + size_t initial_size = this->deferred_batch_.size(); + size_t max_batch = this->get_max_batch_size_(); + while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { + iterator.advance(); + } + + // If the batch is full, process it immediately + // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() + if (this->deferred_batch_.size() >= max_batch) { + this->process_batch_(); + } +} + bool APIConnection::send_disconnect_response_() { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index abcb162865..a16d681760 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -15,6 +15,10 @@ #include <limits> #include <vector> +namespace esphome { +class ComponentIterator; +} // namespace esphome + namespace esphome::api { // Keepalive timeout in milliseconds @@ -366,20 +370,13 @@ class APIConnection final : public APIServerConnectionBase { return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; } - // Helper method to process multiple entities from an iterator in a batch - template<typename Iterator> void process_iterator_batch_(Iterator &iterator) { - size_t initial_size = this->deferred_batch_.size(); - size_t max_batch = this->get_max_batch_size_(); - while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { - iterator.advance(); - } + // Process active iterator (list_entities/initial_state) during connection setup. + // Extracted from loop() — only runs during initial handshake, NONE in steady state. + void __attribute__((noinline)) process_active_iterator_(); - // If the batch is full, process it immediately - // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() - if (this->deferred_batch_.size() >= max_batch) { - this->process_batch_(); - } - } + // Helper method to process multiple entities from an iterator in a batch. + // Takes ComponentIterator base class reference to avoid duplicate template instantiations. + void process_iterator_batch_(ComponentIterator &iterator); #ifdef USE_BINARY_SENSOR static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index bef36dd015..90769f9a81 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -94,7 +94,6 @@ class ListEntitiesIterator : public ComponentIterator { bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; - bool completed() { return this->state_ == IteratorState::NONE; } protected: APIConnection *client_; diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 3c9f33835a..6f8577ca7b 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -88,7 +88,6 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_UPDATE bool on_update(update::UpdateEntity *entity) override; #endif - bool completed() { return this->state_ == IteratorState::NONE; } protected: APIConnection *client_; diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index e13d81a8e4..6c03b74a17 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -26,6 +26,7 @@ class ComponentIterator { public: void begin(bool include_internal = false); void advance(); + bool completed() const { return this->state_ == IteratorState::NONE; } virtual bool on_begin(); #ifdef USE_BINARY_SENSOR virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0; From 225c13326a1c6cc88d3eb9b37ddd9257517fc22b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 21:41:07 -0600 Subject: [PATCH 227/251] [core] Extract dump_config from Application::loop() hot path (#13900) --- esphome/core/application.cpp | 46 ++++++++++++++++++++---------------- esphome/core/application.h | 5 ++++ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0daabc0282..7b5435185d 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -204,36 +204,40 @@ void Application::loop() { this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { - if (this->dump_config_at_ == 0) { - char build_time_str[Application::BUILD_TIME_STR_SIZE]; - this->get_build_time_string(build_time_str); - ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str); + this->process_dump_config_(); + } +} + +void Application::process_dump_config_() { + if (this->dump_config_at_ == 0) { + char build_time_str[Application::BUILD_TIME_STR_SIZE]; + this->get_build_time_string(build_time_str); + ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str); #ifdef ESPHOME_PROJECT_NAME - ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); + ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); #endif #ifdef USE_ESP32 - esp_chip_info_t chip_info; - esp_chip_info(&chip_info); - ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, - chip_info.revision % 100, chip_info.cores); + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, + chip_info.revision % 100, chip_info.cores); #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) - // Suggest optimization for chips that don't need the PSRAM cache workaround - if (chip_info.revision >= 300) { + // Suggest optimization for chips that don't need the PSRAM cache workaround + if (chip_info.revision >= 300) { #ifdef USE_PSRAM - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, - chip_info.revision % 100); + ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, + chip_info.revision % 100); #else - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, - chip_info.revision % 100); -#endif - } -#endif + ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, + chip_info.revision % 100); #endif } - - this->components_[this->dump_config_at_]->call_dump_config(); - this->dump_config_at_++; +#endif +#endif } + + this->components_[this->dump_config_at_]->call_dump_config(); + this->dump_config_at_++; } void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { diff --git a/esphome/core/application.h b/esphome/core/application.h index 592bf809f1..8478100a56 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -519,6 +519,11 @@ class Application { void before_loop_tasks_(uint32_t loop_start_time); void after_loop_tasks_(); + /// Process dump_config output one component per loop iteration. + /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. + /// Caller must ensure dump_config_at_ < components_.size(). + void __attribute__((noinline)) process_dump_config_(); + void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness From 38bba3f5a276310a24787331d333a6eae794963d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Feb 2026 21:42:13 -0600 Subject: [PATCH 228/251] [scheduler] Reduce set_timer_common_ hot path size by 25% (#13899) --- esphome/core/scheduler.cpp | 106 ++++++++++++++++++------------------- esphome/core/scheduler.h | 42 +++++++-------- 2 files changed, 73 insertions(+), 75 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 97ac28b623..4194c3aa9e 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -107,6 +107,24 @@ static void validate_static_string(const char *name) { // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to // avoid the main thread modifying the list while it is being accessed. +// Calculate random offset for interval timers +// Extracted from set_timer_common_ to reduce code size - float math + random_float() +// only needed for intervals, not timeouts +uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { + return static_cast<uint32_t>(std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); +} + +// Check if a retry was already cancelled in items_ or to_add_ +// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated +// Remove before 2026.8.0 along with all retry code +bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id) { + return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true); +} + // Common implementation for both timeout and interval // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, @@ -130,84 +148,66 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Create and populate the scheduler item auto item = this->get_item_from_pool_locked_(); item->component = component; - switch (name_type) { - case NameType::STATIC_STRING: - item->set_static_name(static_name); - break; - case NameType::HASHED_STRING: - item->set_hashed_name(hash_or_id); - break; - case NameType::NUMERIC_ID: - item->set_numeric_id(hash_or_id); - break; - case NameType::NUMERIC_ID_INTERNAL: - item->set_internal_id(hash_or_id); - break; - } + item->set_name(name_type, static_name, hash_or_id); item->type = type; item->callback = std::move(func); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item.get(), false); item->is_retry = is_retry; + // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. + // Using a pointer lets both paths share the cancel + push_back epilogue. + auto *target = &this->to_add_; + #ifndef ESPHOME_THREAD_SINGLE // Special handling for defer() (delay = 0, type = TIMEOUT) // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution - if (!skip_cancel) { - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); - } - this->defer_queue_.push_back(std::move(item)); - return; - } + target = &this->defer_queue_; + } else #endif /* not ESPHOME_THREAD_SINGLE */ - - // Type-specific setup - if (type == SchedulerItem::INTERVAL) { - item->interval = delay; - // first execution happens immediately after a random smallish offset - // Calculate random offset (0 to min(interval/2, 5s)) - uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); - item->set_next_execution(now + offset); + { + // Type-specific setup + if (type == SchedulerItem::INTERVAL) { + item->interval = delay; + // first execution happens immediately after a random smallish offset + uint32_t offset = this->calculate_interval_offset_(delay); + item->set_next_execution(now + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE - SchedulerNameLog name_log; - ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", - name_log.format(name_type, static_name, hash_or_id), delay, offset); + SchedulerNameLog name_log; + ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", + name_log.format(name_type, static_name, hash_or_id), delay, offset); #endif - } else { - item->interval = 0; - item->set_next_execution(now + delay); - } + } else { + item->interval = 0; + item->set_next_execution(now + delay); + } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ - // For retries, check if there's a cancelled timeout first - // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name - if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true) || - has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true))) { - // Skip scheduling - the retry was cancelled + // For retries, check if there's a cancelled timeout first + // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name + if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && + type == SchedulerItem::TIMEOUT && + this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { + // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER - SchedulerNameLog skip_name_log; - ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", - skip_name_log.format(name_type, static_name, hash_or_id)); + SchedulerNameLog skip_name_log; + ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", + skip_name_log.format(name_type, static_name, hash_or_id)); #endif - return; + return; + } } - // If name is provided, do atomic cancel-and-add (unless skip_cancel is true) - // Cancel existing items + // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) if (!skip_cancel) { this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(std::move(item)); + target->push_back(std::move(item)); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ede729d164..394178a831 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,28 +219,15 @@ class Scheduler { // Helper to get the name type NameType get_name_type() const { return name_type_; } - // Helper to set a static string name (no allocation) - void set_static_name(const char *name) { - name_.static_name = name; - name_type_ = NameType::STATIC_STRING; - } - - // Helper to set a hashed string name (hash computed from std::string) - void set_hashed_name(uint32_t hash) { - name_.hash_or_id = hash; - name_type_ = NameType::HASHED_STRING; - } - - // Helper to set a numeric ID name - void set_numeric_id(uint32_t id) { - name_.hash_or_id = id; - name_type_ = NameType::NUMERIC_ID; - } - - // Helper to set an internal numeric ID (separate namespace from NUMERIC_ID) - void set_internal_id(uint32_t id) { - name_.hash_or_id = id; - name_type_ = NameType::NUMERIC_ID_INTERNAL; + // Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id. + // Both union members occupy the same offset, so only one store is needed. + void set_name(NameType type, const char *static_name, uint32_t hash_or_id) { + if (type == NameType::STATIC_STRING) { + name_.static_name = static_name; + } else { + name_.hash_or_id = hash_or_id; + } + name_type_ = type; } static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b); @@ -355,6 +342,17 @@ class Scheduler { // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); + // Helper to calculate random offset for interval timers - extracted to reduce code size of set_timer_common_ + // IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash. + uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay); + + // Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_ + // Remove before 2026.8.0 along with all retry code. + // IMPORTANT: Must not be inlined - retry path is cold and deprecated. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + bool __attribute__((noinline)) + is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); + #ifdef ESPHOME_DEBUG_SCHEDULER // Helper for debug logging in set_timer_common_ - extracted to reduce code size void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id, From 4fb1ddf21204f562600641a77fca7cc226e4792a Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:40:21 +0100 Subject: [PATCH 229/251] [api] Fix compiler format warnings (#13931) --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/proto.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 14ccbd2248..34d9744adc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1510,7 +1510,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; char peername[socket::SOCKADDR_STR_LEN]; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 2a0ddf91db..764dd3f391 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -133,7 +133,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { break; } default: - ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer)); + ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer)); return; } } From 8e785a22161684aa8aab084895d02e208e4dc4e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 08:40:41 -0600 Subject: [PATCH 230/251] [web_server] Remove unnecessary packed attribute from DeferredEvent (#13932) --- esphome/components/web_server/web_server.h | 12 +++++++----- esphome/components/web_server_idf/web_server_idf.h | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 224c051ece..ce09ebf7a9 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -112,10 +112,10 @@ class DeferredUpdateEventSource : public AsyncEventSource { /* This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for - the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a - std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per - entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing - because of dedup) would take up only 0.8 kB. + the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a + std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two + pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors + publishing because of dedup) would take up only 0.8 kB. */ struct DeferredEvent { friend class DeferredUpdateEventSource; @@ -130,7 +130,9 @@ class DeferredUpdateEventSource : public AsyncEventSource { bool operator==(const DeferredEvent &test) const { return (source_ == test.source_ && message_generator_ == test.message_generator_); } - } __attribute__((packed)); + }; + static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *), + "DeferredEvent should have no padding"); protected: // surface a couple methods from the base class diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 1760544963..6a409de74e 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -259,9 +259,9 @@ using message_generator_t = std::string(esphome::web_server::WebServer *, void * /* This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for - the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a - std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per - entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing + the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a + std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers + per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing because of dedup) would take up only 0.8 kB. */ struct DeferredEvent { @@ -277,7 +277,9 @@ struct DeferredEvent { bool operator==(const DeferredEvent &test) const { return (source_ == test.source_ && message_generator_ == test.message_generator_); } -} __attribute__((packed)); +}; +static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *), + "DeferredEvent should have no padding"); class AsyncEventSourceResponse { friend class AsyncEventSource; From 37f97c9043d260600a5a853ff451bb634f7ad2f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 08:41:15 -0600 Subject: [PATCH 231/251] [esp8266][rp2040] Eliminate heap fallback in preference save/load (#13928) --- esphome/components/esp8266/preferences.cpp | 29 +++++++++++----------- esphome/components/rp2040/preferences.cpp | 23 +++++++++-------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index f037b881a8..e749b1f633 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -33,6 +33,10 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255; #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) +// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false). +// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need +// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so +// 64 words (256 bytes) suffices since most preferences go to RTC memory instead. #ifdef USE_ESP8266_PREFERENCES_FLASH static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; #else @@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { return true; } -// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data) -// This handles virtually all real-world preferences without heap allocation -static constexpr size_t PREF_BUFFER_WORDS = 16; +// Maximum buffer for any single preference - bounded by storage sizes. +// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words). +// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions. +static constexpr size_t PREF_MAX_BUFFER_WORDS = + ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS; class ESP8266PreferenceBackend : public ESPPreferenceBackend { public: @@ -141,15 +147,13 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { bool save(const uint8_t *data, size_t len) override { if (bytes_to_words(len) != this->length_words) return false; - const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; - SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size); - uint32_t *buffer = buffer_alloc.get(); + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; memset(buffer, 0, buffer_size * sizeof(uint32_t)); - memcpy(buffer, data, len); buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); - return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) : save_to_rtc(this->offset, buffer, buffer_size); } @@ -157,19 +161,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { bool load(uint8_t *data, size_t len) override { if (bytes_to_words(len) != this->length_words) return false; - const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; - SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size); - uint32_t *buffer = buffer_alloc.get(); - + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) : load_from_rtc(this->offset, buffer, buffer_size); if (!ret) return false; - if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) return false; - memcpy(data, buffer, len); return true; } diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index 172da32adc..fa72fd9a24 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -25,8 +25,8 @@ static uint8_t s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation -static constexpr size_t PREF_BUFFER_SIZE = 64; +// No preference can exceed the total flash storage, so stack buffer covers all cases. +static constexpr size_t PREF_MAX_BUFFER_SIZE = RP2040_FLASH_STORAGE_SIZE; extern "C" uint8_t _EEPROM_start; @@ -46,14 +46,14 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend { bool save(const uint8_t *data, size_t len) override { const size_t buffer_size = len + 1; - SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size); - uint8_t *buffer = buffer_alloc.get(); - + if (buffer_size > PREF_MAX_BUFFER_SIZE) + return false; + uint8_t buffer[PREF_MAX_BUFFER_SIZE]; memcpy(buffer, data, len); - buffer[len] = calculate_crc(buffer, buffer + len, type); + buffer[len] = calculate_crc(buffer, buffer + len, this->type); for (size_t i = 0; i < buffer_size; i++) { - uint32_t j = offset + i; + uint32_t j = this->offset + i; if (j >= RP2040_FLASH_STORAGE_SIZE) return false; uint8_t v = buffer[i]; @@ -66,17 +66,18 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend { } bool load(uint8_t *data, size_t len) override { const size_t buffer_size = len + 1; - SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size); - uint8_t *buffer = buffer_alloc.get(); + if (buffer_size > PREF_MAX_BUFFER_SIZE) + return false; + uint8_t buffer[PREF_MAX_BUFFER_SIZE]; for (size_t i = 0; i < buffer_size; i++) { - uint32_t j = offset + i; + uint32_t j = this->offset + i; if (j >= RP2040_FLASH_STORAGE_SIZE) return false; buffer[i] = s_flash_storage[j]; } - uint8_t crc = calculate_crc(buffer, buffer + len, type); + uint8_t crc = calculate_crc(buffer, buffer + len, this->type); if (buffer[len] != crc) { return false; } From 9bdae5183ca87c99d44381d01cb4c20b4f7ecdab Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Wed, 11 Feb 2026 16:43:55 +0100 Subject: [PATCH 232/251] [nrf52,logger] add support for task_log_buffer_size (#13862) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/logger/__init__.py | 14 +- esphome/components/logger/log_buffer.h | 190 +++++++++++++++ esphome/components/logger/logger.cpp | 91 +++----- esphome/components/logger/logger.h | 219 ++---------------- esphome/components/logger/logger_zephyr.cpp | 2 +- .../logger/task_log_buffer_esp32.cpp | 17 +- .../components/logger/task_log_buffer_esp32.h | 5 +- .../logger/task_log_buffer_host.cpp | 23 +- .../components/logger/task_log_buffer_host.h | 13 +- .../logger/task_log_buffer_libretiny.cpp | 22 +- .../logger/task_log_buffer_libretiny.h | 8 +- .../logger/task_log_buffer_zephyr.cpp | 116 ++++++++++ .../logger/task_log_buffer_zephyr.h | 66 ++++++ esphome/core/defines.h | 1 + .../logger/test.nrf52-adafruit.yaml | 1 + 15 files changed, 479 insertions(+), 309 deletions(-) create mode 100644 esphome/components/logger/log_buffer.h create mode 100644 esphome/components/logger/task_log_buffer_zephyr.cpp create mode 100644 esphome/components/logger/task_log_buffer_zephyr.h diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 40ceaec7dc..b2952d7995 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All( bk72xx=768, ln882x=768, rtl87xx=768, + nrf52=768, ): cv.All( cv.only_on( - [PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX] + [ + PLATFORM_ESP32, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + PLATFORM_NRF52, + ] ), cv.validate_bytes, cv.Any( @@ -313,11 +320,13 @@ async def to_code(config): ) if CORE.is_esp32: cg.add(log.create_pthread_key()) - if CORE.is_esp32 or CORE.is_libretiny: + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") cg.add(log.init_log_buffer(task_log_buffer_size)) + if CORE.using_zephyr: + zephyr_add_prj_conf("MPSC_PBUF", True) elif CORE.is_host: cg.add(log.create_pthread_key()) cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") @@ -417,6 +426,7 @@ async def to_code(config): pass if CORE.is_nrf52: + zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True) if config[CONF_HARDWARE_UART] == UART0: zephyr_add_overlay("""&uart0 { status = "okay";};""") if config[CONF_HARDWARE_UART] == UART1: diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h new file mode 100644 index 0000000000..3d87278248 --- /dev/null +++ b/esphome/components/logger/log_buffer.h @@ -0,0 +1,190 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::logger { + +// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) +static constexpr uint16_t MAX_HEADER_SIZE = 128; + +// ANSI color code last digit (30-38 range, store only last digit to save RAM) +static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { + '\0', // NONE + '1', // ERROR (31 = red) + '3', // WARNING (33 = yellow) + '2', // INFO (32 = green) + '5', // CONFIG (35 = magenta) + '6', // DEBUG (36 = cyan) + '7', // VERBOSE (37 = gray) + '8', // VERY_VERBOSE (38 = white) +}; + +static constexpr char LOG_LEVEL_LETTER_CHARS[] = { + '\0', // NONE + 'E', // ERROR + 'W', // WARNING + 'I', // INFO + 'C', // CONFIG + 'D', // DEBUG + 'V', // VERBOSE (VERY_VERBOSE uses two 'V's) +}; + +// Buffer wrapper for log formatting functions +struct LogBuffer { + char *data; + uint16_t size; + uint16_t pos{0}; + // Replaces the null terminator with a newline for console output. + // Must be called after notify_listeners_() since listeners need null-terminated strings. + // Console output uses length-based writes (buf.pos), so null terminator is not needed. + void terminate_with_newline() { + if (this->pos < this->size) { + this->data[this->pos++] = '\n'; + } else if (this->size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + this->data[this->size - 1] = '\n'; + this->pos = this->size; + } + } + void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) { + // Early return if insufficient space - intentionally don't update pos to prevent partial writes + if (this->pos + MAX_HEADER_SIZE > this->size) + return; + + char *p = this->current_(); + + // Write ANSI color + this->write_ansi_color_(p, level); + + // Construct: [LEVEL][tag:line] + *p++ = '['; + if (level != 0) { + if (level >= 7) { + *p++ = 'V'; // VERY_VERBOSE = "VV" + *p++ = 'V'; + } else { + *p++ = LOG_LEVEL_LETTER_CHARS[level]; + } + } + *p++ = ']'; + *p++ = '['; + + // Copy tag + this->copy_string_(p, tag); + + *p++ = ':'; + + // Format line number without modulo operations + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + *p++ = '0' + thousands; + line -= thousands * 1000; + } + int hundreds = line / 100; + int remainder = line - hundreds * 100; + int tens = remainder / 10; + *p++ = '0' + hundreds; + *p++ = '0' + tens; + *p++ = '0' + (remainder - tens * 10); + *p++ = ']'; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) + // Write thread name with bold red color + if (thread_name != nullptr) { + this->write_ansi_color_(p, 1); // Bold red for thread name + *p++ = '['; + this->copy_string_(p, thread_name); + *p++ = ']'; + this->write_ansi_color_(p, level); // Restore original color + } +#endif + + *p++ = ':'; + *p++ = ' '; + + this->pos = p - this->data; + } + void HOT format_body(const char *format, va_list args) { + this->format_vsnprintf_(format, args); + this->finalize_(); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void HOT format_body_P(PGM_P format, va_list args) { + this->format_vsnprintf_P_(format, args); + this->finalize_(); + } +#endif + void write_body(const char *text, uint16_t text_length) { + this->write_(text, text_length); + this->finalize_(); + } + + private: + bool full_() const { return this->pos >= this->size; } + uint16_t remaining_() const { return this->size - this->pos; } + char *current_() { return this->data + this->pos; } + void write_(const char *value, uint16_t length) { + const uint16_t available = this->remaining_(); + const uint16_t copy_len = (length < available) ? length : available; + if (copy_len > 0) { + memcpy(this->current_(), value, copy_len); + this->pos += copy_len; + } + } + void finalize_() { + // Write color reset sequence + static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; + this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); + // Null terminate + this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; + } + void strip_trailing_newlines_() { + while (this->pos > 0 && this->data[this->pos - 1] == '\n') + this->pos--; + } + void process_vsnprintf_result_(int ret) { + if (ret < 0) + return; + const uint16_t rem = this->remaining_(); + this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret); + this->strip_trailing_newlines_(); + } + void format_vsnprintf_(const char *format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args)); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void format_vsnprintf_P_(PGM_P format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); + } +#endif + // Write ANSI color escape sequence to buffer, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void write_ansi_color_(char *&p, uint8_t level) { + if (level == 0) + return; + // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes) + *p++ = '\033'; + *p++ = '['; + *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold + *p++ = ';'; + *p++ = '3'; + *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = 'm'; + } + // Copy string without null terminator, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void copy_string_(char *&p, const char *str) { + const size_t len = strlen(str); + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by + // piece + memcpy(p, str, len); + p += len; + } +}; + +} // namespace esphome::logger diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index bb20c403e5..e1b49bcb61 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -10,9 +10,9 @@ namespace esphome::logger { static const char *const TAG = "logger"; -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) -// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS) -// Main thread/task always uses direct buffer access for console output and callbacks +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS, +// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks // // For non-main threads/tasks: // - WITH task log buffer: Prefer sending to ring buffer for async processing @@ -31,6 +31,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Get task handle once - used for both main task check and passing to non-main thread handler TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); const bool is_main_task = (current_task == this->main_task_); +#elif (USE_ZEPHYR) + k_tid_t current_task = k_current_get(); + const bool is_main_task = (current_task == this->main_task_); #else // USE_HOST const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_); #endif @@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Host: pass a stack buffer for pthread_getname_np to write into. #if defined(USE_ESP32) || defined(USE_LIBRETINY) const char *thread_name = get_thread_name_(current_task); +#elif defined(USE_ZEPHYR) + char thread_name_buf[MAX_POINTER_REPRESENTATION]; + const char *thread_name = get_thread_name_(thread_name_buf, current_task); #else // USE_HOST char thread_name_buf[THREAD_NAME_BUF_SIZE]; const char *thread_name = this->get_thread_name_(thread_name_buf); @@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li // This is safe to call from any context including ISRs this->enable_loop_soon_any_context(); } -#endif // USE_ESPHOME_TASK_LOG_BUFFER - +#endif // Emergency console logging for non-main threads when ring buffer is full or disabled // This is a fallback mechanism to ensure critical log messages are visible // Note: This may cause interleaved/corrupted console output if multiple threads // log simultaneously, but it's better than losing important messages entirely #ifdef USE_HOST - if (!message_sent) { + if (!message_sent) +#else + if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console +#endif + { +#ifdef USE_HOST // Host always has console output - no baud_rate check needed static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512; #else - if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; #endif @@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li // RAII guard automatically resets on return } #else -// Implementation for single-task platforms (ESP8266, RP2040, Zephyr) -// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path. +// Implementation for single-task platforms (ESP8266, RP2040) // Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking. // Not a problem in practice yet since Zephyr has no API support (logs are console-only). void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; -#ifdef USE_ZEPHYR - char tmp[MAX_POINTER_REPRESENTATION]; - this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, - this->get_thread_name_(tmp)); -#else // Other single-task platforms don't have thread names, so pass nullptr + // Other single-task platforms don't have thread names, so pass nullptr this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr); -#endif } -#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY +#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. @@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate } #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer(size_t total_buffer_size) { -#ifdef USE_HOST // Host uses slot count instead of byte size - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size); -#elif defined(USE_ESP32) // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); -#elif defined(USE_LIBRETINY) - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size); -#endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// Zephyr needs loop working to check when CDC port is open +#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) // Start with loop disabled when using task buffer (unless using USB CDC on ESP32) // The loop will be enabled automatically when messages arrive this->disable_loop_when_buffer_empty_(); @@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) { } #endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::loop() { this->process_messages_(); } +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +void Logger::loop() { + this->process_messages_(); +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) + this->cdc_loop_(); +#endif +} #endif void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available if (this->log_buffer_->has_messages()) { -#ifdef USE_HOST - logger::TaskLogBufferHost::LogMessage *message; - while (this->log_buffer_->get_message_main_loop(&message)) { - const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; - this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text, - message->text_length, buf); - this->log_buffer_->release_message_main_loop(); - this->write_log_buffer_to_console_(buf); - } -#elif defined(USE_ESP32) logger::TaskLogBuffer::LogMessage *message; - const char *text; - void *received_token; - while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { + uint16_t text_length; + while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; - this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length, buf); - // Release the message to allow other tasks to use it as soon as possible - this->log_buffer_->release_message_main_loop(received_token); - this->write_log_buffer_to_console_(buf); - } -#elif defined(USE_LIBRETINY) - logger::TaskLogBufferLibreTiny::LogMessage *message; - const char *text; - while (this->log_buffer_->borrow_message_main_loop(&message, &text)) { - const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; - this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length, buf); + this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, + message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(); this->write_log_buffer_to_console_(buf); } -#endif } -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// Zephyr needs loop working to check when CDC port is open +#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index ecf032ee0e..835542dd8f 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -13,15 +13,11 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -#ifdef USE_HOST +#include "log_buffer.h" #include "task_log_buffer_host.h" -#elif defined(USE_ESP32) #include "task_log_buffer_esp32.h" -#elif defined(USE_LIBRETINY) #include "task_log_buffer_libretiny.h" -#endif -#endif +#include "task_log_buffer_zephyr.h" #ifdef USE_ARDUINO #if defined(USE_ESP8266) @@ -97,195 +93,10 @@ struct CStrCompare { }; #endif -// ANSI color code last digit (30-38 range, store only last digit to save RAM) -static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { - '\0', // NONE - '1', // ERROR (31 = red) - '3', // WARNING (33 = yellow) - '2', // INFO (32 = green) - '5', // CONFIG (35 = magenta) - '6', // DEBUG (36 = cyan) - '7', // VERBOSE (37 = gray) - '8', // VERY_VERBOSE (38 = white) -}; - -static constexpr char LOG_LEVEL_LETTER_CHARS[] = { - '\0', // NONE - 'E', // ERROR - 'W', // WARNING - 'I', // INFO - 'C', // CONFIG - 'D', // DEBUG - 'V', // VERBOSE (VERY_VERBOSE uses two 'V's) -}; - -// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) -static constexpr uint16_t MAX_HEADER_SIZE = 128; - -// "0x" + 2 hex digits per byte + '\0' -static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; - // Stack buffer size for retrieving thread/task names from the OS // macOS allows up to 64 bytes, Linux up to 16 static constexpr size_t THREAD_NAME_BUF_SIZE = 64; -// Buffer wrapper for log formatting functions -struct LogBuffer { - char *data; - uint16_t size; - uint16_t pos{0}; - // Replaces the null terminator with a newline for console output. - // Must be called after notify_listeners_() since listeners need null-terminated strings. - // Console output uses length-based writes (buf.pos), so null terminator is not needed. - void terminate_with_newline() { - if (this->pos < this->size) { - this->data[this->pos++] = '\n'; - } else if (this->size > 0) { - // Buffer was full - replace last char with newline to ensure it's visible - this->data[this->size - 1] = '\n'; - this->pos = this->size; - } - } - void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) { - // Early return if insufficient space - intentionally don't update pos to prevent partial writes - if (this->pos + MAX_HEADER_SIZE > this->size) - return; - - char *p = this->current_(); - - // Write ANSI color - this->write_ansi_color_(p, level); - - // Construct: [LEVEL][tag:line] - *p++ = '['; - if (level != 0) { - if (level >= 7) { - *p++ = 'V'; // VERY_VERBOSE = "VV" - *p++ = 'V'; - } else { - *p++ = LOG_LEVEL_LETTER_CHARS[level]; - } - } - *p++ = ']'; - *p++ = '['; - - // Copy tag - this->copy_string_(p, tag); - - *p++ = ':'; - - // Format line number without modulo operations - if (line > 999) [[unlikely]] { - int thousands = line / 1000; - *p++ = '0' + thousands; - line -= thousands * 1000; - } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - *p++ = '0' + hundreds; - *p++ = '0' + tens; - *p++ = '0' + (remainder - tens * 10); - *p++ = ']'; - -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) - // Write thread name with bold red color - if (thread_name != nullptr) { - this->write_ansi_color_(p, 1); // Bold red for thread name - *p++ = '['; - this->copy_string_(p, thread_name); - *p++ = ']'; - this->write_ansi_color_(p, level); // Restore original color - } -#endif - - *p++ = ':'; - *p++ = ' '; - - this->pos = p - this->data; - } - void HOT format_body(const char *format, va_list args) { - this->format_vsnprintf_(format, args); - this->finalize_(); - } -#ifdef USE_STORE_LOG_STR_IN_FLASH - void HOT format_body_P(PGM_P format, va_list args) { - this->format_vsnprintf_P_(format, args); - this->finalize_(); - } -#endif - void write_body(const char *text, uint16_t text_length) { - this->write_(text, text_length); - this->finalize_(); - } - - private: - bool full_() const { return this->pos >= this->size; } - uint16_t remaining_() const { return this->size - this->pos; } - char *current_() { return this->data + this->pos; } - void write_(const char *value, uint16_t length) { - const uint16_t available = this->remaining_(); - const uint16_t copy_len = (length < available) ? length : available; - if (copy_len > 0) { - memcpy(this->current_(), value, copy_len); - this->pos += copy_len; - } - } - void finalize_() { - // Write color reset sequence - static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; - this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); - // Null terminate - this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; - } - void strip_trailing_newlines_() { - while (this->pos > 0 && this->data[this->pos - 1] == '\n') - this->pos--; - } - void process_vsnprintf_result_(int ret) { - if (ret < 0) - return; - const uint16_t rem = this->remaining_(); - this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret); - this->strip_trailing_newlines_(); - } - void format_vsnprintf_(const char *format, va_list args) { - if (this->full_()) - return; - this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args)); - } -#ifdef USE_STORE_LOG_STR_IN_FLASH - void format_vsnprintf_P_(PGM_P format, va_list args) { - if (this->full_()) - return; - this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); - } -#endif - // Write ANSI color escape sequence to buffer, updates pointer in place - // Caller is responsible for ensuring buffer has sufficient space - void write_ansi_color_(char *&p, uint8_t level) { - if (level == 0) - return; - // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes) - *p++ = '\033'; - *p++ = '['; - *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold - *p++ = ';'; - *p++ = '3'; - *p++ = LOG_LEVEL_COLOR_DIGIT[level]; - *p++ = 'm'; - } - // Copy string without null terminator, updates pointer in place - // Caller is responsible for ensuring buffer has sufficient space - void copy_string_(char *&p, const char *str) { - const size_t len = strlen(str); - // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by - // piece - memcpy(p, str, len); - p += len; - } -}; - #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -411,11 +222,14 @@ class Logger : public Component { bool &flag_; }; -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // Handles non-main thread logging only (~0.1% of calls) // thread_name is resolved by the caller from the task handle, avoiding redundant lookups void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, const char *thread_name); +#endif +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) + void cdc_loop_(); #endif void process_messages_(); void write_msg_(const char *msg, uint16_t len); @@ -534,13 +348,7 @@ class Logger : public Component { std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER -#ifdef USE_HOST - logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed -#elif defined(USE_ESP32) logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed -#elif defined(USE_LIBRETINY) - logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed -#endif #endif // Group smaller types together at the end @@ -552,7 +360,7 @@ class Logger : public Component { #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) bool main_task_recursion_guard_{false}; #ifdef USE_LIBRETINY bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny @@ -595,8 +403,10 @@ class Logger : public Component { } #elif defined(USE_ZEPHYR) - const char *HOT get_thread_name_(std::span<char> buff) { - k_tid_t current_task = k_current_get(); + const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) { + if (current_task == nullptr) { + current_task = k_current_get(); + } if (current_task == main_task_) { return nullptr; // Main task } @@ -635,7 +445,7 @@ class Logger : public Component { // Create RAII guard for non-main task recursion inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); } -#elif defined(USE_LIBRETINY) +#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // LibreTiny doesn't have FreeRTOS TLS, so use a simple approach: // - Main task uses dedicated boolean (same as ESP32) // - Non-main tasks share a single recursion guard @@ -643,6 +453,8 @@ class Logger : public Component { // - Recursion from logging within logging is the main concern // - Cross-task "recursion" is prevented by the buffer mutex anyway // - Missing a recursive call from another task is acceptable (falls back to direct output) + // + // Zephyr use __thread as TLS // Check if non-main task is already in recursion inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; } @@ -651,7 +463,8 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// Zephyr needs loop working to check when CDC port is open +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 1fc0acd573..f565c5760c 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -14,7 +14,7 @@ namespace esphome::logger { static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC -void Logger::loop() { +void Logger::cdc_loop_() { if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; } diff --git a/esphome/components/logger/task_log_buffer_esp32.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp index 56c0a4ae2d..e747ddc4d8 100644 --- a/esphome/components/logger/task_log_buffer_esp32.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() { } } -bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) { - if (message == nullptr || text == nullptr || received_token == nullptr) { +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { + if (this->current_token_) { return false; } @@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char ** } LogMessage *msg = static_cast<LogMessage *>(received_item); - *message = msg; - *text = msg->text_data(); - *received_token = received_item; + message = msg; + text_length = msg->text_length; + this->current_token_ = received_item; return true; } -void TaskLogBuffer::release_message_main_loop(void *token) { - if (token == nullptr) { +void TaskLogBuffer::release_message_main_loop() { + if (this->current_token_ == nullptr) { return; } - vRingbufferReturnItem(ring_buffer_, token); + vRingbufferReturnItem(ring_buffer_, this->current_token_); + this->current_token_ = nullptr; // Update counter to mark all messages as processed last_processed_counter_ = message_counter_.load(std::memory_order_relaxed); } diff --git a/esphome/components/logger/task_log_buffer_esp32.h b/esphome/components/logger/task_log_buffer_esp32.h index 6c1bafaeba..88d72eacfc 100644 --- a/esphome/components/logger/task_log_buffer_esp32.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -52,10 +52,10 @@ class TaskLogBuffer { ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the ring buffer, only call from main loop - bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token); + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); // NOT thread-safe - release a message buffer and update the counter, only call from main loop - void release_message_main_loop(void *token); + void release_message_main_loop(); // Thread-safe - send a message to the ring buffer from any thread bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, @@ -78,6 +78,7 @@ class TaskLogBuffer { // Atomic counter for message tracking (only differences matter) std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed mutable uint16_t last_processed_counter_{0}; // Tracks last processed message + void *current_token_{nullptr}; }; } // namespace esphome::logger diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp index 676686304a..c2ab009db4 100644 --- a/esphome/components/logger/task_log_buffer_host.cpp +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -10,16 +10,16 @@ namespace esphome::logger { -TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) { +TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) { // Allocate message slots this->slots_ = std::make_unique<LogMessage[]>(slot_count); } -TaskLogBufferHost::~TaskLogBufferHost() { +TaskLogBuffer::~TaskLogBuffer() { // unique_ptr handles cleanup automatically } -int TaskLogBufferHost::acquire_write_slot_() { +int TaskLogBuffer::acquire_write_slot_() { // Try to reserve a slot using compare-and-swap size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed); @@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() { } } -void TaskLogBufferHost::commit_write_slot_(int slot_index) { +void TaskLogBuffer::commit_write_slot_(int slot_index) { // Mark the slot as ready for reading this->slots_[slot_index].ready.store(true, std::memory_order_release); @@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) { } } -bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, - const char *format, va_list args) { +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { // Acquire a slot int slot_index = this->acquire_write_slot_(); if (slot_index < 0) { @@ -115,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, return true; } -bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) { - if (message == nullptr) { - return false; - } - +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { size_t current_read = this->read_index_.load(std::memory_order_relaxed); size_t current_write = this->write_index_.load(std::memory_order_acquire); @@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) { return false; } - *message = &msg; + message = &msg; + text_length = msg.text_length; return true; } -void TaskLogBufferHost::release_message_main_loop() { +void TaskLogBuffer::release_message_main_loop() { size_t current_read = this->read_index_.load(std::memory_order_relaxed); // Clear the ready flag diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h index f8e4ee7bee..1d4d2b0ec1 100644 --- a/esphome/components/logger/task_log_buffer_host.h +++ b/esphome/components/logger/task_log_buffer_host.h @@ -21,12 +21,12 @@ namespace esphome::logger { * * Threading Model: Multi-Producer Single-Consumer (MPSC) * - Multiple threads can safely call send_message_thread_safe() concurrently - * - Only the main loop thread calls get_message_main_loop() and release_message_main_loop() + * - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop() * * Producers (multiple threads) Consumer (main loop only) * │ │ * ▼ ▼ - * acquire_write_slot_() get_message_main_loop() + * acquire_write_slot_() bool borrow_message_main_loop() * CAS on reserve_index_ read write_index_ * │ check ready flag * ▼ │ @@ -48,7 +48,7 @@ namespace esphome::logger { * - Atomic CAS for slot reservation allows multiple producers without locks * - Single consumer (main loop) processes messages in order */ -class TaskLogBufferHost { +class TaskLogBuffer { public: // Default number of message slots - host has plenty of memory static constexpr size_t DEFAULT_SLOT_COUNT = 64; @@ -71,15 +71,16 @@ class TaskLogBufferHost { thread_name[0] = '\0'; text[0] = '\0'; } + inline char *text_data() { return this->text; } }; /// Constructor that takes the number of message slots - explicit TaskLogBufferHost(size_t slot_count); - ~TaskLogBufferHost(); + explicit TaskLogBuffer(size_t slot_count); + ~TaskLogBuffer(); // NOT thread-safe - get next message from buffer, only call from main loop // Returns true if a message was retrieved, false if buffer is empty - bool get_message_main_loop(LogMessage **message); + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); // NOT thread-safe - release the message after processing, only call from main loop void release_message_main_loop(); diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index 5a22857dcb..5969f6fb40 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -8,7 +8,7 @@ namespace esphome::logger { -TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) { +TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { this->size_ = total_buffer_size; // Allocate memory for the circular buffer using ESPHome's RAM allocator RAMAllocator<uint8_t> allocator; @@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) { this->mutex_ = xSemaphoreCreateMutex(); } -TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() { +TaskLogBuffer::~TaskLogBuffer() { if (this->mutex_ != nullptr) { vSemaphoreDelete(this->mutex_); this->mutex_ = nullptr; @@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() { } } -size_t TaskLogBufferLibreTiny::available_contiguous_space() const { +size_t TaskLogBuffer::available_contiguous_space() const { if (this->head_ >= this->tail_) { // head is ahead of or equal to tail // Available space is from head to end, plus from start to tail @@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const { } } -bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) { - if (message == nullptr || text == nullptr) { - return false; - } - +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { // Check if buffer was initialized successfully if (this->mutex_ == nullptr || this->storage_ == nullptr) { return false; @@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons this->tail_ = 0; msg = reinterpret_cast<LogMessage *>(this->storage_); } - *message = msg; - *text = msg->text_data(); + message = msg; + text_length = msg->text_length; this->current_message_size_ = message_total_size(msg->text_length); // Keep mutex held until release_message_main_loop() return true; } -void TaskLogBufferLibreTiny::release_message_main_loop() { +void TaskLogBuffer::release_message_main_loop() { // Advance tail past the current message this->tail_ += this->current_message_size_; @@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() { xSemaphoreGive(this->mutex_); } -bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, - const char *thread_name, const char *format, va_list args) { +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) va_list args_copy; va_copy(args_copy, args); diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index bf315e828a..c065065fe7 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -40,7 +40,7 @@ namespace esphome::logger { * - Volatile counter enables fast has_messages() without lock overhead * - If message doesn't fit at end, padding is added and message wraps to start */ -class TaskLogBufferLibreTiny { +class TaskLogBuffer { public: // Structure for a log message header (text data follows immediately after) struct LogMessage { @@ -60,11 +60,11 @@ class TaskLogBufferLibreTiny { static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF; // Constructor that takes a total buffer size - explicit TaskLogBufferLibreTiny(size_t total_buffer_size); - ~TaskLogBufferLibreTiny(); + explicit TaskLogBuffer(size_t total_buffer_size); + ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the buffer, only call from main loop - bool borrow_message_main_loop(LogMessage **message, const char **text); + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); // NOT thread-safe - release a message buffer, only call from main loop void release_message_main_loop(); diff --git a/esphome/components/logger/task_log_buffer_zephyr.cpp b/esphome/components/logger/task_log_buffer_zephyr.cpp new file mode 100644 index 0000000000..44d12d08a3 --- /dev/null +++ b/esphome/components/logger/task_log_buffer_zephyr.cpp @@ -0,0 +1,116 @@ +#ifdef USE_ZEPHYR + +#include "task_log_buffer_zephyr.h" + +namespace esphome::logger { + +__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +static inline uint32_t total_size_in_32bit_words(uint16_t text_length) { + // Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment) + return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t); +} + +static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) { + return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length); +} + +TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { + // alignment to 4 bytes + total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t); + this->mpsc_config_.buf = new uint32_t[total_buffer_size]; + this->mpsc_config_.size = total_buffer_size; + this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE; + this->mpsc_config_.get_wlen = get_wlen, + + mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_); +} + +TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; } + +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { + // First, calculate the exact length needed using a null buffer (no actual writing) + va_list args_copy; + va_copy(args_copy, args); + int ret = vsnprintf(nullptr, 0, format, args_copy); + va_end(args_copy); + + if (ret <= 0) { + return false; // Formatting error or empty message + } + + // Calculate actual text length (capped to maximum size) + static constexpr size_t MAX_TEXT_SIZE = 255; + size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret; + size_t total_size = total_size_in_32bit_words(text_length); + auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT)); + if (msg == nullptr) { + return false; + } + msg->level = level; + msg->tag = tag; + msg->line = line; + strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); + msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination + + // Format the message text directly into the acquired memory + // We add 1 to text_length to ensure space for null terminator during formatting + char *text_area = msg->text_data(); + ret = vsnprintf(text_area, text_length + 1, format, args); + + // Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output) + if (ret < 0) { + // this should not happen, vsnprintf was called already once + // fill with '\n' to not call mpsc_pbuf_free from producer + // it will be trimmed anyway + for (size_t i = 0; i < text_length; ++i) { + text_area[i] = '\n'; + } + text_area[text_length] = 0; + // do not return false to free the buffer from main thread + } + + msg->text_length = text_length; + + mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg)); + return true; +} + +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { + if (this->current_token_) { + return false; + } + + this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_); + + if (this->current_token_ == nullptr) { + return false; + } + + // we claimed buffer already, const_cast is safe here + message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_)); + + text_length = message->text_length; + // Remove trailing newlines + while (text_length > 0 && message->text_data()[text_length - 1] == '\n') { + text_length--; + } + + return true; +} + +void TaskLogBuffer::release_message_main_loop() { + if (this->current_token_ == nullptr) { + return; + } + mpsc_pbuf_free(&this->log_buffer_, this->current_token_); + this->current_token_ = nullptr; +} +#endif // USE_ESPHOME_TASK_LOG_BUFFER + +} // namespace esphome::logger + +#endif // USE_ZEPHYR diff --git a/esphome/components/logger/task_log_buffer_zephyr.h b/esphome/components/logger/task_log_buffer_zephyr.h new file mode 100644 index 0000000000..cc2ed1f687 --- /dev/null +++ b/esphome/components/logger/task_log_buffer_zephyr.h @@ -0,0 +1,66 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include <zephyr/sys/mpsc_pbuf.h> + +namespace esphome::logger { + +// "0x" + 2 hex digits per byte + '\0' +static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; + +extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +class TaskLogBuffer { + public: + // Structure for a log message header (text data follows immediately after) + struct LogMessage { + MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after + uint16_t line; // Source code line number + uint8_t level; // Log level (0-7) +#if defined(CONFIG_THREAD_NAME) + char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads) +#else + char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads) +#endif + const char *tag; // We store the pointer, assuming tags are static + uint16_t text_length; // Length of the message text (up to ~64KB) + + // Methods for accessing message contents + inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); } + }; + // Constructor that takes a total buffer size + explicit TaskLogBuffer(size_t total_buffer_size); + ~TaskLogBuffer(); + + // Check if there are messages ready to be processed using an atomic counter for performance + inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); } + + // Get the total buffer size in bytes + inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); } + + // NOT thread-safe - borrow a message from the ring buffer, only call from main loop + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); + + // NOT thread-safe - release a message buffer and update the counter, only call from main loop + void release_message_main_loop(); + + // Thread-safe - send a message to the ring buffer from any thread + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args); + + protected: + mpsc_pbuf_buffer_config mpsc_config_{}; + mpsc_pbuf_buffer log_buffer_{}; + const mpsc_pbuf_generic *current_token_{}; +}; + +#endif // USE_ESPHOME_TASK_LOG_BUFFER + +} // namespace esphome::logger + +#endif // USE_ZEPHYR diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ee865a7e65..0c888933bf 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -320,6 +320,7 @@ #endif #ifdef USE_NRF52 +#define USE_ESPHOME_TASK_LOG_BUFFER #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml index 70b485daac..821a136250 100644 --- a/tests/components/logger/test.nrf52-adafruit.yaml +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -5,3 +5,4 @@ esphome: logger: level: DEBUG + task_log_buffer_size: 0 From 923445eb5dbaaf87c164c709a2cc2b34473a8a56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 10:06:44 -0600 Subject: [PATCH 233/251] [light] Eliminate redundant clamp in LightCall::validate_() (#13923) --- esphome/components/light/light_call.cpp | 25 ++++++++++--------- esphome/components/light/light_color_values.h | 19 ++++++++------ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 3b4e136ba5..0291b2c3c6 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -270,22 +270,23 @@ LightColorValues LightCall::validate_() { if (this->has_state()) v.set_state(this->state_); -#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \ + // clamp_and_log_if_invalid already clamps in-place, so assign directly + // to avoid redundant clamp code from the setter being inlined. +#define VALIDATE_AND_APPLY(field, name_str, ...) \ if (this->has_##field()) { \ clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ - v.setter(this->field##_); \ + v.field##_ = this->field##_; \ } - VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness") - VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness") - VALIDATE_AND_APPLY(red, set_red, "Red") - VALIDATE_AND_APPLY(green, set_green, "Green") - VALIDATE_AND_APPLY(blue, set_blue, "Blue") - VALIDATE_AND_APPLY(white, set_white, "White") - VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white") - VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white") - VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(), - traits.get_max_mireds()) + VALIDATE_AND_APPLY(brightness, "Brightness") + VALIDATE_AND_APPLY(color_brightness, "Color brightness") + VALIDATE_AND_APPLY(red, "Red") + VALIDATE_AND_APPLY(green, "Green") + VALIDATE_AND_APPLY(blue, "Blue") + VALIDATE_AND_APPLY(white, "White") + VALIDATE_AND_APPLY(cold_white, "Cold white") + VALIDATE_AND_APPLY(warm_white, "Warm white") + VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) #undef VALIDATE_AND_APPLY diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 97756b9f26..dc23263312 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -95,15 +95,18 @@ class LightColorValues { */ void normalize_color() { if (this->color_mode_ & ColorCapability::RGB) { - float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); + float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_)); + // Assign directly to avoid redundant clamp in set_red/green/blue. + // Values are guaranteed in [0,1]: inputs are already clamped to [0,1], + // and dividing by max_value (the largest) keeps results in [0,1]. if (max_value == 0.0f) { - this->set_red(1.0f); - this->set_green(1.0f); - this->set_blue(1.0f); + this->red_ = 1.0f; + this->green_ = 1.0f; + this->blue_ = 1.0f; } else { - this->set_red(this->get_red() / max_value); - this->set_green(this->get_green() / max_value); - this->set_blue(this->get_blue() / max_value); + this->red_ /= max_value; + this->green_ /= max_value; + this->blue_ /= max_value; } } } @@ -276,6 +279,8 @@ class LightColorValues { /// Set the warm white property of these light color values. In range 0.0 to 1.0. void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } + friend class LightCall; + protected: float state_; ///< ON / OFF, float for transition float brightness_; From b1f0db9da812cfcf250a0f3c558232e23c2cbc60 Mon Sep 17 00:00:00 2001 From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:10:32 +0100 Subject: [PATCH 234/251] [bl0942] Update reference values (#12867) --- esphome/components/bl0942/bl0942.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 37b884e6ca..10b29a72c6 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -59,10 +59,10 @@ namespace bl0942 { // // Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4 -static const float BL0942_PREF = 596; // taken from tasmota -static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218 -static const float BL0942_IREF = 251213.46469622; // 305978/1.218 -static const float BL0942_EREF = 3304.61127328; // Measured +static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF +static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider +static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt +static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF struct DataPacket { uint8_t frame_header; @@ -86,11 +86,11 @@ enum LineFrequency : uint8_t { class BL0942 : public PollingComponent, public uart::UARTDevice { public: - void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } - void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } - void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } - void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } - void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; } void set_address(uint8_t address) { this->address_ = address; } void set_reset(bool reset) { this->reset_ = reset; } From 930a1861683b0f66f4800cc820e6718c2c818939 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 11:03:27 -0600 Subject: [PATCH 235/251] [web_server_idf] Use constant-time comparison for Basic Auth (#13868) --- .../web_server_idf/web_server_idf.cpp | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 0dd1948dcc..2e07fb6e0a 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -352,7 +352,26 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out, reinterpret_cast<const uint8_t *>(user_info), user_info_len); - return strcmp(digest, auth_str + auth_prefix_len) == 0; + // Constant-time comparison to avoid timing side channels. + // No early return on length mismatch — the length difference is folded + // into the accumulator so any mismatch is rejected. + const char *provided = auth_str + auth_prefix_len; + size_t digest_len = out; // length from esp_crypto_base64_encode + // Derive provided_len from the already-sized std::string rather than + // rescanning with strlen (avoids attacker-controlled scan length). + size_t provided_len = auth.value().size() - auth_prefix_len; + // Use full-width XOR so any bit difference in the lengths is preserved + // (uint8_t truncation would miss differences in higher bytes, e.g. + // digest_len vs digest_len + 256). + volatile size_t result = digest_len ^ provided_len; + // Iterate over the expected digest length only — the full-width length + // XOR above already rejects any length mismatch, and bounding the loop + // prevents a long Authorization header from forcing extra work. + for (size_t i = 0; i < digest_len; i++) { + char provided_ch = (i < provided_len) ? provided[i] : 0; + result |= static_cast<uint8_t>(digest[i] ^ provided_ch); + } + return result == 0; } void AsyncWebServerRequest::requestAuthentication(const char *realm) const { From 069c90ec4aeb46fc26a23ad3a7376c20f9a8e44c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 11:34:43 -0600 Subject: [PATCH 236/251] [api] Split process_batch_ to reduce stack on single-message hot path (#13907) --- esphome/components/api/api_connection.cpp | 81 +++++++++++++---------- esphome/components/api/api_connection.h | 6 +- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 34d9744adc..4bc3c9b307 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1921,10 +1921,6 @@ bool APIConnection::schedule_batch_() { } void APIConnection::process_batch_() { - // Ensure MessageInfo remains trivially destructible for our placement new approach - static_assert(std::is_trivially_destructible<MessageInfo>::value, - "MessageInfo must remain trivially destructible with this placement-new approach"); - if (this->deferred_batch_.empty()) { this->flags_.batch_scheduled = false; return; @@ -1949,6 +1945,10 @@ void APIConnection::process_batch_() { for (size_t i = 0; i < num_items; i++) { total_estimated_size += this->deferred_batch_[i].estimated_size; } + // Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch + if (total_estimated_size > MAX_BATCH_PACKET_SIZE) { + total_estimated_size = MAX_BATCH_PACKET_SIZE; + } this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); @@ -1972,7 +1972,20 @@ void APIConnection::process_batch_() { return; } - size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); + // Multi-message path — heavy stack frame isolated in separate noinline function + this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size); +} + +// Separated from process_batch_() so the single-message fast path gets a minimal +// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. +void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, + uint8_t footer_size) { + // Ensure MessageInfo remains trivially destructible for our placement new approach + static_assert(std::is_trivially_destructible<MessageInfo>::value, + "MessageInfo must remain trivially destructible with this placement-new approach"); + + const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); + const uint8_t frame_overhead = header_padding + footer_size; // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; @@ -1999,7 +2012,7 @@ void APIConnection::process_batch_() { // Message was encoded successfully // payload_size is header_padding + actual payload size + footer_size - uint16_t proto_payload_size = payload_size - header_padding - footer_size; + uint16_t proto_payload_size = payload_size - frame_overhead; // Use placement new to construct MessageInfo in pre-allocated stack array // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, @@ -2015,42 +2028,38 @@ void APIConnection::process_batch_() { current_offset = shared_buf.size() + footer_size; } - if (items_processed == 0) { - this->deferred_batch_.clear(); - return; - } + if (items_processed > 0) { + // Add footer space for the last message (for Noise protocol MAC) + if (footer_size > 0) { + shared_buf.resize(shared_buf.size() + footer_size); + } - // Add footer space for the last message (for Noise protocol MAC) - if (footer_size > 0) { - shared_buf.resize(shared_buf.size() + footer_size); - } - - // Send all collected messages - APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, - std::span<const MessageInfo>(message_info, items_processed)); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); - } + // Send all collected messages + APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, + std::span<const MessageInfo>(message_info, items_processed)); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); + } #ifdef HAS_PROTO_MESSAGE_DUMP - // Log messages after send attempt for VV debugging - // It's safe to use the buffer for logging at this point regardless of send result - for (size_t i = 0; i < items_processed; i++) { - const auto &item = this->deferred_batch_[i]; - this->log_batch_item_(item); - } + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + for (size_t i = 0; i < items_processed; i++) { + const auto &item = this->deferred_batch_[i]; + this->log_batch_item_(item); + } #endif - // Handle remaining items more efficiently - if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning - this->deferred_batch_.remove_front(items_processed); - // Reschedule for remaining items - this->schedule_batch_(); - } else { - // All items processed - this->clear_batch_(); + // Partial batch — remove processed items and reschedule + if (items_processed < this->deferred_batch_.size()) { + this->deferred_batch_.remove_front(items_processed); + this->schedule_batch_(); + return; + } } + + // All items processed (or none could be processed) + this->clear_batch_(); } // Dispatch message encoding based on message_type diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a16d681760..d3d09a01c8 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -548,8 +548,8 @@ class APIConnection final : public APIServerConnectionBase { batch_start_time = 0; } - // Remove processed items from the front - void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } + // Remove processed items from the front — noinline to keep memmove out of warm callers + void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } @@ -621,6 +621,8 @@ class APIConnection final : public APIServerConnectionBase { bool schedule_batch_(); void process_batch_(); + void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, + uint8_t footer_size) __attribute__((noinline)); void clear_batch_() { this->deferred_batch_.clear(); this->flags_.batch_scheduled = false; From 1411868a0ba980b6fdb26aadf519dcfe0a3377d1 Mon Sep 17 00:00:00 2001 From: Nate Clark <nate@nateclark.com> Date: Wed, 11 Feb 2026 12:40:27 -0500 Subject: [PATCH 237/251] [mqtt.cover] Add option to publish states as JSON payload (#12639) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> --- esphome/components/cover/__init__.py | 23 ++++++++ esphome/components/mqtt/mqtt_cover.cpp | 77 ++++++++++++++++++++------ esphome/components/mqtt/mqtt_cover.h | 6 ++ esphome/const.py | 1 + esphome/core/defines.h | 1 + tests/components/mqtt/common.yaml | 48 ++++++++++++++++ 6 files changed, 140 insertions(+), 16 deletions(-) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 41774f3d71..648fe7decf 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_MQTT_ID, + CONF_MQTT_JSON_STATE_PAYLOAD, CONF_ON_IDLE, CONF_ON_OPEN, CONF_POSITION, @@ -119,6 +120,9 @@ _COVER_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), + cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All( + cv.requires_component("mqtt"), cv.boolean + ), cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic @@ -148,6 +152,22 @@ _COVER_SCHEMA = ( _COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) +def _validate_mqtt_state_topics(config): + if config.get(CONF_MQTT_JSON_STATE_PAYLOAD): + if CONF_POSITION_STATE_TOPIC in config: + raise cv.Invalid( + f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'" + ) + if CONF_TILT_STATE_TOPIC in config: + raise cv.Invalid( + f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'" + ) + return config + + +_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics) + + def cover_schema( class_: MockObjClass, *, @@ -195,6 +215,9 @@ async def setup_cover_core_(var, config): position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC) ) is not None: cg.add(mqtt_.set_custom_position_command_topic(position_command_topic)) + if config.get(CONF_MQTT_JSON_STATE_PAYLOAD): + cg.add_define("USE_MQTT_COVER_JSON") + cg.add(mqtt_.set_use_json_format(True)) if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None: cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic)) if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None: diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index c21af413ed..9752004094 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,17 +67,26 @@ void MQTTCoverComponent::dump_config() { 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); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; +#ifdef USE_MQTT_COVER_JSON + if (this->use_json_format_) { + ESP_LOGCONFIG(TAG, " JSON State Payload: YES"); + } else { +#endif + if (traits.get_supports_position()) { + ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic_to(topic_buf).c_str()); + } + if (traits.get_supports_tilt()) { + ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic_to(topic_buf).c_str()); + } +#ifdef USE_MQTT_COVER_JSON + } +#endif if (traits.get_supports_position()) { - ESP_LOGCONFIG(TAG, - " Position State Topic: '%s'\n" - " Position Command Topic: '%s'", - this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str()); + ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str()); } if (traits.get_supports_tilt()) { - ESP_LOGCONFIG(TAG, - " Tilt State Topic: '%s'\n" - " Tilt Command Topic: '%s'", - this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str()); + ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str()); } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { @@ -92,13 +101,33 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; } - if (traits.get_supports_position()) { - root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); - root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); - } - if (traits.get_supports_tilt()) { - root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic(); - root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic(); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; +#ifdef USE_MQTT_COVER_JSON + if (this->use_json_format_) { + // JSON mode: all state published to state_topic as JSON, use templates to extract + root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}"); + if (traits.get_supports_position()) { + root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf); + root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}"); + root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf); + } + if (traits.get_supports_tilt()) { + root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf); + root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}"); + root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf); + } + } else +#endif + { + // Standard mode: separate topics for position and tilt + if (traits.get_supports_position()) { + root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf); + root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf); + } + if (traits.get_supports_tilt()) { + root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf); + root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf); + } } if (traits.get_supports_tilt() && !traits.get_supports_position()) { config.command_topic = false; @@ -111,8 +140,24 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_; bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } bool MQTTCoverComponent::publish_state() { auto traits = this->cover_->get_traits(); - bool success = true; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; +#ifdef USE_MQTT_COVER_JSON + if (this->use_json_format_) { + return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, + traits.get_supports_position()); + if (traits.get_supports_position()) { + root[ESPHOME_F("position")] = static_cast<int>(roundf(this->cover_->position * 100)); + } + if (traits.get_supports_tilt()) { + root[ESPHOME_F("tilt")] = static_cast<int>(roundf(this->cover_->tilt * 100)); + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + }); + } +#endif + bool success = true; if (traits.get_supports_position()) { char pos[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0); diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index 13582d14d1..f801af5d12 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -27,12 +27,18 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { bool publish_state(); void dump_config() override; +#ifdef USE_MQTT_COVER_JSON + void set_use_json_format(bool use_json_format) { this->use_json_format_ = use_json_format; } +#endif protected: const char *component_type() const override; const EntityBase *get_entity() const override; cover::Cover *cover_; +#ifdef USE_MQTT_COVER_JSON + bool use_json_format_{false}; +#endif }; } // namespace esphome::mqtt diff --git a/esphome/const.py b/esphome/const.py index 4bf47b8f83..00d8013cc6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -639,6 +639,7 @@ CONF_MOVEMENT_COUNTER = "movement_counter" CONF_MOVING_DISTANCE = "moving_distance" CONF_MQTT = "mqtt" CONF_MQTT_ID = "mqtt_id" +CONF_MQTT_JSON_STATE_PAYLOAD = "mqtt_json_state_payload" CONF_MULTIPLE = "multiple" CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLY = "multiply" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0c888933bf..bfe9d620df 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -145,6 +145,7 @@ #define USE_MD5 #define USE_SHA256 #define USE_MQTT +#define USE_MQTT_COVER_JSON #define USE_NETWORK #define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 4cf2692593..8c58e9b080 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -219,6 +219,7 @@ cover: name: Template Cover state_topic: some/topic/cover qos: 2 + mqtt_json_state_payload: true lambda: |- if (id(some_binary_sensor).state) { return COVER_OPEN; @@ -231,6 +232,53 @@ cover: stop_action: - logger.log: stop_action optimistic: true + - platform: template + name: Template Cover with Position and Tilt + state_topic: some/topic/cover_pt + position_state_topic: some/topic/cover_pt/position + position_command_topic: some/topic/cover_pt/position/set + tilt_state_topic: some/topic/cover_pt/tilt + tilt_command_topic: some/topic/cover_pt/tilt/set + qos: 2 + has_position: true + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + position_action: + - logger.log: position_action + tilt_action: + - logger.log: tilt_action + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + - platform: template + name: Template Cover with Position and Tilt JSON + state_topic: some/topic/cover_pt_json + qos: 2 + mqtt_json_state_payload: true + has_position: true + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + position_action: + - logger.log: position_action + tilt_action: + - logger.log: tilt_action + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true datetime: - platform: template From 0ec02d48869c782ff38f57b7b2f47f7a77c04aef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 11:41:53 -0600 Subject: [PATCH 238/251] [preferences] Replace per-element erase with clear() in sync() (#13934) --- esphome/components/esp32/preferences.cpp | 8 +++----- esphome/components/libretiny/preferences.cpp | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7d5af023b4..8d6fdc86f6 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -124,14 +124,11 @@ class ESP32Preferences : public ESPPreferences { return true; ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); - // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; esp_err_t last_err = ESP_OK; uint32_t last_key = 0; - // go through vector from back to front (makes erase easier/more efficient) - for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { - const auto &save = s_pending_save[i]; + for (const auto &save : s_pending_save) { char key_str[KEY_BUFFER_SIZE]; snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); @@ -150,8 +147,9 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } - s_pending_save.erase(s_pending_save.begin() + i); } + s_pending_save.clear(); + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 5a60b535da..8549631e46 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -114,14 +114,11 @@ class LibreTinyPreferences : public ESPPreferences { return true; ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); - // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; fdb_err_t last_err = FDB_NO_ERR; uint32_t last_key = 0; - // go through vector from back to front (makes erase easier/more efficient) - for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { - const auto &save = s_pending_save[i]; + for (const auto &save : s_pending_save) { char key_str[KEY_BUFFER_SIZE]; snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); @@ -141,8 +138,9 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } - s_pending_save.erase(s_pending_save.begin() + i); } + s_pending_save.clear(); + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { From 8d62a6a88a4dc016c836794c6d3e66376feef9cd Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:54:31 +0100 Subject: [PATCH 239/251] [openthread] Fix warning on old C89 implicit field zero init (#13935) --- esphome/components/openthread/openthread_esp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index a9aff3cce4..79cd809809 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -104,7 +104,7 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION - otLinkModeConfig link_mode_config = {0}; + otLinkModeConfig link_mode_config{}; #if CONFIG_OPENTHREAD_FTD link_mode_config.mRxOnWhenIdle = true; link_mode_config.mDeviceType = true; From c9c125aa8d5b30295e7fa93a08070077440b929d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 11:54:58 -0600 Subject: [PATCH 240/251] [socket] Devirtualize Socket::ready() and implement working ready() for LWIP raw TCP (#13913) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../components/socket/bsd_sockets_impl.cpp | 29 ++----------------- .../components/socket/lwip_raw_tcp_impl.cpp | 4 +++ .../components/socket/lwip_sockets_impl.cpp | 29 ++----------------- esphome/components/socket/socket.cpp | 4 +++ esphome/components/socket/socket.h | 24 ++++++++++++--- esphome/core/application.cpp | 9 ------ esphome/core/application.h | 16 +++++++++- 7 files changed, 47 insertions(+), 68 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index b670b9c068..c96713f376 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -16,19 +16,13 @@ namespace esphome::socket { class BSDSocketImpl final : public Socket { public: - BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { -#ifdef USE_SOCKET_SELECT_SUPPORT + BSDSocketImpl(int fd, bool monitor_loop = false) { + this->fd_ = fd; // Register new socket with the application for select() if monitoring requested if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds this->loop_monitored_ = App.register_socket_fd(this->fd_); - } else { - this->loop_monitored_ = false; } -#else - // Without select support, ignore monitor_loop parameter - (void) monitor_loop; -#endif } ~BSDSocketImpl() override { if (!this->closed_) { @@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket { int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int close() override { if (!this->closed_) { -#ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } -#endif int ret = ::close(this->fd_); this->closed_ = true; return ret; @@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket { ::fcntl(this->fd_, F_SETFL, fl); return 0; } - - int get_fd() const override { return this->fd_; } - -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const override { - if (!this->loop_monitored_) - return true; - return App.is_socket_ready(this->fd_); - } -#endif - - protected: - int fd_; - bool closed_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; -#endif }; // Helper to create a socket with optional monitoring diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index a9c2eda4e8..aa37386d70 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -452,6 +452,8 @@ class LWIPRawImpl : public Socket { errno = ENOSYS; return -1; } + bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } + int setblocking(bool blocking) final { if (pcb_ == nullptr) { errno = ECONNRESET; @@ -576,6 +578,8 @@ class LWIPRawListenImpl final : public LWIPRawImpl { tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler } + bool ready() const override { return this->accepted_socket_count_ > 0; } + std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override { if (pcb_ == nullptr) { errno = EBADF; diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index a885f243f3..79d68e085a 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -11,19 +11,13 @@ namespace esphome::socket { class LwIPSocketImpl final : public Socket { public: - LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { -#ifdef USE_SOCKET_SELECT_SUPPORT + LwIPSocketImpl(int fd, bool monitor_loop = false) { + this->fd_ = fd; // Register new socket with the application for select() if monitoring requested if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds this->loop_monitored_ = App.register_socket_fd(this->fd_); - } else { - this->loop_monitored_ = false; } -#else - // Without select support, ignore monitor_loop parameter - (void) monitor_loop; -#endif } ~LwIPSocketImpl() override { if (!this->closed_) { @@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket { int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int close() override { if (!this->closed_) { -#ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } -#endif int ret = lwip_close(this->fd_); this->closed_ = true; return ret; @@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket { lwip_fcntl(this->fd_, F_SETFL, fl); return 0; } - - int get_fd() const override { return this->fd_; } - -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const override { - if (!this->loop_monitored_) - return true; - return App.is_socket_ready(this->fd_); - } -#endif - - protected: - int fd_; - bool closed_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; -#endif }; // Helper to create a socket with optional monitoring diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index fd8725b363..2fcc162ead 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -10,6 +10,10 @@ namespace esphome::socket { Socket::~Socket() {} +#ifdef USE_SOCKET_SELECT_SUPPORT +bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); } +#endif + // Platform-specific inet_ntop wrappers #if defined(USE_SOCKET_IMPL_LWIP_TCP) // LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index e8b0948acd..c0098d689a 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -63,13 +63,29 @@ class Socket { virtual int setblocking(bool blocking) = 0; virtual int loop() { return 0; }; - /// Get the underlying file descriptor (returns -1 if not supported) - virtual int get_fd() const { return -1; } + /// Get the underlying file descriptor (returns -1 if not supported) + /// Non-virtual: only one socket implementation is active per build. +#ifdef USE_SOCKET_SELECT_SUPPORT + int get_fd() const { return this->fd_; } +#else + int get_fd() const { return -1; } +#endif /// Check if socket has data ready to read - /// For loop-monitored sockets, checks with the Application's select() results - /// For non-monitored sockets, always returns true (assumes data may be available) + /// For select()-based sockets: non-virtual, checks Application's select() results + /// For LWIP raw TCP sockets: virtual, checks internal buffer state +#ifdef USE_SOCKET_SELECT_SUPPORT + bool ready() const; +#else virtual bool ready() const { return true; } +#endif + + protected: +#ifdef USE_SOCKET_SELECT_SUPPORT + int fd_{-1}; + bool closed_{false}; + bool loop_monitored_{false}; +#endif }; /// Create a socket of the given domain, type and protocol. diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 7b5435185d..449acc64cf 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -609,15 +609,6 @@ void Application::unregister_socket_fd(int fd) { } } -bool Application::is_socket_ready(int fd) const { - // This function is thread-safe for reading the result of select() - // However, it should only be called after select() has been executed in the main loop - // The read_fds_ is only modified by select() in the main loop - if (fd < 0 || fd >= FD_SETSIZE) - return false; - - return FD_ISSET(fd, &this->read_fds_); -} #endif void Application::yield_with_select_(uint32_t delay_ms) { diff --git a/esphome/core/application.h b/esphome/core/application.h index 8478100a56..30611227a2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -101,6 +101,10 @@ #include "esphome/components/update/update_entity.h" #endif +namespace esphome::socket { +class Socket; +} // namespace esphome::socket + namespace esphome { // Teardown timeout constant (in milliseconds) @@ -491,7 +495,8 @@ class Application { void unregister_socket_fd(int fd); /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run - bool is_socket_ready(int fd) const; + /// The read_fds_ is only modified by select() in the main loop + bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); } #ifdef USE_WAKE_LOOP_THREADSAFE /// Wake the main event loop from a FreeRTOS task @@ -503,6 +508,15 @@ class Application { protected: friend Component; + friend class socket::Socket; + +#ifdef USE_SOCKET_SELECT_SUPPORT + /// Fast path for Socket::ready() via friendship - skips negative fd check. + /// Safe because: fd was validated in register_socket_fd() at registration time, + /// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded). + /// FD_ISSET may include its own upper bounds check depending on platform. + bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } +#endif void register_component_(Component *comp); From 483b7693e1c79d974de4f22b126bc0f918004569 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 13:57:08 -0600 Subject: [PATCH 241/251] [api] Fix debug asserts in production code, encode_bool bug, and reduce flash overhead (#13936) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../api/api_frame_helper_plaintext.cpp | 5 +- esphome/components/api/proto.h | 95 +++++++------------ esphome/core/defines.h | 1 + tests/integration/conftest.py | 1 + 4 files changed, 40 insertions(+), 62 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index ed3cc8934e..5069dbf68b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -295,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe buf_start[header_offset] = 0x00; // indicator // Encode varints directly into buffer - ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - ProtoVarInt(msg.message_type) - .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); + encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); // Add iovec for this message (header + payload) size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 41ea0043f9..8ac79633cf 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) { return count; } +/// Encode a varint directly into a pre-allocated buffer. +/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate). +inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) { + while (val > 0x7F) { + *buffer++ = static_cast<uint8_t>(val | 0x80); + val >>= 7; + } + *buffer = static_cast<uint8_t>(val); +} + /* * StringRef Ownership Model for API Protocol Messages * =================================================== @@ -93,17 +103,17 @@ class ProtoVarInt { ProtoVarInt() : value_(0) {} explicit ProtoVarInt(uint64_t value) : value_(value) {} + /// Parse a varint from buffer. consumed must be a valid pointer (not null). static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { - if (len == 0) { - if (consumed != nullptr) - *consumed = 0; +#ifdef ESPHOME_DEBUG_API + assert(consumed != nullptr); +#endif + if (len == 0) return {}; - } // Most common case: single-byte varint (values 0-127) if ((buffer[0] & 0x80) == 0) { - if (consumed != nullptr) - *consumed = 1; + *consumed = 1; return ProtoVarInt(buffer[0]); } @@ -122,14 +132,11 @@ class ProtoVarInt { result |= uint64_t(val & 0x7F) << uint64_t(bitpos); bitpos += 7; if ((val & 0x80) == 0) { - if (consumed != nullptr) - *consumed = i + 1; + *consumed = i + 1; return ProtoVarInt(result); } } - if (consumed != nullptr) - *consumed = 0; return {}; // Incomplete or invalid varint } @@ -153,50 +160,6 @@ class ProtoVarInt { // with ZigZag encoding return decode_zigzag64(this->value_); } - /** - * Encode the varint value to a pre-allocated buffer without bounds checking. - * - * @param buffer The pre-allocated buffer to write the encoded varint to - * @param len The size of the buffer in bytes - * - * @note The caller is responsible for ensuring the buffer is large enough - * to hold the encoded value. Use ProtoSize::varint() to calculate - * the exact size needed before calling this method. - * @note No bounds checking is performed for performance reasons. - */ - void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) { - uint64_t val = this->value_; - if (val <= 0x7F) { - buffer[0] = val; - return; - } - size_t i = 0; - while (val && i < len) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - buffer[i++] = temp | 0x80; - } else { - buffer[i++] = temp; - } - } - } - void encode(std::vector<uint8_t> &out) { - uint64_t val = this->value_; - if (val <= 0x7F) { - out.push_back(val); - return; - } - while (val) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - out.push_back(temp | 0x80); - } else { - out.push_back(temp); - } - } - } protected: uint64_t value_; @@ -256,8 +219,20 @@ class ProtoWriteBuffer { public: ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {} void write(uint8_t value) { this->buffer_->push_back(value); } - void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } - void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } + void encode_varint_raw(uint32_t value) { + while (value > 0x7F) { + this->buffer_->push_back(static_cast<uint8_t>(value | 0x80)); + value >>= 7; + } + this->buffer_->push_back(static_cast<uint8_t>(value)); + } + void encode_varint_raw_64(uint64_t value) { + while (value > 0x7F) { + this->buffer_->push_back(static_cast<uint8_t>(value | 0x80)); + value >>= 7; + } + this->buffer_->push_back(static_cast<uint8_t>(value)); + } /** * Encode a field key (tag/wire type combination). * @@ -307,13 +282,13 @@ class ProtoWriteBuffer { if (value == 0 && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 - this->encode_varint_raw(ProtoVarInt(value)); + this->encode_varint_raw_64(value); } void encode_bool(uint32_t field_id, bool value, bool force = false) { if (!value && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->write(0x01); + this->buffer_->push_back(value ? 0x01 : 0x00); } void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) @@ -938,13 +913,15 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa this->buffer_->resize(this->buffer_->size() + varint_length_bytes); // Write the length varint directly - ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes); + encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); // Now encode the message content - it will append to the buffer value.encode(*this); +#ifdef ESPHOME_DEBUG_API // Verify that the encoded size matches what we calculated assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); +#endif } // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bfe9d620df..7e6df31ea2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -14,6 +14,7 @@ #define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" #define ESPHOME_DEBUG_SCHEDULER +#define ESPHOME_DEBUG_API // Default threading model for static analysis (ESP32 is multi-threaded with atomics) #define ESPHOME_THREAD_MULTI_ATOMICS diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 50e8d4122b..36df1bc83e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -197,6 +197,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s " platformio_options:\n" " build_flags:\n" ' - "-DDEBUG" # Enable assert() statements\n' + ' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n' ' - "-g" # Add debug symbols', ) From 7287a43f2a30d0477b9d78064aca0d9331bdb844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:12:05 -0600 Subject: [PATCH 242/251] Bump docker/build-push-action from 6.18.0 to 6.19.1 in /.github/actions/build-image (#13937) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 9c7f051e05..494304eced 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 374cbf4452e01053fe0bd7a0cf666f11f712e2f7 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Wed, 11 Feb 2026 22:21:10 +0100 Subject: [PATCH 243/251] [nrf52,zigbee] count sleep time of zigbee thread (#13933) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/zigbee/zigbee_zephyr.cpp | 15 ++++++++++++--- esphome/components/zigbee/zigbee_zephyr.h | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index 65639de61b..c103363b4a 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -61,11 +61,19 @@ void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) { break; } + auto before = millis(); auto err = zigbee_default_signal_handler(bufid); if (err != RET_OK) { ESP_LOGE(TAG, "Zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err)); } + if (sig == ZB_COMMON_SIGNAL_CAN_SLEEP) { + this->sleep_remainder_ += millis() - before; + uint32_t seconds = this->sleep_remainder_ / 1000; + this->sleep_remainder_ -= seconds * 1000; + this->sleep_time_ += seconds; + } + switch (sig) { case ZB_BDB_SIGNAL_STEERING: ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status); @@ -213,6 +221,7 @@ void ZigbeeComponent::dump_config() { "Zigbee\n" " Wipe on boot: %s\n" " Device is joined to the network: %s\n" + " Sleep time: %us\n" " Current channel: %d\n" " Current page: %d\n" " Sleep threshold: %ums\n" @@ -221,9 +230,9 @@ void ZigbeeComponent::dump_config() { " Short addr: 0x%04X\n" " Long pan id: 0x%s\n" " Short pan id: 0x%04X", - get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(), - zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf, - zb_get_pan_id()); + get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(), + zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), + extended_pan_id_buf, zb_get_pan_id()); dump_reporting_(); } diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index bd4b092ad5..dcc2b40a16 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -92,6 +92,8 @@ class ZigbeeComponent : public Component { CallbackManager<void()> join_cb_; Trigger<> join_trigger_; bool force_report_{false}; + uint32_t sleep_time_{}; + uint32_t sleep_remainder_{}; }; class ZigbeeEntity { From e12ed08487a310860ba9ea39b7b75b99cb69ceca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 15:24:24 -0600 Subject: [PATCH 244/251] [wifi] Add CompactString to reduce WiFi scan heap fragmentation (#13472) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../esp32_improv/esp32_improv_component.cpp | 4 +- .../improv_serial/improv_serial_component.cpp | 20 ++- esphome/components/wifi/wifi_component.cpp | 144 +++++++++++++----- esphome/components/wifi/wifi_component.h | 76 ++++++++- .../wifi/wifi_component_esp8266.cpp | 26 ++-- .../wifi/wifi_component_esp_idf.cpp | 25 ++- .../wifi/wifi_component_libretiny.cpp | 8 +- .../components/wifi/wifi_component_pico_w.cpp | 9 +- .../wifi_info/wifi_info_text_sensor.cpp | 2 +- 9 files changed, 225 insertions(+), 89 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 1a19472c87..83bc842a3d 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -338,8 +338,8 @@ void ESP32ImprovComponent::process_incoming_data_() { return; } wifi::WiFiAP sta{}; - sta.set_ssid(command.ssid); - sta.set_password(command.password); + sta.set_ssid(command.ssid.c_str()); + sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index b4d9943955..edceb9a3b1 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command switch (command.command) { case improv::WIFI_SETTINGS: { wifi::WiFiAP sta{}; - sta.set_ssid(command.ssid); - sta.set_password(command.password); + sta.set_ssid(command.ssid.c_str()); + sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); @@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command for (auto &scan : results) { if (scan.get_is_hidden()) continue; - const std::string &ssid = scan.get_ssid(); - if (std::find(networks.begin(), networks.end(), ssid) != networks.end()) + const char *ssid_cstr = scan.get_ssid().c_str(); + // Check if we've already sent this SSID + bool duplicate = false; + for (const auto &seen : networks) { + if (strcmp(seen.c_str(), ssid_cstr) == 0) { + duplicate = true; + break; + } + } + if (duplicate) continue; + // Only allocate std::string after confirming it's not a duplicate + std::string ssid(ssid_cstr); // Send each ssid separately to avoid overflowing the buffer char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null *int8_to_str(rssi_buf, scan.get_rssi()) = '\0'; std::vector<uint8_t> data = improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false); this->send_response_(data); - networks.push_back(ssid); + networks.push_back(std::move(ssid)); } // Send empty response to signify the end of the list. std::vector<uint8_t> data = diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e350f990af..61d05d7635 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -20,6 +20,7 @@ #endif #include <algorithm> +#include <new> #include <utility> #include "lwip/dns.h" #include "lwip/err.h" @@ -47,6 +48,69 @@ namespace esphome::wifi { static const char *const TAG = "wifi"; +// CompactString implementation +CompactString::CompactString(const char *str, size_t len) { + if (len > MAX_LENGTH) { + len = MAX_LENGTH; // Clamp to max valid length + } + + this->length_ = len; + if (len <= INLINE_CAPACITY) { + // Store inline with null terminator + this->is_heap_ = 0; + if (len > 0) { + std::memcpy(this->storage_, str, len); + } + this->storage_[len] = '\0'; + } else { + // Heap allocate with null terminator + this->is_heap_ = 1; + char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory) + std::memcpy(heap_data, str, len); + heap_data[len] = '\0'; + this->set_heap_ptr_(heap_data); + } +} + +CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {} + +CompactString &CompactString::operator=(const CompactString &other) { + if (this != &other) { + this->~CompactString(); + new (this) CompactString(other); + } + return *this; +} + +CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) { + // Copy full storage (includes null terminator for inline, or pointer for heap) + std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1); + other.length_ = 0; + other.is_heap_ = 0; + other.storage_[0] = '\0'; +} + +CompactString &CompactString::operator=(CompactString &&other) noexcept { + if (this != &other) { + this->~CompactString(); + new (this) CompactString(std::move(other)); + } + return *this; +} + +CompactString::~CompactString() { + if (this->is_heap_) { + delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory) + } +} + +bool CompactString::operator==(const CompactString &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0; +} +bool CompactString::operator==(const StringRef &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0; +} + /// WiFi Retry Logic - Priority-Based BSSID Selection /// /// The WiFi component uses a state machine with priority degradation to handle connection failures @@ -349,18 +413,18 @@ bool WiFiComponent::needs_scan_results_() const { return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); } -bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { +bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const { // Check if this SSID is configured as hidden // If explicitly marked hidden, we should always try hidden mode regardless of scan results for (const auto &conf : this->sta_) { - if (conf.get_ssid() == ssid && conf.get_hidden()) { + if (conf.ssid_ == ssid && conf.get_hidden()) { return false; // Treat as not seen - force hidden mode attempt } } // Otherwise, check if we saw it in scan results for (const auto &scan : this->scan_result_) { - if (scan.get_ssid() == ssid) { + if (scan.ssid_ == ssid) { return true; } } @@ -409,14 +473,14 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t continue; } // For BSSID-only configs (empty SSID), match by BSSID - if (sta.get_ssid().empty()) { + if (sta.ssid_.empty()) { if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) { return true; } continue; } // Match by SSID - if (sta.get_ssid() == ssid) { + if (sta.ssid_ == ssid) { return true; } } @@ -465,18 +529,18 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { if (!include_explicit_hidden && sta.get_hidden()) { int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) { - ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str()); continue; } } // In BLIND_RETRY mode, treat all networks as candidates // In SCAN_BASED mode, only retry networks that weren't seen in the scan - if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { - ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i)); + if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.ssid_)) { + ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast<int>(i)); return static_cast<int8_t>(i); } - ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str()); } // No hidden SSIDs found return -1; @@ -593,11 +657,11 @@ void WiFiComponent::start() { // Fast connect optimization: only use when we have saved BSSID+channel data // Without saved data, try first configured network or use normal flow if (loaded_fast_connect) { - ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); + ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str()); this->start_connecting(params); } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { // No saved data, but have configured networks - try first non-hidden network - ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); + ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str()); this->selected_sta_index_ = 0; params = this->build_params_for_current_phase_(); this->start_connecting(params); @@ -827,7 +891,7 @@ void WiFiComponent::setup_ap_config_() { if (this->ap_setup_) return; - if (this->ap_.get_ssid().empty()) { + if (this->ap_.ssid_.empty()) { // Build AP SSID from app name without heap allocation // WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7 static constexpr size_t AP_SSID_MAX_LEN = 32; @@ -863,7 +927,7 @@ void WiFiComponent::setup_ap_config_() { " AP SSID: '%s'\n" " AP Password: '%s'\n" " IP Address: %s", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); + this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); #ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); @@ -960,9 +1024,12 @@ WiFiAP WiFiComponent::get_sta() const { return config ? *config : WiFiAP{}; } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { + this->save_wifi_sta(ssid.c_str(), password.c_str()); +} +void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination - strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 - strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 + strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 + strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); @@ -996,14 +1063,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGI(TAG, "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", - ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, + ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:\n" " SSID: '%s'", - ap.get_ssid().c_str()); + ap.ssid_.c_str()); if (ap.has_bssid()) { ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { @@ -1036,7 +1103,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { client_key_present ? "present" : "not present"); } else { #endif - ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str()); #ifdef USE_WIFI_WPA2_EAP } #endif @@ -1411,7 +1478,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN && config && !config->get_hidden() && this->scan_result_.empty()) { - ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str()); + ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str()); } // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; @@ -1825,11 +1892,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } // Get SSID for logging (use pointer to avoid copy) - const std::string *ssid = nullptr; + const char *ssid = nullptr; if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { - ssid = &this->scan_result_[0].get_ssid(); + ssid = this->scan_result_[0].ssid_.c_str(); } else if (const WiFiAP *config = this->get_selected_sta_()) { - ssid = &config->get_ssid(); + ssid = config->ssid_.c_str(); } // Only decrease priority on the last attempt for this phase @@ -1849,8 +1916,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } char bssid_s[18]; format_mac_addr_upper(failed_bssid.value().data(), bssid_s); - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", - ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "", + bssid_s, old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start @@ -2098,10 +2165,14 @@ void WiFiComponent::save_fast_connect_settings_() { } #endif -void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } +void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } +void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); } void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; } void WiFiAP::clear_bssid() { this->bssid_ = {}; } -void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +void WiFiAP::set_password(const std::string &password) { + this->password_ = CompactString(password.c_str(), password.size()); +} +void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); } #ifdef USE_WIFI_WPA2_EAP void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); } #endif @@ -2111,10 +2182,8 @@ void WiFiAP::clear_channel() { this->channel_ = 0; } void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; } #endif void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } -const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; } bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } -const std::string &WiFiAP::get_password() const { return this->password_; } #ifdef USE_WIFI_WPA2_EAP const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; } #endif @@ -2125,12 +2194,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip #endif bool WiFiAP::get_hidden() const { return this->hidden_; } -WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, - bool is_hidden) +WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, + bool with_auth, bool is_hidden) : bssid_(bssid), channel_(channel), rssi_(rssi), - ssid_(std::move(ssid)), + ssid_(ssid, ssid_len), with_auth_(with_auth), is_hidden_(is_hidden) {} bool WiFiScanResult::matches(const WiFiAP &config) const { @@ -2139,9 +2208,9 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { // don't match SSID if (!this->is_hidden_) return false; - } else if (!config.get_ssid().empty()) { + } else if (!config.ssid_.empty()) { // check if SSID matches - if (config.get_ssid() != this->ssid_) + if (this->ssid_ != config.ssid_) return false; } else { // network is configured without SSID - match other settings @@ -2152,15 +2221,15 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { #ifdef USE_WIFI_WPA2_EAP // BSSID requires auth but no PSK or EAP credentials given - if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) + if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value())) return false; // BSSID does not require auth, but PSK or EAP credentials given - if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value())) + if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value())) return false; #else // If PSK given, only match for networks with auth (and vice versa) - if (config.get_password().empty() == this->with_auth_) + if (config.password_.empty() == this->with_auth_) return false; #endif @@ -2173,7 +2242,6 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { bool WiFiScanResult::get_matches() const { return this->matches_; } void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; } const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; } -const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; } uint8_t WiFiScanResult::get_channel() const { return this->channel_; } int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } @@ -2284,7 +2352,7 @@ void WiFiComponent::process_roaming_scan_() { for (const auto &result : this->scan_result_) { // Must be same SSID, different BSSID - if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid) + if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid) continue; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 58f19c184a..ac28a1bc81 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -172,12 +172,67 @@ template<typename T> using wifi_scan_vector_t = std::vector<T>; template<typename T> using wifi_scan_vector_t = FixedVector<T>; #endif +/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated. +/// Used internally for WiFi SSID/password storage to reduce heap fragmentation. +class CompactString { + public: + static constexpr uint8_t MAX_LENGTH = 127; + static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes + + CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; } + CompactString(const char *str, size_t len); + CompactString(const CompactString &other); + CompactString(CompactString &&other) noexcept; + CompactString &operator=(const CompactString &other); + CompactString &operator=(CompactString &&other) noexcept; + ~CompactString(); + + const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; } + const char *c_str() const { return this->data(); } // Always null-terminated + size_t size() const { return this->length_; } + bool empty() const { return this->length_ == 0; } + + /// Return a StringRef view of this string (zero-copy) + StringRef ref() const { return StringRef(this->data(), this->size()); } + + bool operator==(const CompactString &other) const; + bool operator!=(const CompactString &other) const { return !(*this == other); } + bool operator==(const StringRef &other) const; + bool operator!=(const StringRef &other) const { return !(*this == other); } + bool operator==(const char *other) const { return *this == StringRef(other); } + bool operator!=(const char *other) const { return !(*this == other); } + + protected: + char *get_heap_ptr_() const { + char *ptr; + std::memcpy(&ptr, this->storage_, sizeof(ptr)); + return ptr; + } + void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); } + + // Storage for string data. When is_heap_=0, contains the string directly (null-terminated). + // When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation. + char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator + uint8_t length_ : 7; // String length (0-127) + uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage + // Total size: 20 bytes (19 bytes storage + 1 byte bitfields) +}; + +static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); + class WiFiAP { + friend class WiFiComponent; + friend class WiFiScanResult; + public: void set_ssid(const std::string &ssid); + void set_ssid(const char *ssid); + void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } void set_bssid(const bssid_t &bssid); void clear_bssid(); void set_password(const std::string &password); + void set_password(const char *password); + void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); } #ifdef USE_WIFI_WPA2_EAP void set_eap(optional<EAPAuth> eap_auth); #endif // USE_WIFI_WPA2_EAP @@ -188,10 +243,10 @@ class WiFiAP { void set_manual_ip(optional<ManualIP> manual_ip); #endif void set_hidden(bool hidden); - const std::string &get_ssid() const; + StringRef get_ssid() const { return this->ssid_.ref(); } + StringRef get_password() const { return this->password_.ref(); } const bssid_t &get_bssid() const; bool has_bssid() const; - const std::string &get_password() const; #ifdef USE_WIFI_WPA2_EAP const optional<EAPAuth> &get_eap() const; #endif // USE_WIFI_WPA2_EAP @@ -204,8 +259,8 @@ class WiFiAP { bool get_hidden() const; protected: - std::string ssid_; - std::string password_; + CompactString ssid_; + CompactString password_; #ifdef USE_WIFI_WPA2_EAP optional<EAPAuth> eap_; #endif // USE_WIFI_WPA2_EAP @@ -220,15 +275,18 @@ class WiFiAP { }; class WiFiScanResult { + friend class WiFiComponent; + public: - WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden); + WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, + bool is_hidden); bool matches(const WiFiAP &config) const; bool get_matches() const; void set_matches(bool matches); const bssid_t &get_bssid() const; - const std::string &get_ssid() const; + StringRef get_ssid() const { return this->ssid_.ref(); } uint8_t get_channel() const; int8_t get_rssi() const; bool get_with_auth() const; @@ -242,7 +300,7 @@ class WiFiScanResult { bssid_t bssid_; uint8_t channel_; int8_t rssi_; - std::string ssid_; + CompactString ssid_; int8_t priority_{0}; bool matches_{false}; bool with_auth_; @@ -381,6 +439,8 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + void save_wifi_sta(const char *ssid, const char *password); + void save_wifi_sta(StringRef ssid, StringRef password) { this->save_wifi_sta(ssid.c_str(), password.c_str()); } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -545,7 +605,7 @@ class WiFiComponent : public Component { int8_t find_first_non_hidden_index_() const; /// Check if an SSID was seen in the most recent scan results /// Used to skip hidden mode for SSIDs we know are visible - bool ssid_was_seen_in_scan_(const std::string &ssid) const; + bool ssid_was_seen_in_scan_(const CompactString &ssid) const; /// Check if full scan results are needed (captive portal active, improv, listeners) bool needs_full_scan_results_() const; /// Check if network matches any configured network (for scan result filtering) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 6488de8dae..c87345f0bf 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -247,16 +247,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ssid)) { ESP_LOGE(TAG, "SSID too long"); return false; } - if (ap.get_password().size() > sizeof(conf.password)) { + if (ap.password_.size() > sizeof(conf.password)) { ESP_LOGE(TAG, "Password too long"); return false; } - memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size()); if (ap.has_bssid()) { conf.bssid_set = 1; @@ -266,7 +266,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.threshold.authmode = AUTH_OPEN; } else { // Set threshold based on configured minimum auth mode @@ -738,8 +738,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid); if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) { this->scan_result_.emplace_back( - bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, - std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); + bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr, + it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); } else { this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel); } @@ -832,27 +832,27 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; struct softap_config conf {}; - if (ap.get_ssid().size() > sizeof(conf.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ssid)) { ESP_LOGE(TAG, "AP SSID too long"); return false; } - memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - conf.ssid_len = static_cast<uint8>(ap.get_ssid().size()); + memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + conf.ssid_len = static_cast<uint8>(ap.ssid_.size()); conf.channel = ap.has_channel() ? ap.get_channel() : 1; conf.ssid_hidden = ap.get_hidden(); conf.max_connection = 5; conf.beacon_interval = 100; - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.authmode = AUTH_OPEN; *conf.password = 0; } else { conf.authmode = AUTH_WPA2_PSK; - if (ap.get_password().size() > sizeof(conf.password)) { + if (ap.password_.size() > sizeof(conf.password)) { ESP_LOGE(TAG, "AP password too long"); return false; } - memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size()); } ETS_UART_INTR_DISABLE(); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index d74d083954..52ee482121 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -300,19 +300,19 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { + if (ap.ssid_.size() > sizeof(conf.sta.ssid)) { ESP_LOGE(TAG, "SSID too long"); return false; } - if (ap.get_password().size() > sizeof(conf.sta.password)) { + if (ap.password_.size() > sizeof(conf.sta.password)) { ESP_LOGE(TAG, "Password too long"); return false; } - memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + memcpy(reinterpret_cast<char *>(conf.sta.password), ap.password_.c_str(), ap.password_.size()); // The weakest authmode to accept in the fast scan mode - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.sta.threshold.authmode = WIFI_AUTH_OPEN; } else { // Set threshold based on configured minimum auth mode @@ -864,8 +864,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { bssid_t bssid; std::copy(record.bssid, record.bssid + 6, bssid.begin()); - std::string ssid(ssid_cstr); - this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi, + this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); } else { this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); @@ -1055,26 +1054,26 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ap.ssid)) { ESP_LOGE(TAG, "AP SSID too long"); return false; } - memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); + memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size()); conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1; - conf.ap.ssid_hidden = ap.get_ssid().size(); + conf.ap.ssid_hidden = ap.get_hidden(); conf.ap.max_connection = 5; conf.ap.beacon_interval = 100; - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.ap.authmode = WIFI_AUTH_OPEN; *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - if (ap.get_password().size() > sizeof(conf.ap.password)) { + if (ap.password_.size() > sizeof(conf.ap.password)) { ESP_LOGE(TAG, "AP password too long"); return false; } - memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast<char *>(conf.ap.password), ap.password_.c_str(), ap.password_.size()); } // pairwise cipher of SoftAP, group cipher will be derived using this. diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 5fd9d7663b..2cc05928af 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -193,7 +193,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; String ssid = WiFi.SSID(); - if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) { + if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) { WiFi.disconnect(); } @@ -213,7 +213,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { s_sta_state = LTWiFiSTAState::CONNECTING; s_ignored_disconnect_count = 0; - WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), + WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), ap.get_channel(), // 0 = auto ap.has_bssid() ? ap.get_bssid().data() : NULL); if (status != WL_CONNECTED) { @@ -688,7 +688,7 @@ void WiFiComponent::wifi_scan_done_callback_() { auto &ap = scan->ap[i]; this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3], ap.bssid.addr[4], ap.bssid.addr[5]}, - std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, + ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); } else { auto &ap = scan->ap[i]; @@ -735,7 +735,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { yield(); - return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), + return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden()); } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 818ad1059c..1baf21e2b2 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -78,7 +78,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; #endif - auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); + auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str()); if (ret != WL_CONNECTED) return false; @@ -149,9 +149,8 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re bssid_t bssid; std::copy(result->bssid, result->bssid + 6, bssid.begin()); - std::string ssid(ssid_cstr); - WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, - ssid_cstr[0] == '\0'); + WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi, + result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0'); if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { this->scan_result_.push_back(res); } @@ -204,7 +203,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } #endif - WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1); + WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1); return true; } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index a63b30b892..b5ebfd7390 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t<wi for (const auto &scan : results) { if (scan.get_is_hidden()) continue; - const std::string &ssid = scan.get_ssid(); + const auto &ssid = scan.get_ssid(); // Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9 if (ptr + ssid.size() + 9 > end) break; From fecb145a7170b708e2e03e50a39a04b2eada94d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 17:42:18 -0600 Subject: [PATCH 245/251] [web_server_idf] Revert multipart upload buffer back to heap to fix httpd stack overflow (#13941) --- esphome/components/web_server_idf/web_server_idf.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 2e07fb6e0a..d7d6f90355 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -881,12 +881,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } }); - // Process data - use stack buffer to avoid heap allocation - char buffer[MULTIPART_CHUNK_SIZE]; + // Use heap buffer - 1460 bytes is too large for the httpd task stack + auto buffer = std::make_unique<char[]>(MULTIPART_CHUNK_SIZE); size_t bytes_since_yield = 0; for (size_t remaining = r->content_len; remaining > 0;) { - int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE)); + int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE)); if (recv_len <= 0) { httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, @@ -894,7 +894,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; } - if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) { + if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) { ESP_LOGW(TAG, "Multipart parser error"); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); return ESP_FAIL; From ae42bfa40448930cf9c101a7bfbc200c86c57c7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 17:42:33 -0600 Subject: [PATCH 246/251] [web_server_idf] Remove std::string temporaries from multipart header parsing (#13940) --- .../components/web_server_idf/multipart.cpp | 87 +++++++++++-------- esphome/components/web_server_idf/multipart.h | 9 +- esphome/components/web_server_idf/utils.cpp | 11 ++- esphome/components/web_server_idf/utils.h | 4 +- .../web_server_idf/web_server_idf.cpp | 5 +- 5 files changed, 67 insertions(+), 49 deletions(-) diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 52dafeb997..7744272a5c 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -54,14 +54,15 @@ size_t MultipartReader::parse(const char *data, size_t len) { void MultipartReader::process_header_(const char *value, size_t length) { // Process the completed header (field + value pair) - std::string value_str(value, length); + const char *field = current_header_field_.c_str(); + size_t field_len = current_header_field_.length(); - if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) { + if (str_startswith_case_insensitive(field, field_len, "content-disposition")) { // Parse name and filename from Content-Disposition - current_part_.name = extract_header_param(value_str, "name"); - current_part_.filename = extract_header_param(value_str, "filename"); - } else if (str_startswith_case_insensitive(current_header_field_, "content-type")) { - current_part_.content_type = str_trim(value_str); + extract_header_param(value, length, "name", current_part_.name); + extract_header_param(value, length, "filename", current_part_.filename); + } else if (str_startswith_case_insensitive(field, field_len, "content-type")) { + str_trim(value, length, current_part_.content_type); } // Clear field for next header @@ -107,25 +108,29 @@ int MultipartReader::on_part_data_end(multipart_parser *parser) { // ========== Utility Functions ========== // Case-insensitive string prefix check -bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) { - if (str.length() < prefix.length()) { +bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix) { + size_t prefix_len = strlen(prefix); + if (str_len < prefix_len) { return false; } - return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length()); + return str_ncmp_ci(str, prefix, prefix_len); } // Extract a parameter value from a header line // Handles both quoted and unquoted values -std::string extract_header_param(const std::string &header, const std::string ¶m) { +// Assigns to out if found, clears out otherwise +void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out) { + size_t param_len = strlen(param); size_t search_pos = 0; - while (search_pos < header.length()) { + while (search_pos < header_len) { // Look for param name - const char *found = stristr(header.c_str() + search_pos, param.c_str()); + const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param); if (!found) { - return ""; + out.clear(); + return; } - size_t pos = found - header.c_str(); + size_t pos = found - header; // Check if this is a word boundary (not part of another parameter) if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') { @@ -134,14 +139,14 @@ std::string extract_header_param(const std::string &header, const std::string &p } // Move past param name - pos += param.length(); + pos += param_len; // Skip whitespace and find '=' - while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { + while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } - if (pos >= header.length() || header[pos] != '=') { + if (pos >= header_len || header[pos] != '=') { search_pos = pos; continue; } @@ -149,36 +154,39 @@ std::string extract_header_param(const std::string &header, const std::string &p pos++; // Skip '=' // Skip whitespace after '=' - while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { + while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } - if (pos >= header.length()) { - return ""; + if (pos >= header_len) { + out.clear(); + return; } // Check if value is quoted if (header[pos] == '"') { pos++; - size_t end = header.find('"', pos); - if (end != std::string::npos) { - return header.substr(pos, end - pos); + const char *end = static_cast<const char *>(memchr(header + pos, '"', header_len - pos)); + if (end) { + out.assign(header + pos, end - (header + pos)); + return; } // Malformed - no closing quote - return ""; + out.clear(); + return; } // Unquoted value - find the end (semicolon, comma, or end of string) size_t end = pos; - while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' && - header[end] != '\t') { + while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') { end++; } - return header.substr(pos, end - pos); + out.assign(header + pos, end - pos); + return; } - return ""; + out.clear(); } // Parse boundary from Content-Type header @@ -189,13 +197,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st return false; } + size_t content_type_len = strlen(content_type); + // Check for multipart/form-data (case-insensitive) - if (!stristr(content_type, "multipart/form-data")) { + if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) { return false; } // Look for boundary parameter - const char *b = stristr(content_type, "boundary="); + const char *b = strcasestr_n(content_type, content_type_len, "boundary="); if (!b) { return false; } @@ -238,14 +248,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st return true; } -// Trim whitespace from both ends of a string -std::string str_trim(const std::string &str) { - size_t start = str.find_first_not_of(" \t\r\n"); - if (start == std::string::npos) { - return ""; - } - size_t end = str.find_last_not_of(" \t\r\n"); - return str.substr(start, end - start + 1); +// Trim whitespace from both ends, assign result to out +void str_trim(const char *str, size_t len, std::string &out) { + const char *start = str; + const char *end = str + len; + while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n')) + start++; + while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n')) + end--; + out.assign(start, end - start); } } // namespace esphome::web_server_idf diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index 9008be6459..cb1e0ecd1d 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -66,19 +66,20 @@ class MultipartReader { // ========== Utility Functions ========== // Case-insensitive string prefix check -bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix); +bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix); // Extract a parameter value from a header line // Handles both quoted and unquoted values -std::string extract_header_param(const std::string &header, const std::string ¶m); +// Assigns to out if found, clears out otherwise +void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out); // Parse boundary from Content-Type header // Returns true if boundary found, false otherwise // boundary_start and boundary_len will point to the boundary value bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len); -// Trim whitespace from both ends of a string -std::string str_trim(const std::string &str); +// Trim whitespace from both ends, assign result to out +void str_trim(const char *str, size_t len, std::string &out); } // namespace esphome::web_server_idf #endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index a1a2793b4a..81ae626277 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -98,8 +98,8 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { return true; } -// Case-insensitive string search (like strstr but case-insensitive) -const char *stristr(const char *haystack, const char *needle) { +// Bounded case-insensitive string search (like strcasestr but length-bounded) +const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) { if (!haystack) { return nullptr; } @@ -109,7 +109,12 @@ const char *stristr(const char *haystack, const char *needle) { return haystack; } - for (const char *p = haystack; *p; p++) { + if (haystack_len < needle_len) { + return nullptr; + } + + const char *end = haystack + haystack_len - needle_len + 1; + for (const char *p = haystack; p < end; p++) { if (str_ncmp_ci(p, needle, needle_len)) { return p; } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index bb0610dac0..87635c0458 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -25,8 +25,8 @@ inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b) // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n); -// Case-insensitive string search (like strstr but case-insensitive) -const char *stristr(const char *haystack, const char *needle); +// Bounded case-insensitive string search (like strcasestr but length-bounded) +const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle); } // namespace esphome::web_server_idf #endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index d7d6f90355..f1f89beb49 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -171,10 +171,11 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { const char *content_type_char = content_type.value().c_str(); // Check most common case first - if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) { + size_t content_type_len = strlen(content_type_char); + if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) { // Normal form data - proceed with regular handling #ifdef USE_WEBSERVER_OTA - } else if (stristr(content_type_char, "multipart/form-data") != nullptr) { + } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) { auto *server = static_cast<AsyncWebServer *>(r->user_ctx); return server->handle_multipart_upload_(r, content_type_char); #endif From 96eb129cf80f7dcf8f0aa08e57a3b341fc0293c6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:29:17 -0500 Subject: [PATCH 247/251] [esp32] Bump Arduino to 3.3.7, platform to 55.03.37 (#13943) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 14 ++++++++------ esphome/components/esp32/boards.py | 16 ++++++++++++++++ esphome/core/defines.h | 2 +- platformio.ini | 6 +++--- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 3ffe32af88..d6d401ee66 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7 +ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a680a78951..b78b945a24 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -645,11 +645,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, 6), - "latest": cv.Version(3, 3, 6), - "dev": cv.Version(3, 3, 6), + "recommended": cv.Version(3, 3, 7), + "latest": cv.Version(3, 3, 7), + "dev": cv.Version(3, 3, 7), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 7): cv.Version(55, 3, 37), 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"), @@ -668,6 +669,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, 7): cv.Version(5, 5, 2), 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), @@ -691,7 +693,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, 36), + cv.Version(5, 5, 2): cv.Version(55, 3, 37), 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), @@ -708,8 +710,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 36), - "latest": cv.Version(55, 3, 36), + "recommended": cv.Version(55, 3, 37), + "latest": cv.Version(55, 3, 37), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 8b066064f2..66367d63ae 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1686,6 +1686,10 @@ BOARDS = { "name": "Espressif ESP32-C6-DevKitM-1", "variant": VARIANT_ESP32C6, }, + "esp32-c61-devkitc1": { + "name": "Espressif ESP32-C61-DevKitC-1 (4 MB Flash)", + "variant": VARIANT_ESP32C61, + }, "esp32-c61-devkitc1-n8r2": { "name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)", "variant": VARIANT_ESP32C61, @@ -1718,6 +1722,10 @@ BOARDS = { "name": "Espressif ESP32-P4 rev.300 generic", "variant": VARIANT_ESP32P4, }, + "esp32-p4_r3-evboard": { + "name": "Espressif ESP32-P4 Function EV Board v1.6 (rev.301)", + "variant": VARIANT_ESP32P4, + }, "esp32-pico-devkitm-2": { "name": "Espressif ESP32-PICO-DevKitM-2", "variant": VARIANT_ESP32, @@ -2554,6 +2562,10 @@ BOARDS = { "name": "XinaBox CW02", "variant": VARIANT_ESP32, }, + "yb_esp32s3_amp": { + "name": "YelloByte YB-ESP32-S3-AMP", + "variant": VARIANT_ESP32S3, + }, "yb_esp32s3_amp_v2": { "name": "YelloByte YB-ESP32-S3-AMP (Rev.2)", "variant": VARIANT_ESP32S3, @@ -2562,6 +2574,10 @@ BOARDS = { "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", "variant": VARIANT_ESP32S3, }, + "yb_esp32s3_dac": { + "name": "YelloByte YB-ESP32-S3-DAC", + "variant": VARIANT_ESP32S3, + }, "yb_esp32s3_drv": { "name": "YelloByte YB-ESP32-S3-DRV", "variant": VARIANT_ESP32S3, diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7e6df31ea2..8fc30760c7 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -239,7 +239,7 @@ #define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 6) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_MANUAL_IP diff --git a/platformio.ini b/platformio.ini index 6b29daf87a..09b3d8722d 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.36/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component @@ -169,7 +169,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.36/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/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 db6aea8969ba5fe6f7e6202e1d8f68f27a13cad3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:11:48 -0500 Subject: [PATCH 248/251] Allow Python 3.14 (#13945) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .github/workflows/ci.yml | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 841c297bce..8718772f53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,7 @@ jobs: python-version: - "3.11" - "3.13" + - "3.14" os: - ubuntu-latest - macOS-latest diff --git a/pyproject.toml b/pyproject.toml index 339bc65eed..c6a2c22a5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ classifiers = [ "Topic :: Home Automation", ] -# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 -requires-python = ">=3.11.0,<3.14" +# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76 +requires-python = ">=3.11.0,<3.15" dynamic = ["dependencies", "optional-dependencies", "version"] From c9d2adb717954968d212cc242a08751d41d3de56 Mon Sep 17 00:00:00 2001 From: Awesome Walrus <74941879+QRPp@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:34:59 +0000 Subject: [PATCH 249/251] [wifi] Allow fast_connect without preconfigured networks (#13946) --- esphome/components/wifi/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 6863e6fb62..e865de8663 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -288,11 +288,6 @@ def _validate(config): config = config.copy() config[CONF_NETWORKS] = [] - if config.get(CONF_FAST_CONNECT, False): - networks = config.get(CONF_NETWORKS, []) - if not networks: - raise cv.Invalid("At least one network required for fast_connect!") - if CONF_USE_ADDRESS not in config: use_address = CORE.name + config[CONF_DOMAIN] if CONF_MANUAL_IP in config: From da1ea2cfa3204797a9ea79a2d4103c4ac56b469a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Feb 2026 23:07:05 -0600 Subject: [PATCH 250/251] [ethernet] Add per-PHY compile guards to eliminate unused PHY drivers (#13947) --- esphome/components/ethernet/__init__.py | 7 +- .../ethernet/ethernet_component.cpp | 69 ++++++++++++------- esphome/core/defines.h | 6 ++ 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 38489ceb2b..52f5f44d41 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -130,11 +130,16 @@ ETHERNET_TYPES = { } # PHY types that need compile-time defines for conditional compilation +# Each RMII PHY type gets a define so unused PHY drivers are excluded by the linker _PHY_TYPE_TO_DEFINE = { + "LAN8720": "USE_ETHERNET_LAN8720", + "RTL8201": "USE_ETHERNET_RTL8201", + "DP83848": "USE_ETHERNET_DP83848", + "IP101": "USE_ETHERNET_IP101", + "JL1101": "USE_ETHERNET_JL1101", "KSZ8081": "USE_ETHERNET_KSZ8081", "KSZ8081RNA": "USE_ETHERNET_KSZ8081", "LAN8670": "USE_ETHERNET_LAN8670", - # Add other PHY types here only if they need conditional compilation } SPI_ETHERNET_TYPES = ["W5500", "DM9051"] diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index af7fed608b..f9d98ad51b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -186,31 +186,43 @@ void EthernetComponent::setup() { } #endif #if CONFIG_ETH_USE_ESP32_EMAC +#ifdef USE_ETHERNET_LAN8720 case ETHERNET_TYPE_LAN8720: { this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_RTL8201 case ETHERNET_TYPE_RTL8201: { this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_DP83848 case ETHERNET_TYPE_DP83848: { this->phy_ = esp_eth_phy_new_dp83848(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_IP101 case ETHERNET_TYPE_IP101: { this->phy_ = esp_eth_phy_new_ip101(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_JL1101 case ETHERNET_TYPE_JL1101: { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_KSZ8081 case ETHERNET_TYPE_KSZ8081: case ETHERNET_TYPE_KSZ8081RNA: { this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); break; } +#endif #ifdef USE_ETHERNET_LAN8670 case ETHERNET_TYPE_LAN8670: { this->phy_ = esp_eth_phy_new_lan867x(&phy_config); @@ -343,26 +355,32 @@ void EthernetComponent::loop() { void EthernetComponent::dump_config() { const char *eth_type; switch (this->type_) { +#ifdef USE_ETHERNET_LAN8720 case ETHERNET_TYPE_LAN8720: eth_type = "LAN8720"; break; - +#endif +#ifdef USE_ETHERNET_RTL8201 case ETHERNET_TYPE_RTL8201: eth_type = "RTL8201"; break; - +#endif +#ifdef USE_ETHERNET_DP83848 case ETHERNET_TYPE_DP83848: eth_type = "DP83848"; break; - +#endif +#ifdef USE_ETHERNET_IP101 case ETHERNET_TYPE_IP101: eth_type = "IP101"; break; - +#endif +#ifdef USE_ETHERNET_JL1101 case ETHERNET_TYPE_JL1101: eth_type = "JL1101"; break; - +#endif +#ifdef USE_ETHERNET_KSZ8081 case ETHERNET_TYPE_KSZ8081: eth_type = "KSZ8081"; break; @@ -370,19 +388,22 @@ void EthernetComponent::dump_config() { case ETHERNET_TYPE_KSZ8081RNA: eth_type = "KSZ8081RNA"; break; - +#endif +#if CONFIG_ETH_SPI_ETHERNET_W5500 case ETHERNET_TYPE_W5500: eth_type = "W5500"; break; - - case ETHERNET_TYPE_OPENETH: - eth_type = "OPENETH"; - break; - +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 case ETHERNET_TYPE_DM9051: eth_type = "DM9051"; break; - +#endif +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: + eth_type = "OPENETH"; + break; +#endif #ifdef USE_ETHERNET_LAN8670 case ETHERNET_TYPE_LAN8670: eth_type = "LAN8670"; @@ -686,16 +707,22 @@ void EthernetComponent::dump_connect_params_() { char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " IP Address: %s\n" " Hostname: '%s'\n" " Subnet: %s\n" " Gateway: %s\n" " DNS1: %s\n" - " DNS2: %s", + " DNS2: %s\n" + " MAC Address: %s\n" + " Is Full Duplex: %s\n" + " Link Speed: %u", network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), - network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf)); + network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf), + this->get_eth_mac_address_pretty_into_buffer(mac_buf), + YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); #if USE_NETWORK_IPV6 struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -706,14 +733,6 @@ void EthernetComponent::dump_connect_params_() { ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); } #endif /* USE_NETWORK_IPV6 */ - - char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - ESP_LOGCONFIG(TAG, - " MAC Address: %s\n" - " Is Full Duplex: %s\n" - " Link Speed: %u", - this->get_eth_mac_address_pretty_into_buffer(mac_buf), - YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); } #ifdef USE_ETHERNET_SPI @@ -837,13 +856,15 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { esp_err_t err; - constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; +#ifdef USE_ETHERNET_RTL8201 + constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); } +#endif ESP_LOGD(TAG, "Writing to PHY Register Address: 0x%02" PRIX32 "\n" @@ -852,11 +873,13 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); +#ifdef USE_ETHERNET_RTL8201 if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { ESP_LOGD(TAG, "Select PHY Register Page 0x00"); err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); } +#endif } #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8fc30760c7..bfa33e4e59 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -241,7 +241,13 @@ #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7) #define USE_ETHERNET +#define USE_ETHERNET_LAN8720 +#define USE_ETHERNET_RTL8201 +#define USE_ETHERNET_DP83848 +#define USE_ETHERNET_IP101 +#define USE_ETHERNET_JL1101 #define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_LAN8670 #define USE_ETHERNET_MANUAL_IP #define USE_ETHERNET_IP_STATE_LISTENERS #define USE_ETHERNET_CONNECT_TRIGGER From d6461251f925c4be1197a34f7d6be761a1cc014f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:04:19 +1300 Subject: [PATCH 251/251] Bump version to 2026.3.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index efc2f464e3..1a9e0b4e10 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0-dev +PROJECT_NUMBER = 2026.3.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 00d8013cc6..f72cbc8893 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0-dev" +__version__ = "2026.3.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (