From d29288547ec2d5b9e68b61c2186522efaad041b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:33 -0600 Subject: [PATCH 01/89] [core] Use constexpr for PROGMEM arrays (#14127) Co-authored-by: Claude Opus 4.6 --- esphome/cpp_generator.py | 2 +- tests/unit_tests/test_cpp_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 83f2d6cf81..fe666bdd6e 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -424,7 +424,7 @@ class ProgmemAssignmentExpression(AssignmentExpression): super().__init__(type_, "", name, rhs) def __str__(self): - return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}" + return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}" class StaticConstAssignmentExpression(AssignmentExpression): diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 8755e6e2a1..049d21027f 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -325,7 +325,7 @@ class TestStatements: ), ( cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar"), - 'static const uint16_t foo[] PROGMEM = "bar"', + 'static constexpr uint16_t foo[] PROGMEM = "bar"', ), ), ) From 94712b3961dd962d0a43636301c5edccd08121b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:44 -0600 Subject: [PATCH 02/89] [esp8266][web_server] Use constexpr for PROGMEM arrays in codegen (#14128) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp8266/gpio.py | 4 ++-- esphome/components/web_server/__init__.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 2e8d6496bc..43508afaf9 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -202,11 +202,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 294a5e0a15..9305a2de61 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -280,10 +280,8 @@ def add_resource_as_progmem( content_encoded = gzip.compress(content_encoded) content_encoded_size = len(content_encoded) bytes_as_int = ", ".join(str(x) for x in content_encoded) - uint8_t = f"const uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" - size_t = ( - f"const size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" - ) + uint8_t = f"constexpr uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" + size_t = f"constexpr size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" cg.add_global(cg.RawExpression(uint8_t)) cg.add_global(cg.RawExpression(size_t)) From c1265a9490711813a6036b8a772dd36e3e871089 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:57 -0600 Subject: [PATCH 03/89] [core] Use constexpr for hand-written PROGMEM arrays in C++ (#14129) Co-authored-by: Claude Opus 4.6 --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/bme680/bme680.cpp | 8 +++--- .../components/captive_portal/captive_index.h | 4 +-- esphome/components/display/display.cpp | 6 ++-- esphome/components/ili9xxx/ili9xxx_init.h | 28 +++++++++---------- esphome/components/max7219/max7219.cpp | 2 +- esphome/components/max7219digit/max7219font.h | 2 +- .../components/ssd1306_base/ssd1306_base.cpp | 2 +- esphome/components/st7735/st7735.cpp | 2 +- esphome/components/tm1621/tm1621.cpp | 2 +- esphome/components/tm1637/tm1637.cpp | 2 +- esphome/components/tm1638/sevenseg.h | 2 +- .../components/web_server/server_index_v2.h | 4 +-- .../components/web_server/server_index_v3.h | 4 +-- 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 492988128a..2aad732f7f 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -19,7 +19,7 @@ namespace esphome::api { static const char *const TAG = "api.noise"; #ifdef USE_ESP8266 -static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; #else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 5e52c84b3d..e3cd80de00 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -22,11 +22,11 @@ static const uint8_t BME680_REGISTER_CHIPID = 0xD0; static const uint8_t BME680_REGISTER_FIELD0 = 0x1D; -const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, - 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, + 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; -const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, - -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, + -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; [[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) { switch (oversampling) { diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 645ebb7a2f..a81edc1900 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 53a087803c..2bd7d03600 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -810,9 +810,9 @@ bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { return min_y < max_y; } -const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' - {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' - {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' +constexpr uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 7b176ed57a..f0c6a94a65 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -7,7 +7,7 @@ namespace esphome { namespace ili9xxx { // clang-format off -static const uint8_t PROGMEM INITCMD_M5STACK[] = { +static constexpr uint8_t PROGMEM INITCMD_M5STACK[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -37,7 +37,7 @@ static const uint8_t PROGMEM INITCMD_M5STACK[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_M5CORE[] = { +static constexpr uint8_t PROGMEM INITCMD_M5CORE[] = { ILI9XXX_SETEXTC, 3, 0xFF,0x93,0x42, // Turn on the external command ILI9XXX_PWCTR1 , 2, 0x12, 0x12, ILI9XXX_PWCTR2 , 1, 0x03, @@ -56,7 +56,7 @@ static const uint8_t PROGMEM INITCMD_M5CORE[] = { -static const uint8_t PROGMEM INITCMD_ILI9341[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9341[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -86,7 +86,7 @@ static const uint8_t PROGMEM INITCMD_ILI9341[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9481[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -105,7 +105,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481_18[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -124,7 +124,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9486[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9486[] = { ILI9XXX_SLPOUT, 0x80, ILI9XXX_PIXFMT, 1, 0x55, ILI9XXX_PWCTR3, 1, 0x44, @@ -173,7 +173,7 @@ static const uint8_t INITCMD_WAVESHARE_RES_3_5[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9488_A[] = { ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, @@ -206,7 +206,7 @@ static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ST7796[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7796[] = { // This ST7796S initilization routine was copied from https://github.com/prenticedavid/Adafruit_ST7796S_kbv/blob/master/Adafruit_ST7796S_kbv.cpp ILI9XXX_SWRESET, 0x80, // Soft reset, then delay 150 ms ILI9XXX_CSCON, 1, 0xC3, // ?? Unlock Manufacturer @@ -226,7 +226,7 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOX[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOX[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -256,7 +256,7 @@ static const uint8_t PROGMEM INITCMD_S3BOX[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -286,7 +286,7 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7789V[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7789V[] = { ILI9XXX_SLPOUT , 0x80, // Exit Sleep ILI9XXX_DISPON , 0x80, // Display on ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR @@ -313,7 +313,7 @@ static const uint8_t PROGMEM INITCMD_ST7789V[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9A01A[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9A01A[] = { 0xEF, 0, 0xEB, 1, 0x14, // ? 0xFE, 0, @@ -367,7 +367,7 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9D01N[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9D01N[] = { // Enable Inter_command 0xFE, 0, // Inter Register Enable 1 (FEh) 0xEF, 0, // Inter Register Enable 2 (EFh) @@ -426,7 +426,7 @@ static const uint8_t PROGMEM INITCMD_GC9D01N[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7735[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7735[] = { ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms ILI9XXX_DELAY(10), ILI9XXX_SLPOUT , 0, // Exit Sleep, delay diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index d701e6fc86..bec62ea005 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -15,7 +15,7 @@ static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; static const uint8_t MAX7219_REGISTER_TEST = 0x0F; static const uint8_t MAX7219_UNKNOWN_CHAR = 0b11111111; -const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { +constexpr uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h index 22d64d1ecd..53674dc60f 100644 --- a/esphome/components/max7219digit/max7219font.h +++ b/esphome/components/max7219digit/max7219font.h @@ -7,7 +7,7 @@ namespace max7219digit { // bit patterns for the CP437 font -const uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { +constexpr uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index be99bd93da..5bd83ec8a8 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -49,7 +49,7 @@ struct ModelDimensions { uint8_t width; uint8_t height; }; -static const ModelDimensions MODEL_DIMS[] PROGMEM = { +static constexpr ModelDimensions MODEL_DIMS[] PROGMEM = { {128, 32}, // SSD1306_MODEL_128_32 {128, 64}, // SSD1306_MODEL_128_64 {96, 16}, // SSD1306_MODEL_96_16 diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 1a74b5ce1e..58459b79bb 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -68,7 +68,7 @@ static const uint8_t ST7735_GMCTRP1 = 0xE0; static const uint8_t ST7735_GMCTRN1 = 0xE1; // clang-format off -static const uint8_t PROGMEM +static constexpr uint8_t PROGMEM BCMD[] = { // Init commands for 7735B screens 18, // 18 commands in list: ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp index 6859973857..c82d306460 100644 --- a/esphome/components/tm1621/tm1621.cpp +++ b/esphome/components/tm1621/tm1621.cpp @@ -23,7 +23,7 @@ enum Tm1621Device { TM1621_USER, TM1621_POWR316D, TM1621_THR316D }; const uint8_t TM1621_COMMANDS[] = {TM1621_SYS_EN, TM1621_LCD_ON, TM1621_BIAS, TM1621_TIMER_DIS, TM1621_WDT_DIS, TM1621_TONE_OFF, TM1621_IRQ_DIS}; -const char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; +constexpr char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; // 0 1 2 3 4 5 6 7 8 9 - off const uint8_t TM1621_DIGIT_ROW[2][12] = {{0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x6F, 0x51, 0x7F, 0x7B, 0x20, 0x00}, {0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02, 0x00}}; diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 49da01472f..f9c876f40c 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -27,7 +27,7 @@ const uint8_t TM1637_DATA_FIXED_ADDR = 0x04; //!< Fixed address // --- // D X // XABCDEFG -const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { +constexpr uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/tm1638/sevenseg.h b/esphome/components/tm1638/sevenseg.h index e20a55a69f..a4c16c7422 100644 --- a/esphome/components/tm1638/sevenseg.h +++ b/esphome/components/tm1638/sevenseg.h @@ -4,7 +4,7 @@ namespace esphome { namespace tm1638 { namespace TM1638Translation { -const unsigned char SEVEN_SEG[] PROGMEM = { +constexpr unsigned char SEVEN_SEG[] PROGMEM = { 0x00, /* (space) */ 0x86, /* ! */ 0x22, /* " */ diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index cc37db6a6b..b5dac9ae4c 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -697,7 +697,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x56, 0x78, 0xff, 0xff, 0x01, 0xa2, 0x89, 0x8c, 0x0d, 0xc4, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xc3, 0x97, 0x11, 0x55, 0xb5, 0x65, 0x2c, 0x8a, 0x8a, 0x55, 0x0b, 0xd0, 0xba, 0x80, 0x1b, 0x32, 0xb0, 0x81, 0x4f, 0x27, 0x63, 0xf1, 0x7e, 0x88, 0xe3, 0xd8, 0x52, 0x84, 0x55, 0xe8, 0x35, 0x5b, 0x2b, 0x82, 0xe1, 0xed, 0x1f, 0xfd, 0xde, 0x63, 0x38, 0x3a, 0x71, 0x78, 0xb0, 0x42, 0x17, 0x15, 0x54, 0x23, 0xe1, 0xaa, 0x28, 0x11, 0x94, 0x23, diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index bd47071dce..1f61b19fb5 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -4104,7 +4104,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x37, 0x7a, 0x03, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x5b, 0x36, 0x7a, 0x53, 0xc2, 0x36, 0x06, 0x5a, 0x1f, 0xd4, 0x4e, 0x00, 0xb3, 0xd6, 0xea, 0xff, 0x0a, 0xab, 0x51, 0x94, 0xb1, 0xe6, 0xb0, 0x2e, 0x61, 0xbb, 0x1a, 0x70, 0x3b, 0xd8, 0x06, 0xfd, 0x7d, 0x2f, 0x1a, 0x00, 0x55, 0x35, 0xe3, 0xa8, 0x1c, 0x62, 0xca, 0xd3, 0xb4, 0x00, 0xdb, 0x5e, 0x43, 0xa7, 0x14, 0x08, 0xa4, 0x51, 0x99, 0x96, 0xb6, From afbc45bf32fe8146183d0d44fc16213cdebddbdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 20:35:42 -0600 Subject: [PATCH 04/89] [e131] Drain all queued packets per loop iteration (#14133) --- esphome/components/e131/e131.cpp | 26 +++++--------------------- esphome/components/e131/e131.h | 9 +++++++++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 941927122c..a7a695c167 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -70,27 +70,12 @@ void E131Component::loop() { E131Packet packet; int universe = 0; uint8_t buf[1460]; + ssize_t len; -#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) - ssize_t len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { - return; - } - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - return; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - + // Drain all queued packets so multi-universe frames are applied + // atomically before the light writes. Without this, each universe + // packet would trigger a separate full-strip write causing tearing. + while ((len = this->read_(buf, sizeof(buf))) > 0) { if (!this->packet_(buf, (size_t) len, universe, packet)) { ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); continue; @@ -100,7 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index fee447b678..8f0b808946 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -46,6 +46,15 @@ class E131Component : public esphome::Component { void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } protected: + inline ssize_t read_(uint8_t *buf, size_t len) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + return this->socket_->read(buf, len); +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.parsePacket()) + return -1; + return this->udp_.read(buf, len); +#endif + } bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); From 7a2a149061c3b9480a3d1900f3e8cad1f1ad75ac Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:43:29 -0500 Subject: [PATCH 05/89] [esp32] Bump ESP-IDF to 5.5.3 (#14122) Co-authored-by: Claude Opus 4.6 --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 17 +++++++++++++---- esphome/components/esp32_ble/__init__.py | 15 +++++++++------ platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index d6d401ee66..df584fa716 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e +3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b1b3f0dc16..cdde1c4ed5 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -643,7 +643,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, 7): cv.Version(5, 5, 3), 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), @@ -662,11 +662,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 2), - "latest": cv.Version(5, 5, 2), - "dev": cv.Version(5, 5, 2), + "recommended": cv.Version(5, 5, 3), + "latest": cv.Version(5, 5, 3), + "dev": cv.Version(5, 5, 3), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3): cv.Version(55, 3, 37), 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"), @@ -1471,6 +1472,14 @@ async def to_code(config): f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 + # from y to n. PlatformIO uses sections.ld.in (for rev <3) or + # sections.rev3.ld.in (for rev >=3) based on board definition. + # Set the sdkconfig option to match the board's revision. + if variant == VARIANT_ESP32P4: + is_rev3 = "_r3" in config[CONF_BOARD] + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, # and for PSRAM users saves significant IRAM by keeping C library functions in ROM. diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index d2020ada22..80fcf051b9 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -413,18 +413,21 @@ def final_validation(config): add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True) add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True) - # Check if BLE Server is needed - has_ble_server = "esp32_ble_server" in full_config - # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client) has_ble_client = ( "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled - # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1) + # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a + # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED + # guard, causing a compile error when both are disabled. + # Additionally, when GATT Client is enabled, GATT Server must also be enabled + # as an internal dependency in the Bluedroid stack. # See: https://github.com/espressif/esp-idf/issues/17724 - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: + # has_ble_server = "esp32_ble_server" in full_config + # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index 09b3d8722d..fdd6a36428 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino 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.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 + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf 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 + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = espidf lib_deps = From b67b2cc3ab86ebbc7cfb3d0a893bf96bd50ef9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:20 -0500 Subject: [PATCH 06/89] [ld2450] Add frame header synchronization to fix initialization regression (#14135) Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ld2450/ld2450.cpp | 22 ++- esphome/components/ld2450/ld2450.h | 1 + tests/components/ld2450/common.h | 61 +++++++ tests/components/ld2450/ld2450_readline.cpp | 181 ++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/components/ld2450/common.h create mode 100644 tests/components/ld2450/ld2450_readline.cpp diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 1ea5c18271..2af45235a3 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,15 +769,33 @@ void LD2450Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from accumulating mid-frame data after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index fe69cd81d0..44e5912b2a 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h new file mode 100644 index 0000000000..d5ffbe1295 --- /dev/null +++ b/tests/components/ld2450/common.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/ld2450/ld2450.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::ld2450::testing { + +// Mock UART component to satisfy UARTDevice parent requirement. +class MockUARTComponent : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(size_t, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +// Expose protected members for testing. +class TestableLD2450 : public LD2450Component { + public: + using LD2450Component::buffer_data_; + using LD2450Component::buffer_pos_; + using LD2450Component::readline_; + + void feed(const std::vector &data) { + for (uint8_t byte : data) { + this->readline_(byte); + } + } +}; + +// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes +// All-zero targets means no presence detected. +inline std::vector make_periodic_frame(uint8_t fill = 0x00) { + std::vector frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER + for (int i = 0; i < 24; i++) { + frame.push_back(fill); // 3 targets * 8 bytes + } + frame.push_back(0x55); // DATA_FRAME_FOOTER + frame.push_back(0xCC); + return frame; +} + +// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful. +// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes +inline std::vector make_ack_frame() { + return { + 0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER + 0x04, 0x00, // length = 4 + 0xFF, 0x01, // command = enable_conf, status = success + 0x00, 0x00, // result = ok + 0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER + }; +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp new file mode 100644 index 0000000000..68b1dd6881 --- /dev/null +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +namespace esphome::ld2450::testing { + +class LD2450ReadlineTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->mock_uart_); + // Ensure clean state + ASSERT_EQ(this->ld2450_.buffer_pos_, 0); + } + + MockUARTComponent mock_uart_; + TestableLD2450 ld2450_; +}; + +// --- Good data tests --- + +TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) { + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // After a complete valid frame, buffer should be reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) { + auto frame = make_ack_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) { + auto frame = make_periodic_frame(); + for (int i = 0; i < 5; i++) { + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed"; + } +} + +TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { + auto periodic = make_periodic_frame(); + auto ack = make_ack_frame(); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(ack); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Garbage rejection tests --- + +TEST_F(LD2450ReadlineTest, GarbageDiscarded) { + // Feed bytes that don't match any header start byte + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; + this->ld2450_.feed(garbage); + // Header sync should discard all of these + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; + this->ld2450_.feed(garbage); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Header synchronization tests --- + +TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { + // Start of a data frame header, then invalid byte + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) + // Parser should have reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { + // Start of a command frame header, then invalid byte + this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { + // Partial header that fails, then a complete valid frame + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { + // Start data header, mismatch at byte 2, but mismatch byte is start of command header + this->ld2450_.feed({0xAA, 0xFF}); + EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header + + this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] + // Parser should reset and start new frame with 0xFD + EXPECT_EQ(this->ld2450_.buffer_pos_, 1); + EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); +} + +// --- Mid-frame / overflow recovery tests --- + +TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { + // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) + // These bytes would be part of target data in a real frame + std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; + this->ld2450_.feed(mid_frame); + // All discarded (none match header start bytes) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now feed a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, OverflowRecovery) { + // Feed a valid data frame header followed by enough filler to cause overflow. + // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x11); // Filler that won't match any footer + } + // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Feed a valid frame and verify recovery + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { + // Simulate the bug scenario: repeated overflows should not prevent recovery. + // Feed 3 rounds of overflow-inducing data. + for (int round = 0; round < 3; round++) { + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x22); + } + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; + } + + // Parser should still recover and process a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { + // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) + // followed by normal periodic data. + // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage + std::vector restart_noise = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data + 0x55, 0xCC, // stale footer bytes + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + }; + + this->ld2450_.feed(restart_noise); + // All garbage should be discarded + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now the LD2450 starts sending valid frames + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +} // namespace esphome::ld2450::testing From a2f0607c1e860d1173c80e0a763fda99e71e69f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:04:38 -0500 Subject: [PATCH 07/89] [ld2410] Add frame header synchronization to readline_() (#14136) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 95a04f768a..a3c2193d67 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,15 +601,33 @@ void LD2410Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from getting stuck in an overflow loop after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { From 5af871acce9c33284d483620131056af2ad48071 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:36:12 -0500 Subject: [PATCH 08/89] [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 02250c5911..358793fe64 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -21,7 +21,9 @@ namespace esphome::ld2420 { static constexpr uint8_t CALIBRATE_SAMPLES = 64; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands +// inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { From efe54e3b5e3d3ddefee703048ff37bb4ec9b0b5f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:25:25 -0500 Subject: [PATCH 09/89] [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 19 +-- esphome/components/ld2410/ld2410.h | 6 +- esphome/components/ld2450/ld2450.cpp | 19 +-- esphome/components/ld2450/ld2450.h | 8 +- tests/components/ld2450/ld2450_readline.cpp | 158 ++++++++------------ 5 files changed, 72 insertions(+), 138 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index a3c2193d67..f8f782f804 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,28 +601,11 @@ void LD2410Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from getting stuck in an overflow loop after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index efe585fb76..687ed21d1d 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -33,8 +33,10 @@ namespace esphome::ld2410 { using namespace ld24xx; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 2af45235a3..d30c164769 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,28 +769,11 @@ void LD2450Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from accumulating mid-frame data after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44e5912b2a..30f96c0a9c 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,9 +38,11 @@ using namespace ld24xx; // Constants static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 45; +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp index 68b1dd6881..cb97f633bf 100644 --- a/tests/components/ld2450/ld2450_readline.cpp +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -48,19 +48,39 @@ TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Garbage rejection tests --- - -TEST_F(LD2450ReadlineTest, GarbageDiscarded) { - // Feed bytes that don't match any header start byte - std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; - this->ld2450_.feed(garbage); - // Header sync should discard all of these - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} +// --- Garbage then valid frame tests --- TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + // Garbage bytes accumulate in the buffer but don't match any footer. + // A valid frame follows; its footer resets the buffer and resyncs. std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; this->ld2450_.feed(garbage); + EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // Footer from the valid frame resyncs the parser + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Footer-based resynchronization tests --- + +TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) { + // Garbage containing a periodic frame footer (0x55 0xCC) triggers + // a buffer reset, allowing the next frame to be parsed cleanly. + std::vector garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC}; + this->ld2450_.feed(garbage_with_footer); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) { + // Garbage containing a command frame footer (04 03 02 01) also resyncs. + std::vector garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01}; + this->ld2450_.feed(garbage_with_footer); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); auto frame = make_periodic_frame(); @@ -68,112 +88,56 @@ TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Header synchronization tests --- +// --- Overflow recovery tests --- -TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { - // Start of a data frame header, then invalid byte - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) - // Parser should have reset - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { - // Start of a command frame header, then invalid byte - this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { - // Partial header that fails, then a complete valid frame - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { - // Start data header, mismatch at byte 2, but mismatch byte is start of command header - this->ld2450_.feed({0xAA, 0xFF}); - EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header - - this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] - // Parser should reset and start new frame with 0xFD - EXPECT_EQ(this->ld2450_.buffer_pos_, 1); - EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); -} - -// --- Mid-frame / overflow recovery tests --- - -TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { - // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) - // These bytes would be part of target data in a real frame - std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; - this->ld2450_.feed(mid_frame); - // All discarded (none match header start bytes) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now feed a valid frame - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, OverflowRecovery) { - // Feed a valid data frame header followed by enough filler to cause overflow. - // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x11); // Filler that won't match any footer - } - // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 +TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) { + // Fill the buffer to capacity with filler that won't match any footer. + // MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); + this->ld2450_.feed(overflow_data); + // After overflow, buffer_pos_ resets to 0 (via the < 4 early return path) + EXPECT_LT(this->ld2450_.buffer_pos_, 4); +} + +TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) { + // Overflow, then a valid frame should be processed. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - // Feed a valid frame and verify recovery auto frame = make_periodic_frame(); this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { - // Simulate the bug scenario: repeated overflows should not prevent recovery. - // Feed 3 rounds of overflow-inducing data. - for (int round = 0; round < 3; round++) { - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x22); - } - this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; - } - - // Parser should still recover and process a valid frame +TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) { + // The key fix: the buffer (45) is large enough that a desynced periodic frame's + // footer (at most 30 bytes into the stream) will land inside the buffer before overflow. + // Simulate starting 10 bytes into a periodic frame, then a full frame follows. + std::vector mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; + // Then a complete periodic frame whose footer will land at position 40 (10 + 30), + // well within the buffer size of 45. auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); + mid_frame.insert(mid_frame.end(), frame.begin(), frame.end()); + + this->ld2450_.feed(mid_frame); + // The footer from the frame should have triggered a reset EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { - // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) - // followed by normal periodic data. - // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage +TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) { + // Simulate LD2450 restart: burst of garbage followed by valid periodic frames. + // The garbage + first frame should fit in the buffer so the footer resyncs. std::vector restart_noise = { - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data - 0x55, 0xCC, // stale footer bytes - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data }; + auto frame = make_periodic_frame(); + // 8 garbage + 30 frame = 38 bytes, well within buffer of 45 + restart_noise.insert(restart_noise.end(), frame.begin(), frame.end()); this->ld2450_.feed(restart_noise); - // All garbage should be discarded - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now the LD2450 starts sending valid frames - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + // Subsequent frames should work normally this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } From efe8a6c8ebfd3e831b9757356db7f08fc331472e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 17 Feb 2026 18:45:21 +0100 Subject: [PATCH 10/89] [esp32_ble_server] fix infinitely large characteristic value (#14011) --- .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 28 +++++++++++++++---- .../esp32_ble_server/ble_characteristic.h | 1 - 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522fac..cb494ed1bc 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0..a1b1ff94bb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789..c2cdb1660c 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -77,7 +77,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; From 8c0cc3a2d8bfc978865e940b03e30262f9331ead Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:00 -0600 Subject: [PATCH 11/89] [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) --- esphome/components/udp/__init__.py | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index bfaa5f2516..c9586d0b95 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -14,6 +14,7 @@ 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 MockObj +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -65,33 +66,47 @@ RELOCATED = { ) } -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.Any( - cv.port, - cv.Schema( + +def _consume_udp_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for UDP component.""" + from esphome.components import socket + + # UDP uses up to 2 sockets: 1 broadcast + 1 listen + # Whether each is used depends on code generation, so register worst case + socket.consume_sockets(2, "udp")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( { - cv.Required(CONF_LISTEN_PORT): cv.port, - cv.Required(CONF_BROADCAST_PORT): cv.port, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), } ), - ), - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(trigger_args) - ), - } - ), - } -).extend(RELOCATED) + } + ).extend(RELOCATED), + _consume_udp_sockets, +) async def register_udp_client(var, config): From e4aa23abaac53320b89d99ecd39efe9e60cbe7bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:37 -0600 Subject: [PATCH 12/89] [web_server] Double socket allocation to prevent connection exhaustion (#14067) --- esphome/components/web_server/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8b02a6baee..294a5e0a15 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 2 concurrent client connections - # (browser makes 2 connections for page + event stream) - sockets_needed = 3 + # Web server needs 1 listening socket + typically 5 concurrent client connections + # (browser opens connections for page resources, SSE event stream, and POST + # requests for entity control which may linger before closing) + sockets_needed = 6 socket.consume_sockets(sockets_needed, "web_server")(config) return config From 887172d663c5f7bd1ffc4a4ac35f99eecccf2a6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:08:53 -0600 Subject: [PATCH 13/89] [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 8ac5a28d8f..ef4cc980f6 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 +#include #include #endif @@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - 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; + 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), }; From cb8b14e64b6c4fbd01f96e75106f0b2ed452ffcc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:24 -0600 Subject: [PATCH 14/89] [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) --- esphome/components/web_server/web_server.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index dfd602be6b..c894d32a4b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1913,6 +1913,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); this->add_sorting_info_(root, obj); } @@ -1935,10 +1938,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe root[ESPHOME_F("target_temperature")] = target; } - root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); - root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); - root[ESPHOME_F("step")] = traits.get_target_temperature_step(); - if (traits.get_supports_away_mode()) { root[ESPHOME_F("away")] = obj->is_away(); } From 2491b4f85c1825b7dd8bc4273eafca643cf8c120 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:37 -0600 Subject: [PATCH 15/89] [ld2420] Use constexpr for compile-time constants (#14079) --- esphome/components/ld2420/ld2420.cpp | 118 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 6 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 69b69f4a61..cf78a1a460 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,73 +63,73 @@ namespace esphome::ld2420 { static const char *const TAG = "ld2420"; // Local const's -static const uint16_t REFRESH_RATE_MS = 1000; +static constexpr uint16_t REFRESH_RATE_MS = 1000; // Command sets -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; +static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE; +static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF; +static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002; +static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008; +static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020; +static constexpr uint16_t CMD_READ_REGISTER = 0x0002; +static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013; +static constexpr uint16_t CMD_READ_VERSION = 0x0000; +static constexpr uint16_t CMD_RESTART = 0x0068; +static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001; +static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012; -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static constexpr uint8_t CMD_MAX_BYTES = 0x64; +static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static constexpr uint8_t LD2420_ERROR_NONE = 0x00; +static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01; // Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; +static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000; +static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001; +static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004; +static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static constexpr uint16_t FACTORY_TIMEOUT = 120; +static constexpr uint16_t FACTORY_MIN_GATE = 1; +static constexpr uint16_t FACTORY_MAX_GATE = 12; // COMMAND_BYTE Header & Footer -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const int CALIBRATE_VERSION_MIN = 154; -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304; +static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static constexpr int CALIBRATE_VERSION_MIN = 154; +static constexpr uint8_t CMD_FRAME_COMMAND = 6; +static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4; +static constexpr uint8_t CMD_FRAME_STATUS = 7; +static constexpr uint8_t CMD_ERROR_WORD = 8; +static constexpr uint8_t ENERGY_SENSOR_START = 9; +static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4; static const char *const OP_NORMAL_MODE_STRING = "Normal"; static const char *const OP_SIMPLE_MODE_STRING = "Simple"; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 6d81f86497..02250c5911 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,9 +20,9 @@ namespace esphome::ld2420 { -static const uint8_t CALIBRATE_SAMPLES = 64; -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 16; +static constexpr uint8_t CALIBRATE_SAMPLES = 64; +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, From 25b14f995309a7dfbb4857d53f8c9a67aa8a7308 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:27:05 -0600 Subject: [PATCH 16/89] [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) --- esphome/components/e131/e131.cpp | 31 ++++++++++++++++++++++++- esphome/components/e131/e131.h | 8 +++++++ esphome/components/e131/e131_packet.cpp | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 4187857901..941927122c 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -14,12 +14,17 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_) { this->socket_->close(); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + this->udp_.stop(); +#endif } void E131Component::setup() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); int enable = 1; @@ -50,6 +55,13 @@ void E131Component::setup() { this->mark_failed(); return; } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.begin(PORT)) { + ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT); + this->mark_failed(); + return; + } +#endif join_igmp_groups_(); } @@ -59,19 +71,36 @@ void E131Component::loop() { int universe = 0; uint8_t buf[1460]; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) ssize_t len = this->socket_->read(buf, sizeof(buf)); if (len == -1) { return; } if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); return; } if (!this->process_(universe, packet)) { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + while (auto packet_size = this->udp_.parsePacket()) { + auto len = this->udp_.read(buf, sizeof(buf)); + if (len <= 0) + continue; + + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); + continue; + } + + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d4b272eae2..72da9ddebe 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,7 +1,11 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include +#endif #include "esphome/core/component.h" #include @@ -45,7 +49,11 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr socket_; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + WiFiUDP udp_; +#endif std::vector light_effects_; std::map universe_consumers_; }; diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index ed081e5758..aa5c740454 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; +#endif for (auto universe : universe_consumers_) { if (!universe.second) From 2d2178c90a7d8357a1b51cd51c02b6f800d70559 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:00:34 -0500 Subject: [PATCH 17/89] [socket] Fix IPv6 compilation error on host platform (#14101) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/socket.cpp | 10 ++++++++-- tests/components/socket/common.yaml | 11 +++++++++++ tests/components/socket/test-ipv6.esp32-idf.yaml | 4 ++++ tests/components/socket/test-ipv6.host.yaml | 4 ++++ tests/components/socket/test.esp32-idf.yaml | 1 + tests/components/socket/test.host.yaml | 3 +++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/components/socket/common.yaml create mode 100644 tests/components/socket/test-ipv6.esp32-idf.yaml create mode 100644 tests/components/socket/test-ipv6.host.yaml create mode 100644 tests/components/socket/test.esp32-idf.yaml create mode 100644 tests/components/socket/test.host.yaml diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 2fcc162ead..6154c497e0 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s #if USE_NETWORK_IPV6 else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) { const auto *addr = reinterpret_cast(addr_ptr); -#ifndef USE_SOCKET_IMPL_LWIP_TCP - // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) +#ifdef USE_HOST + // Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union) + if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) && + esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#elif !defined(USE_SOCKET_IMPL_LWIP_TCP) + // Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout) if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { diff --git a/tests/components/socket/common.yaml b/tests/components/socket/common.yaml new file mode 100644 index 0000000000..aaf49f1611 --- /dev/null +++ b/tests/components/socket/common.yaml @@ -0,0 +1,11 @@ +substitutions: + network_enable_ipv6: "false" + +socket: + +wifi: + ssid: MySSID + password: password1 + +network: + enable_ipv6: ${network_enable_ipv6} diff --git a/tests/components/socket/test-ipv6.esp32-idf.yaml b/tests/components/socket/test-ipv6.esp32-idf.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/socket/test-ipv6.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/socket/test-ipv6.host.yaml b/tests/components/socket/test-ipv6.host.yaml new file mode 100644 index 0000000000..fdd52c574e --- /dev/null +++ b/tests/components/socket/test-ipv6.host.yaml @@ -0,0 +1,4 @@ +socket: + +network: + enable_ipv6: true diff --git a/tests/components/socket/test.esp32-idf.yaml b/tests/components/socket/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/socket/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.host.yaml b/tests/components/socket/test.host.yaml new file mode 100644 index 0000000000..e0c5d7cea3 --- /dev/null +++ b/tests/components/socket/test.host.yaml @@ -0,0 +1,3 @@ +socket: + +network: From a343ff19895c0699671766f7ee8a0b0ec81bd321 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:00:05 -0500 Subject: [PATCH 18/89] [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 52f5f44d41..935d2004d4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -218,12 +218,19 @@ def _validate(config): ) elif config[CONF_TYPE] != "OPENETH": if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " - "Please update your configuration to use 'clk' instead." + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, ) - mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) del config[CONF_CLK_MODE] elif CONF_CLK not in config: raise cv.Invalid("'clk' is a required option for [ethernet].") From ac76fc44098f2a7adb74b212093c9353ff0c8912 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:25:26 -0500 Subject: [PATCH 19/89] [pulse_counter] Fix build failure when use_pcnt is false (#14111) Co-authored-by: Claude Opus 4.6 --- .../components/pulse_counter/pulse_counter_sensor.h | 4 ++-- .../pulse_counter/test-no-pcnt.esp32-idf.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index a7913d5d66..7a68858099 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -8,10 +8,10 @@ #if defined(USE_ESP32) #include -#ifdef SOC_PCNT_SUPPORTED +#if defined(SOC_PCNT_SUPPORTED) && __has_include() #include #define HAS_PCNT -#endif // SOC_PCNT_SUPPORTED +#endif // defined(SOC_PCNT_SUPPORTED) && __has_include() #endif // USE_ESP32 namespace esphome { diff --git a/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml new file mode 100644 index 0000000000..cd15cc781d --- /dev/null +++ b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: pulse_counter + name: Pulse Counter + pin: 4 + use_pcnt: false + count_mode: + rising_edge: INCREMENT + falling_edge: DECREMENT + internal_filter: 13us + update_interval: 15s From d78496321e2a65208fe83d7ea93d7107914419b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 11:41:00 -0600 Subject: [PATCH 20/89] [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_ble/__init__.py | 10 ++++++++++ tests/components/esp32_ble/test.esp32-c2-idf.yaml | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 tests/components/esp32_ble/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index dcc3ce71cf..d2020ada22 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -387,6 +388,15 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM, + # esp_bt_controller_init fails with ESP_ERR_NO_MEM. + # CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share + # space more flexibly, giving the BT controller enough contiguous memory. + # This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled. + if get_esp32_variant() == VARIANT_ESP32C2: + add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True) + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False) + # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() diff --git a/tests/components/esp32_ble/test.esp32-c2-idf.yaml b/tests/components/esp32_ble/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..f8defaf28f --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false From 0fc09462ff73e82b380389c3ae1f885947fc70f5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:45:04 -0500 Subject: [PATCH 21/89] [safe_mode] Log brownout as reset reason on OTA rollback (#14113) Co-authored-by: Claude Opus 4.6 --- esphome/components/safe_mode/safe_mode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f32511531a..6cae4bf9d5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -11,6 +11,7 @@ #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include +#include #endif namespace esphome::safe_mode { @@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() { "OTA rollback detected! Rolled back from partition '%s'\n" "The device reset before the boot was marked successful", last_invalid->label); + if (esp_reset_reason() == ESP_RST_BROWNOUT) { + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" + "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + } } #endif } From f412ab4f8b954ca7f81125237df452832decaa56 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:14:48 -0500 Subject: [PATCH 22/89] [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e865de8663..afceec6c54 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,4 +1,5 @@ import logging +import math from esphome import automation from esphome.automation import Condition @@ -493,6 +494,13 @@ async def to_code(config): cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + if CORE.is_esp32: + # Set PHY max TX power to match output_power so calibration also uses + # reduced power. This prevents brownout during PHY init on marginal + # power supplies, which is critical for OTA updates with rollback enabled. + # Kconfig range is 10-20, ESPHome allows 8.5-20.5 + phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER]))) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power) # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) From 7bdeb32a8a1ca43e07b64e80daa0afa13bc68206 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 17:48:18 -0600 Subject: [PATCH 23/89] [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6c242220a6..ea7a09fee6 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -19,6 +19,13 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual state from the boot console that requires +/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) { // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks // UART on default UART0 pins that may have residual state from boot console. // Reset these pins before configuring UART to ensure they're in a clean state. - if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(tx)) { gpio_reset_pin(static_cast(tx)); } - if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(rx)) { gpio_reset_pin(static_cast(rx)); } - // Setup pins after reset to preserve open drain/pullup/pulldown flags + // Setup pins after reset to configure GPIO direction and pull resistors. + // For UART0 default pins, setup() must always be called because gpio_reset_pin() + // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), + // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for + // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). + // For other pins, only call setup() if pull or open-drain flags are set to avoid + // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; From e7e1acc0a2fb1e1c3c5ce7fc448cca2050bb2d17 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:12:37 -0500 Subject: [PATCH 24/89] [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132) Co-authored-by: Claude Opus 4.6 --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ef4cc980f6..5e62c0a410 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -117,7 +117,7 @@ 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 max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From d19c1b689af4dbbe85e24f02580e586f6025ea73 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:20 -0500 Subject: [PATCH 25/89] [ld2450] Add frame header synchronization to fix initialization regression (#14135) Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ld2450/ld2450.cpp | 22 ++- esphome/components/ld2450/ld2450.h | 1 + tests/components/ld2450/common.h | 61 +++++++ tests/components/ld2450/ld2450_readline.cpp | 181 ++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/components/ld2450/common.h create mode 100644 tests/components/ld2450/ld2450_readline.cpp diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 1ea5c18271..2af45235a3 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,15 +769,33 @@ void LD2450Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from accumulating mid-frame data after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index fe69cd81d0..44e5912b2a 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h new file mode 100644 index 0000000000..d5ffbe1295 --- /dev/null +++ b/tests/components/ld2450/common.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/ld2450/ld2450.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::ld2450::testing { + +// Mock UART component to satisfy UARTDevice parent requirement. +class MockUARTComponent : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(size_t, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +// Expose protected members for testing. +class TestableLD2450 : public LD2450Component { + public: + using LD2450Component::buffer_data_; + using LD2450Component::buffer_pos_; + using LD2450Component::readline_; + + void feed(const std::vector &data) { + for (uint8_t byte : data) { + this->readline_(byte); + } + } +}; + +// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes +// All-zero targets means no presence detected. +inline std::vector make_periodic_frame(uint8_t fill = 0x00) { + std::vector frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER + for (int i = 0; i < 24; i++) { + frame.push_back(fill); // 3 targets * 8 bytes + } + frame.push_back(0x55); // DATA_FRAME_FOOTER + frame.push_back(0xCC); + return frame; +} + +// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful. +// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes +inline std::vector make_ack_frame() { + return { + 0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER + 0x04, 0x00, // length = 4 + 0xFF, 0x01, // command = enable_conf, status = success + 0x00, 0x00, // result = ok + 0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER + }; +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp new file mode 100644 index 0000000000..68b1dd6881 --- /dev/null +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +namespace esphome::ld2450::testing { + +class LD2450ReadlineTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->mock_uart_); + // Ensure clean state + ASSERT_EQ(this->ld2450_.buffer_pos_, 0); + } + + MockUARTComponent mock_uart_; + TestableLD2450 ld2450_; +}; + +// --- Good data tests --- + +TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) { + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // After a complete valid frame, buffer should be reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) { + auto frame = make_ack_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) { + auto frame = make_periodic_frame(); + for (int i = 0; i < 5; i++) { + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed"; + } +} + +TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { + auto periodic = make_periodic_frame(); + auto ack = make_ack_frame(); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(ack); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Garbage rejection tests --- + +TEST_F(LD2450ReadlineTest, GarbageDiscarded) { + // Feed bytes that don't match any header start byte + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; + this->ld2450_.feed(garbage); + // Header sync should discard all of these + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; + this->ld2450_.feed(garbage); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Header synchronization tests --- + +TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { + // Start of a data frame header, then invalid byte + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) + // Parser should have reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { + // Start of a command frame header, then invalid byte + this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { + // Partial header that fails, then a complete valid frame + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { + // Start data header, mismatch at byte 2, but mismatch byte is start of command header + this->ld2450_.feed({0xAA, 0xFF}); + EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header + + this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] + // Parser should reset and start new frame with 0xFD + EXPECT_EQ(this->ld2450_.buffer_pos_, 1); + EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); +} + +// --- Mid-frame / overflow recovery tests --- + +TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { + // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) + // These bytes would be part of target data in a real frame + std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; + this->ld2450_.feed(mid_frame); + // All discarded (none match header start bytes) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now feed a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, OverflowRecovery) { + // Feed a valid data frame header followed by enough filler to cause overflow. + // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x11); // Filler that won't match any footer + } + // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Feed a valid frame and verify recovery + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { + // Simulate the bug scenario: repeated overflows should not prevent recovery. + // Feed 3 rounds of overflow-inducing data. + for (int round = 0; round < 3; round++) { + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x22); + } + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; + } + + // Parser should still recover and process a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { + // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) + // followed by normal periodic data. + // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage + std::vector restart_noise = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data + 0x55, 0xCC, // stale footer bytes + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + }; + + this->ld2450_.feed(restart_noise); + // All garbage should be discarded + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now the LD2450 starts sending valid frames + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +} // namespace esphome::ld2450::testing From 49afe53a2cfae55feb304695ee1735a5f0e5bc43 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:04:38 -0500 Subject: [PATCH 26/89] [ld2410] Add frame header synchronization to readline_() (#14136) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 95a04f768a..a3c2193d67 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,15 +601,33 @@ void LD2410Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from getting stuck in an overflow loop after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { From 4c8e0575f94fb660bba85403347b999a424abe92 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:36:12 -0500 Subject: [PATCH 27/89] [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 02250c5911..358793fe64 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -21,7 +21,9 @@ namespace esphome::ld2420 { static constexpr uint8_t CALIBRATE_SAMPLES = 64; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands +// inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { From 28d510191c150ac3163bfdc80f23738c2cddb908 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:25:25 -0500 Subject: [PATCH 28/89] [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 19 +-- esphome/components/ld2410/ld2410.h | 6 +- esphome/components/ld2450/ld2450.cpp | 19 +-- esphome/components/ld2450/ld2450.h | 8 +- tests/components/ld2450/ld2450_readline.cpp | 158 ++++++++------------ 5 files changed, 72 insertions(+), 138 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index a3c2193d67..f8f782f804 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,28 +601,11 @@ void LD2410Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from getting stuck in an overflow loop after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index efe585fb76..687ed21d1d 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -33,8 +33,10 @@ namespace esphome::ld2410 { using namespace ld24xx; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 2af45235a3..d30c164769 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,28 +769,11 @@ void LD2450Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from accumulating mid-frame data after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44e5912b2a..30f96c0a9c 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,9 +38,11 @@ using namespace ld24xx; // Constants static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 45; +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp index 68b1dd6881..cb97f633bf 100644 --- a/tests/components/ld2450/ld2450_readline.cpp +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -48,19 +48,39 @@ TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Garbage rejection tests --- - -TEST_F(LD2450ReadlineTest, GarbageDiscarded) { - // Feed bytes that don't match any header start byte - std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; - this->ld2450_.feed(garbage); - // Header sync should discard all of these - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} +// --- Garbage then valid frame tests --- TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + // Garbage bytes accumulate in the buffer but don't match any footer. + // A valid frame follows; its footer resets the buffer and resyncs. std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; this->ld2450_.feed(garbage); + EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // Footer from the valid frame resyncs the parser + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Footer-based resynchronization tests --- + +TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) { + // Garbage containing a periodic frame footer (0x55 0xCC) triggers + // a buffer reset, allowing the next frame to be parsed cleanly. + std::vector garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC}; + this->ld2450_.feed(garbage_with_footer); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) { + // Garbage containing a command frame footer (04 03 02 01) also resyncs. + std::vector garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01}; + this->ld2450_.feed(garbage_with_footer); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); auto frame = make_periodic_frame(); @@ -68,112 +88,56 @@ TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Header synchronization tests --- +// --- Overflow recovery tests --- -TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { - // Start of a data frame header, then invalid byte - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) - // Parser should have reset - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { - // Start of a command frame header, then invalid byte - this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { - // Partial header that fails, then a complete valid frame - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { - // Start data header, mismatch at byte 2, but mismatch byte is start of command header - this->ld2450_.feed({0xAA, 0xFF}); - EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header - - this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] - // Parser should reset and start new frame with 0xFD - EXPECT_EQ(this->ld2450_.buffer_pos_, 1); - EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); -} - -// --- Mid-frame / overflow recovery tests --- - -TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { - // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) - // These bytes would be part of target data in a real frame - std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; - this->ld2450_.feed(mid_frame); - // All discarded (none match header start bytes) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now feed a valid frame - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, OverflowRecovery) { - // Feed a valid data frame header followed by enough filler to cause overflow. - // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x11); // Filler that won't match any footer - } - // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 +TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) { + // Fill the buffer to capacity with filler that won't match any footer. + // MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); + this->ld2450_.feed(overflow_data); + // After overflow, buffer_pos_ resets to 0 (via the < 4 early return path) + EXPECT_LT(this->ld2450_.buffer_pos_, 4); +} + +TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) { + // Overflow, then a valid frame should be processed. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - // Feed a valid frame and verify recovery auto frame = make_periodic_frame(); this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { - // Simulate the bug scenario: repeated overflows should not prevent recovery. - // Feed 3 rounds of overflow-inducing data. - for (int round = 0; round < 3; round++) { - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x22); - } - this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; - } - - // Parser should still recover and process a valid frame +TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) { + // The key fix: the buffer (45) is large enough that a desynced periodic frame's + // footer (at most 30 bytes into the stream) will land inside the buffer before overflow. + // Simulate starting 10 bytes into a periodic frame, then a full frame follows. + std::vector mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; + // Then a complete periodic frame whose footer will land at position 40 (10 + 30), + // well within the buffer size of 45. auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); + mid_frame.insert(mid_frame.end(), frame.begin(), frame.end()); + + this->ld2450_.feed(mid_frame); + // The footer from the frame should have triggered a reset EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { - // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) - // followed by normal periodic data. - // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage +TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) { + // Simulate LD2450 restart: burst of garbage followed by valid periodic frames. + // The garbage + first frame should fit in the buffer so the footer resyncs. std::vector restart_noise = { - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data - 0x55, 0xCC, // stale footer bytes - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data }; + auto frame = make_periodic_frame(); + // 8 garbage + 30 frame = 38 bytes, well within buffer of 45 + restart_noise.insert(restart_noise.end(), frame.begin(), frame.end()); this->ld2450_.feed(restart_noise); - // All garbage should be discarded - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now the LD2450 starts sending valid frames - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + // Subsequent frames should work normally this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } From 8aaf0b8d8546471d5733b7e565a29f2be2d4582e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:17:12 -0500 Subject: [PATCH 29/89] Bump version to 2026.2.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 38135f9106..d41a79b0dc 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 +PROJECT_NUMBER = 2026.2.1 # 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 9115055e7b..b3c15b1e27 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" +__version__ = "2026.2.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b0a35559b3aa905a46b662b5834689a45b840e33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 10:19:01 -0600 Subject: [PATCH 30/89] [esp32] Bump ESP-IDF to 5.5.3.1, revert GATTS workaround (#14147) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 25 +++++++++++++++--------- esphome/components/esp32_ble/__init__.py | 15 ++++++-------- platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index df584fa716..777c846371 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 +5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cdde1c4ed5..b6682100f7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -587,16 +587,22 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: 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: +def _format_framework_espidf_version( + ver: cv.Version, release: str | None = None +) -> str: # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): ext = "tar.xz" else: ext = "zip" + # Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1") + ver_str = f"{ver.major}.{ver.minor}.{ver.patch}" + if ver.extra: + ver_str += f".{ver.extra}" if release: - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}" def _is_framework_url(source: str) -> bool: @@ -643,7 +649,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, 3), + cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), 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), @@ -662,11 +668,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 3), - "latest": cv.Version(5, 5, 3), - "dev": cv.Version(5, 5, 3), + "recommended": cv.Version(5, 5, 3, "1"), + "latest": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 3, "1"), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), @@ -730,7 +737,7 @@ def _check_versions(config): platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) value[CONF_SOURCE] = value.get( CONF_SOURCE, - _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + _format_framework_espidf_version(version, value.get(CONF_RELEASE)), ) if _is_framework_url(value[CONF_SOURCE]): value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" @@ -1428,7 +1435,7 @@ async def to_code(config): if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: cg.add_platformio_option( "platform_packages", - [_format_framework_espidf_version(idf_ver, None)], + [_format_framework_espidf_version(idf_ver)], ) # Use stub package to skip downloading precompiled libs stubs_dir = CORE.relative_build_path("arduino_libs_stub") diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 80fcf051b9..c0e2f78bde 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -418,16 +418,13 @@ def final_validation(config): "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a - # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED - # guard, causing a compile error when both are disabled. - # Additionally, when GATT Client is enabled, GATT Server must also be enabled - # as an internal dependency in the Bluedroid stack. + # Check if BLE Server is needed + has_ble_server = "esp32_ble_server" in full_config + + # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled + # This is an internal dependency in the Bluedroid stack # See: https://github.com/espressif/esp-idf/issues/17724 - # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: - # has_ble_server = "esp32_ble_server" in full_config - # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index fdd6a36428..e35dce2228 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino 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.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf 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.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = espidf lib_deps = From 9ce01fc369c5451e1f980e49cea08e51b6a0cc53 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:05 -0500 Subject: [PATCH 31/89] [esp32] Add engineering_sample option for ESP32-P4 (#14139) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 43 +++++++++++++++++-- esphome/components/esp32/boards.py | 5 ++- script/generate-esp32-boards.py | 15 ++++++- tests/components/esp32/test.esp32-p4-idf.yaml | 1 + 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b6682100f7..06677006ea 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -87,6 +87,7 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" @@ -785,6 +786,15 @@ def _detect_variant(value): # variant has already been validated against the known set value = value.copy() value[CONF_BOARD] = STANDARD_BOARDS[variant] + if variant == VARIANT_ESP32P4: + engineering_sample = value.get(CONF_ENGINEERING_SAMPLE) + if engineering_sample is None: + _LOGGER.warning( + "No board specified for ESP32-P4. Defaulting to production silicon (rev3). " + "If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'." + ) + elif engineering_sample: + value[CONF_BOARD] = "esp32-p4-evboard" elif board in BOARDS: variant = variant or BOARDS[board][KEY_VARIANT] if variant != BOARDS[board][KEY_VARIANT]: @@ -848,6 +858,30 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if ( + config[CONF_VARIANT] != VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' is only supported on {VARIANT_ESP32P4}", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) + if ( + config[CONF_VARIANT] == VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + board_is_es = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + if config[CONF_ENGINEERING_SAMPLE] != board_is_es: + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' does not match board '{config[CONF_BOARD]}'", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( @@ -1197,6 +1231,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( *FULL_CPU_FREQUENCIES, upper=True ), + cv.Optional(CONF_ENGINEERING_SAMPLE): cv.boolean, cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), @@ -1482,10 +1517,12 @@ async def to_code(config): # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 # from y to n. PlatformIO uses sections.ld.in (for rev <3) or # sections.rev3.ld.in (for rev >=3) based on board definition. - # Set the sdkconfig option to match the board's revision. + # Set the sdkconfig option to match the board's chip revision. if variant == VARIANT_ESP32P4: - is_rev3 = "_r3" in config[CONF_BOARD] - add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + is_eng_sample = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", is_eng_sample) # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 66367d63ae..2bd08e7c39 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -20,7 +20,7 @@ STANDARD_BOARDS = { VARIANT_ESP32C6: "esp32-c6-devkitm-1", VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2", VARIANT_ESP32H2: "esp32-h2-devkitm-1", - VARIANT_ESP32P4: "esp32-p4-evboard", + VARIANT_ESP32P4: "esp32-p4_r3-evboard", VARIANT_ESP32S2: "esp32-s2-kaluga-1", VARIANT_ESP32S3: "esp32-s3-devkitc-1", } @@ -1713,10 +1713,12 @@ BOARDS = { "esp32-p4": { "name": "Espressif ESP32-P4 ES (pre rev.300) generic", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4-evboard": { "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4_r3": { "name": "Espressif ESP32-P4 rev.300 generic", @@ -2141,6 +2143,7 @@ BOARDS = { "m5stack-tab5-p4": { "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "m5stack-timer-cam": { "name": "M5Stack Timer CAM", diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 81b78b04be..ab4a38ced5 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -43,10 +43,14 @@ def get_boards(): name = board_info["name"] board = fname.stem variant = mcu.upper() - boards[board] = { + chip_variant = board_info["build"].get("chip_variant", "") + entry = { "name": name, "variant": f"VARIANT_{variant}", } + if chip_variant.endswith("_es"): + entry["engineering_sample"] = True + boards[board] = entry return boards @@ -55,6 +59,12 @@ TEMPLATE = """ "%s": { "variant": %s, },""" +TEMPLATE_ES = """ "%s": { + "name": "%s", + "variant": %s, + "engineering_sample": True, + },""" + def main(check: bool): boards = get_boards() @@ -66,7 +76,8 @@ def main(check: bool): if line == "BOARDS = {": parts.append(line) parts.extend( - TEMPLATE % (board, info["name"], info["variant"]) + (TEMPLATE_ES if info.get("engineering_sample") else TEMPLATE) + % (board, info["name"], info["variant"]) for board, info in sorted(boards.items()) ) parts.append("}") diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index bc054f5aee..fd42fac5a3 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -1,5 +1,6 @@ esp32: variant: esp32p4 + engineering_sample: true flash_size: 32MB cpu_frequency: 400MHz framework: From 403235e2d4ec6a27ac4ad35f79b7dc2afb413611 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:29 -0500 Subject: [PATCH 32/89] [wifi] Add band_mode configuration for ESP32-C5 dual-band WiFi (#14148) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 22 ++++++++++++++++++- esphome/components/wifi/wifi_component.cpp | 16 ++++++++++++++ esphome/components/wifi/wifi_component.h | 13 +++++++++++ .../wifi/wifi_component_esp_idf.cpp | 7 ++++++ tests/components/wifi/test.esp32-c5-idf.yaml | 5 +++++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/components/wifi/test.esp32-c5-idf.yaml diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index afceec6c54..540d0a0ab1 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,7 +5,12 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM -from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + const, + get_esp32_variant, + only_on_variant, +) from esphome.components.network import ( has_high_performance_networking, ip_address_literal, @@ -64,6 +69,7 @@ _LOGGER = logging.getLogger(__name__) NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" CONF_POST_CONNECT_ROAMING = "post_connect_roaming" @@ -90,6 +96,13 @@ WIFI_POWER_SAVE_MODES = { "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } +WiFiBandMode = cg.global_ns.enum("wifi_band_mode_t") +WIFI_BAND_MODES = { + "AUTO": WiFiBandMode.WIFI_BAND_MODE_AUTO, + "2.4GHZ": WiFiBandMode.WIFI_BAND_MODE_2G_ONLY, + "5GHZ": WiFiBandMode.WIFI_BAND_MODE_5G_ONLY, +} + WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") WIFI_MIN_AUTH_MODES = { "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, @@ -353,6 +366,11 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All( cv.boolean, cv.only_on_esp32 ), + cv.Optional(CONF_BAND_MODE): cv.All( + cv.enum(WIFI_BAND_MODES, upper=True), + cv.only_on_esp32, + only_on_variant(supported=[const.VARIANT_ESP32C5]), + ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean, @@ -527,6 +545,8 @@ async def to_code(config): cg.add(var.set_btm(config[CONF_ENABLE_BTM])) if config[CONF_ENABLE_RRM]: cg.add(var.set_rrm(config[CONF_ENABLE_RRM])) + if CONF_BAND_MODE in config: + cg.add(var.set_band_mode(config[CONF_BAND_MODE])) if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2efac8d26..8b3060c7c3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1469,6 +1469,22 @@ void WiFiComponent::dump_config() { ESP_LOGCONFIG(TAG, " Disabled"); return; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + const char *band_mode_s; + switch (this->band_mode_) { + case WIFI_BAND_MODE_2G_ONLY: + band_mode_s = "2.4GHz"; + break; + case WIFI_BAND_MODE_5G_ONLY: + band_mode_s = "5GHz"; + break; + case WIFI_BAND_MODE_AUTO: + default: + band_mode_s = "Auto"; + break; + } + ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s); +#endif if (this->is_connected()) { this->print_connect_params_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4a038f602c..5f903e092a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -45,6 +45,10 @@ extern "C" { #include #endif +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) +#include +#endif + #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) #include #include @@ -435,6 +439,9 @@ class WiFiComponent : public Component { void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; } +#endif void set_passive_scan(bool passive); @@ -652,6 +659,9 @@ class WiFiComponent : public Component { bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + bool wifi_apply_band_mode_(); +#endif bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); @@ -774,6 +784,9 @@ class WiFiComponent : public Component { // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; +#endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 52ee482121..57bbceb1b8 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -292,6 +292,10 @@ bool WiFiComponent::wifi_apply_power_save_() { return success; } +#ifdef SOC_WIFI_SUPPORT_5G +bool WiFiComponent::wifi_apply_band_mode_() { return esp_wifi_set_band_mode(this->band_mode_) == ESP_OK; } +#endif + bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // enable STA if (!this->wifi_mode_(true, {})) @@ -726,6 +730,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_started = true; // re-apply power save mode wifi_apply_power_save_(); +#ifdef SOC_WIFI_SUPPORT_5G + wifi_apply_band_mode_(); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); diff --git a/tests/components/wifi/test.esp32-c5-idf.yaml b/tests/components/wifi/test.esp32-c5-idf.yaml new file mode 100644 index 0000000000..92a52db09e --- /dev/null +++ b/tests/components/wifi/test.esp32-c5-idf.yaml @@ -0,0 +1,5 @@ +wifi: + band_mode: 5GHZ + +packages: + - !include common.yaml From 9c0eed8a67a392868d2a8e6c8a220314271c847e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:03:39 -0500 Subject: [PATCH 33/89] [e131] Remove dead LWIP TCP code path from loop() (#14155) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/e131/e131.cpp | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d04ab8a58d..a7a695c167 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -85,22 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - continue; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } - } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { From b85a49cdb3c65c24ffdb725a56dc9f48c2fd7f3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:27:15 -0600 Subject: [PATCH 34/89] Bump github/codeql-action from 4.32.3 to 4.32.4 (#14161) Signed-off-by: dependabot[bot] 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 376825bad6..5d7c32eaa9 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@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 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@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: category: "/language:${{matrix.language}}" From 1a376328911b184cbc5b7251688607f7ff62384a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:27:45 +0000 Subject: [PATCH 35/89] Bump pylint from 4.0.4 to 4.0.5 (#14160) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9e99855f6f..611e552829 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==4.0.4 +pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.15.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating From edfc3e3501ec3b0652a7a341d5542f70c488b79c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:32:41 +0000 Subject: [PATCH 36/89] Bump ruff from 0.15.1 to 0.15.2 (#14159) 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 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d89060b0d..07d02e0e3c 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.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 611e552829..3e5dc8a90c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.1 # also change in .pre-commit-config.yaml when updating +ruff==0.15.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From 48115eca18b8c9937595a0fe26ce50fde4ebb15f Mon Sep 17 00:00:00 2001 From: Pawelo <81100874+pgolawsk@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:08:31 +0100 Subject: [PATCH 37/89] [safe_mode] Extract RTC_KEY constant for shared use (#14121) Co-authored-by: J. Nick Koston --- esphome/components/safe_mode/safe_mode.cpp | 2 +- esphome/components/safe_mode/safe_mode.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 6cae4bf9d5..bd80048c64 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -104,7 +104,7 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en this->safe_mode_enable_time_ = enable_time; this->safe_mode_boot_is_good_after_ = boot_is_good_after; this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences->make_preference(233825507UL, false); + this->rtc_ = global_preferences->make_preference(RTC_KEY, false); #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Check partition state to detect if bootloader supports rollback diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index d6f669f39f..a4d27c15da 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -11,6 +11,9 @@ namespace esphome::safe_mode { +/// RTC key for storing boot loop counter - used by safe_mode and preferences backends +constexpr uint32_t RTC_KEY = 233825507UL; + /// SafeModeComponent provides a safe way to recover from repeated boot failures class SafeModeComponent : public Component { public: From db6aa58f40b4c6be322f8c26312605bb2fca8dd6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:06:46 -0500 Subject: [PATCH 38/89] [max7219digit] Fix typo in action names (#14162) Co-authored-by: Claude Opus 4.6 --- esphome/components/max7219digit/display.py | 22 +++++++++++----------- tests/components/max7219digit/common.yaml | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index e6d53efc5d..a251eaccea 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -133,12 +133,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_invert_to_code(config, action_id, template_arg, args): +async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -146,12 +146,12 @@ async def max7129digit_invert_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_visible_to_code(config, action_id, template_arg, args): +async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -159,12 +159,12 @@ async def max7129digit_visible_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_reverse_to_code(config, action_id, template_arg, args): +async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -183,9 +183,9 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA + "max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA ) -async def max7129digit_intensity_to_code(config, action_id, template_arg, args): +async def max7219digit_intensity_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) diff --git a/tests/components/max7219digit/common.yaml b/tests/components/max7219digit/common.yaml index 525b7b8d3e..4d2dbb781d 100644 --- a/tests/components/max7219digit/common.yaml +++ b/tests/components/max7219digit/common.yaml @@ -13,10 +13,10 @@ esphome: on_boot: - priority: 100 then: - - max7129digit.invert_off: - - max7129digit.invert_on: - - max7129digit.turn_on: - - max7129digit.turn_off: - - max7129digit.reverse_on: - - max7129digit.reverse_off: - - max7129digit.intensity: 10 + - max7219digit.invert_off: + - max7219digit.invert_on: + - max7219digit.turn_on: + - max7219digit.turn_off: + - max7219digit.reverse_on: + - max7219digit.reverse_off: + - max7219digit.intensity: 10 From 1d3054ef5e463f1b018e297ffe547c7e621ddfd4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 20 Feb 2026 23:12:50 +0100 Subject: [PATCH 39/89] [nrf52,logger] Early debug (#11685) --- esphome/components/debug/debug_zephyr.cpp | 45 +-------- esphome/components/logger/__init__.py | 24 +++++ esphome/components/logger/logger.cpp | 3 + esphome/components/logger/logger.h | 1 + esphome/components/logger/logger_zephyr.cpp | 97 +++++++++++++++++++ esphome/components/nrf52/__init__.py | 1 + esphome/components/zephyr/gpio.h | 6 +- esphome/components/zephyr/preferences.h | 6 +- esphome/components/zephyr/reset_reason.cpp | 63 ++++++++++++ esphome/components/zephyr/reset_reason.h | 18 ++++ esphome/config_validation.py | 2 + esphome/core/defines.h | 2 + .../logger/test.nrf52-adafruit.yaml | 2 + 13 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 esphome/components/zephyr/reset_reason.cpp create mode 100644 esphome/components/zephyr/reset_reason.h diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 0291cc3061..ecca7150bd 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include #include "esphome/core/log.h" +#include #include #include #include @@ -15,16 +16,6 @@ static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8; -static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { - if (!set) { - return pos; - } - if (pos > 0) { - pos = buf_append_printf(buf, size, pos, ", "); - } - return buf_append_printf(buf, size, pos, "%s", reason); -} - static inline uint32_t read_mem_u32(uintptr_t addr) { return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) } @@ -57,39 +48,7 @@ static inline uint32_t sd_version_get() { } const char *DebugComponent::get_reset_reason_(std::span buffer) { - char *buf = buffer.data(); - const size_t size = RESET_REASON_BUFFER_SIZE; - - uint32_t cause; - auto ret = hwinfo_get_reset_cause(&cause); - if (ret) { - ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); - buf[0] = '\0'; - return buf; - } - size_t pos = 0; - - pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); - pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); - pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); - - // Ensure null termination if nothing was written - if (pos == 0) { - buf[0] = '\0'; - } - + const char *buf = zephyr::get_reset_reason(buffer); ESP_LOGD(TAG, "Reset Reason: %s", buf); return buf; } diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index b2952d7995..c8f3c52911 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -101,6 +101,8 @@ CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" +CONF_WAIT_FOR_CDC = "wait_for_cdc" +CONF_EARLY_MESSAGE = "early_message" UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], @@ -208,6 +210,12 @@ def validate_initial_no_higher_than_global(config): return config +def validate_wait_for_cdc(config): + if config.get(CONF_WAIT_FOR_CDC) and config.get(CONF_HARDWARE_UART) != USB_CDC: + raise cv.Invalid("wait_for_cdc requires hardware_uart: USB_CDC") + return config + + Logger = logger_ns.class_("Logger", cg.Component) LoggerMessageTrigger = logger_ns.class_( "LoggerMessageTrigger", @@ -300,10 +308,18 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault( CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True ): cv.All(cv.only_on_esp8266, cv.boolean), + cv.SplitDefault(CONF_WAIT_FOR_CDC, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), + cv.boolean, + ), + cv.SplitDefault(CONF_EARLY_MESSAGE, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), cv.boolean + ), } ).extend(cv.COMPONENT_SCHEMA), validate_local_no_higher_than_global, validate_initial_no_higher_than_global, + validate_wait_for_cdc, ) @@ -425,13 +441,21 @@ async def to_code(config): except cv.Invalid: pass + if config.get(CONF_WAIT_FOR_CDC): + cg.add_define("USE_LOGGER_WAIT_FOR_CDC") + if config.get(CONF_EARLY_MESSAGE): + cg.add_define("USE_LOGGER_EARLY_MESSAGE") + if CORE.is_nrf52: + # esphome implement own fatal error handler which save PC/LR before reset + zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False) 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: zephyr_add_overlay("""&uart1 { status = "okay";};""") if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") zephyr_add_prj_conf("UART_LINE_CTRL", True) zephyr_add_cdc_acm(config, 0) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index e1b49bcb61..87963c5fc5 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -261,6 +261,9 @@ void Logger::dump_config() { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif +#ifdef USE_ZEPHYR + dump_crash_(); +#endif } void Logger::set_log_level(uint8_t level) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 2a7552af92..c6c379f6c6 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -317,6 +317,7 @@ class Logger : public Component { Stream *hw_serial_{nullptr}; #endif #if defined(USE_ZEPHYR) + void dump_crash_(); const device *uart_dev_{nullptr}; #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index f565c5760c..d6193ff36b 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -8,9 +8,30 @@ #include #include #include +#ifdef USE_LOGGER_EARLY_MESSAGE +#include +#endif + +namespace esphome::zephyr_coredump { + +__attribute__((weak)) void print_coredump() {} + +} // namespace esphome::zephyr_coredump namespace esphome::logger { +static const uint32_t CRASH_MAGIC = 0xDEADBEEF; + +__attribute__((section(".noinit"))) struct { + uint32_t magic; + uint32_t reason; + uint32_t pc; + uint32_t lr; +#if defined(CONFIG_THREAD_NAME) + char thread[CONFIG_THREAD_MAX_NAME_LEN]; +#endif +} crash_buf; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC @@ -57,10 +78,26 @@ void Logger::pre_setup() { ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); } else { this->uart_dev_ = uart_dev; +#if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) + uint32_t dtr = 0; + uint32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs + while (dtr == 0 && count-- != 0) { + uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); + delay(10); + arch_feed_wdt(); + } +#endif } } global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_LOGGER_EARLY_MESSAGE + char reason_buffer[zephyr::RESET_REASON_BUFFER_SIZE]; + const char *reset_reason = zephyr::get_reset_reason(std::span(reason_buffer)); + ESP_LOGI(TAG, "Reset reason: %s", reset_reason); + dump_crash_(); + zephyr_coredump::print_coredump(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { @@ -93,6 +130,66 @@ const LogString *Logger::get_uart_selection_() { } } +static const uint8_t REASON_BUF_SIZE = 32; + +static const char *reason_to_str(unsigned int reason, char *buf) { + switch (reason) { + case K_ERR_CPU_EXCEPTION: + return "CPU exception"; + case K_ERR_SPURIOUS_IRQ: + return "Unhandled interrupt"; + case K_ERR_STACK_CHK_FAIL: + return "Stack overflow"; + case K_ERR_KERNEL_OOPS: + return "Kernel oops"; + case K_ERR_KERNEL_PANIC: + return "Kernel panic"; + default: + snprintf(buf, REASON_BUF_SIZE, "Unknown error (%u)", reason); + return buf; + } +} + +void Logger::dump_crash_() { + ESP_LOGD(TAG, "Crash buffer address %p", &crash_buf); + if (crash_buf.magic == CRASH_MAGIC) { + char reason_buf[REASON_BUF_SIZE]; + ESP_LOGE(TAG, "Last crash:"); + ESP_LOGE(TAG, "Reason=%s PC=0x%08x LR=0x%08x", reason_to_str(crash_buf.reason, reason_buf), crash_buf.pc, + crash_buf.lr); +#if defined(CONFIG_THREAD_NAME) + ESP_LOGE(TAG, "Thread: %s", crash_buf.thread); +#endif + } +} + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + crash_buf.magic = CRASH_MAGIC; + crash_buf.reason = reason; + if (esf) { + crash_buf.pc = esf->basic.pc; + crash_buf.lr = esf->basic.lr; + } +#if defined(CONFIG_THREAD_NAME) + auto thread = k_current_get(); + const char *name = k_thread_name_get(thread); + if (name) { + strncpy(crash_buf.thread, name, sizeof(crash_buf.thread) - 1); + crash_buf.thread[sizeof(crash_buf.thread) - 1] = '\0'; + } else { + crash_buf.thread[0] = '\0'; + } +#endif + arch_restart(); +} + } // namespace esphome::logger +extern "C" { + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + esphome::logger::k_sys_fatal_error_handler(reason, esf); +} +} + #endif diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 7d3d59f0ad..95e3670124 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -266,6 +266,7 @@ async def to_code(config: ConfigType) -> None: }; """ ) + zephyr_add_prj_conf("REBOOT", True) @coroutine_with_priority(CoroPriority.DIAGNOSTICS) diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 94f25f02ac..907fbe9f9c 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -3,8 +3,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/hal.h" #include -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { class ZephyrGPIOPin : public InternalGPIOPin { public: @@ -39,7 +38,6 @@ class ZephyrGPIOPin : public InternalGPIOPin { bool value_{false}; }; -} // namespace zephyr -} // namespace esphome +} // namespace esphome::zephyr #endif // USE_ZEPHYR diff --git a/esphome/components/zephyr/preferences.h b/esphome/components/zephyr/preferences.h index 6a37e41b46..4bee96d79e 100644 --- a/esphome/components/zephyr/preferences.h +++ b/esphome/components/zephyr/preferences.h @@ -2,12 +2,10 @@ #ifdef USE_ZEPHYR -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { void setup_preferences(); -} // namespace zephyr -} // namespace esphome +} #endif diff --git a/esphome/components/zephyr/reset_reason.cpp b/esphome/components/zephyr/reset_reason.cpp new file mode 100644 index 0000000000..24b32196db --- /dev/null +++ b/esphome/components/zephyr/reset_reason.cpp @@ -0,0 +1,63 @@ +#include "reset_reason.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome::zephyr { + +static const char *const TAG = "zephyr"; + +static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { + if (!set) { + return pos; + } + if (pos > 0) { + pos = buf_append_printf(buf, size, pos, ", "); + } + return buf_append_printf(buf, size, pos, "%s", reason); +} + +const char *get_reset_reason(std::span buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + + uint32_t cause; + auto ret = hwinfo_get_reset_cause(&cause); + if (ret) { + ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); + buf[0] = '\0'; + return buf; + } + size_t pos = 0; + + if (cause == 0) { + pos = append_reset_reason(buf, size, pos, true, "None"); + } else { + pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); + pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); + pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); + } + + // Ensure null termination if nothing was written + if (pos == 0) { + buf[0] = '\0'; + } + return buf; +} +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/reset_reason.h b/esphome/components/zephyr/reset_reason.h new file mode 100644 index 0000000000..2c2e7b8470 --- /dev/null +++ b/esphome/components/zephyr/reset_reason.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) + +#include +#include + +namespace esphome::zephyr { + +static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; + +const char *get_reset_reason(std::span buffer); + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a9d1a72e5a..ef1c66a20e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -70,6 +70,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, SCHEDULER_DONT_RUN, TYPE_GIT, @@ -695,6 +696,7 @@ def only_with_framework( only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) +only_on_nrf52 = only_on(PLATFORM_NRF52) only_on_rp2040 = only_on(PLATFORM_RP2040) only_with_arduino = only_with_framework(Framework.ARDUINO) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a7fb9f197c..c82b222a3d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -334,6 +334,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER +#define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_WAIT_FOR_CDC #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 821a136250..062451188f 100644 --- a/tests/components/logger/test.nrf52-adafruit.yaml +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -5,4 +5,6 @@ esphome: logger: level: DEBUG + wait_for_cdc: true + early_message: true task_log_buffer_size: 0 From d206c75b0b815c1cdd5d4f0741f0b04f22918138 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:44 -0600 Subject: [PATCH 40/89] [logger] Fix loop disable optimization using wrong preprocessor guard (#14158) --- esphome/components/logger/logger.cpp | 13 ++++++------- esphome/components/logger/logger.h | 10 +++++----- esphome/components/logger/logger_zephyr.cpp | 2 +- esphome/core/defines.h | 4 ++++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 87963c5fc5..22a95e4835 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -170,19 +170,19 @@ void Logger::init_log_buffer(size_t total_buffer_size) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); -// 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) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Start with loop disabled when using task buffer // The loop will be enabled automatically when messages arrive + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() this->disable_loop_when_buffer_empty_(); #endif } #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { this->process_messages_(); -#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) this->cdc_loop_(); #endif } @@ -204,8 +204,7 @@ void Logger::process_messages_() { this->write_log_buffer_to_console_(buf); } } -// Zephyr needs loop working to check when CDC port is open -#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_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 c6c379f6c6..8bf1edebb8 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -147,7 +147,7 @@ class Logger : public Component { #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. @@ -229,7 +229,7 @@ class Logger : public Component { 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) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) void cdc_loop_(); #endif void process_messages_(); @@ -465,9 +465,9 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -// 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) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Disable loop when task buffer is empty + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // concurrently. If that happens between our check and disable_loop(), the enable request diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index d6193ff36b..c2d24d6efc 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -34,7 +34,7 @@ __attribute__((section(".noinit"))) struct { static const char *const TAG = "logger"; -#ifdef USE_LOGGER_USB_CDC +#ifdef USE_LOGGER_UART_SELECTION_USB_CDC void Logger::cdc_loop_() { if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c82b222a3d..5109dd36f4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -270,10 +270,12 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #define USE_LOGGER_USB_SERIAL_JTAG #endif #endif @@ -335,6 +337,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_UART_SELECTION_USB_CDC +#define USE_LOGGER_USB_CDC #define USE_LOGGER_WAIT_FOR_CDC #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 From 35037d1a5b7c5ad7f4dbabaaa60dab2e7d76af01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:58 -0600 Subject: [PATCH 41/89] [core] Deduplicate base64 encode/decode logic (#14143) Co-authored-by: Claude Opus 4.6 --- esphome/core/helpers.cpp | 88 ++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 09e755ca71..9f850b5df8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -545,38 +545,36 @@ static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } +// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. +static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { + char char_array_4[4]; + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (int j = 0; j < count; j++) + ret += BASE64_CHARS[static_cast(char_array_4[j])]; +} + std::string base64_encode(const uint8_t *buf, size_t buf_len) { std::string ret; int i = 0; - int j = 0; char char_array_3[3]; - char char_array_4[4]; while (buf_len--) { char_array_3[i++] = *(buf++); if (i == 3) { - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (i = 0; (i < 4); i++) - ret += BASE64_CHARS[static_cast(char_array_4[i])]; + base64_encode_triple(char_array_3, 4, ret); i = 0; } } if (i) { - for (j = i; j < 3; j++) + for (int j = i; j < 3; j++) char_array_3[j] = '\0'; - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (j = 0; (j < i + 1); j++) - ret += BASE64_CHARS[static_cast(char_array_4[j])]; + base64_encode_triple(char_array_3, i + 1, ret); while ((i++ < 3)) ret += '='; @@ -589,13 +587,33 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); } +// Decode 4 base64 characters to up to 'count' output bytes, returns true if truncated. +static inline bool base64_decode_quad(uint8_t *char_array_4, int count, uint8_t *buf, size_t buf_len, size_t &out) { + for (int i = 0; i < 4; i++) + char_array_4[i] = base64_find_char(char_array_4[i]); + + uint8_t char_array_3[3]; + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + bool truncated = false; + for (int j = 0; j < count; j++) { + if (out < buf_len) { + buf[out++] = char_array_3[j]; + } else { + truncated = true; + } + } + return truncated; +} + size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) { size_t in_len = encoded_len; int i = 0; - int j = 0; size_t in = 0; size_t out = 0; - uint8_t char_array_4[4], char_array_3[3]; + uint8_t char_array_4[4]; bool truncated = false; // SAFETY: The loop condition checks is_base64() before processing each character. @@ -605,42 +623,16 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b char_array_4[i++] = encoded_data[in]; in++; if (i == 4) { - for (i = 0; i < 4; i++) - char_array_4[i] = base64_find_char(char_array_4[i]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (i = 0; i < 3; i++) { - if (out < buf_len) { - buf[out++] = char_array_3[i]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, 3, buf, buf_len, out); i = 0; } } if (i) { - for (j = i; j < 4; j++) + for (int j = i; j < 4; j++) char_array_4[j] = 0; - for (j = 0; j < 4; j++) - char_array_4[j] = base64_find_char(char_array_4[j]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (j = 0; j < i - 1; j++) { - if (out < buf_len) { - buf[out++] = char_array_3[j]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, i - 1, buf, buf_len, out); } if (truncated) { From a3f279c1cf150f6908b84df9976e72821995e7b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:14 -0600 Subject: [PATCH 42/89] [usb_host] Implement disable_loop/enable_loop pattern for USB components (#14163) --- esphome/components/usb_host/usb_host.h | 21 +++++++++++-------- .../components/usb_host/usb_host_client.cpp | 17 ++++++++++++++- esphome/components/usb_uart/usb_uart.cpp | 11 +++++++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d1ec356613..a6a97d0bd7 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -73,12 +73,12 @@ static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than mai // used to report a transfer status struct TransferStatus { - bool success; - uint16_t error_code; uint8_t *data; size_t data_len; - uint8_t endpoint; void *user_data; + uint16_t error_code; + uint8_t endpoint; + bool success; }; using transfer_cb_t = std::function; @@ -127,7 +127,7 @@ class USBClient : public Component { friend class USBHost; public: - USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} + USBClient(uint16_t vid, uint16_t pid) : trq_in_use_(0), vid_(vid), pid_(pid) {} void setup() override; void loop() override; // setup must happen after the host bus has been setup @@ -148,6 +148,10 @@ class USBClient : public Component { EventPool event_pool; protected: + // Process USB events from the queue. Returns true if any work was done. + // Subclasses should call this instead of USBClient::loop() to combine + // with their own work check for a single disable_loop() decision. + bool process_usb_events_(); void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); @@ -161,20 +165,19 @@ class USBClient : public Component { static void usb_task_fn(void *arg); [[noreturn]] void usb_task_loop() const; + // Members ordered to minimize struct padding on 32-bit platforms + TransferRequest requests_[MAX_REQUESTS]{}; TaskHandle_t usb_task_handle_{nullptr}; - usb_host_client_handle_t handle_{}; usb_device_handle_t device_handle_{}; int device_addr_{-1}; int state_{USB_CLIENT_INIT}; - uint16_t vid_{}; - uint16_t pid_{}; // Lock-free pool management using atomic bitmask (no dynamic allocation) // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) - // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots std::atomic trq_in_use_; - TransferRequest requests_[MAX_REQUESTS]{}; + uint16_t vid_{}; + uint16_t pid_{}; }; class USBHost : public Component { public: diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 0612d7a841..a9be38fb03 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -197,6 +197,9 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + // Re-enable component loop to process the queued event + client->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); @@ -243,10 +246,13 @@ void USBClient::usb_task_loop() const { } } -void USBClient::loop() { +bool USBClient::process_usb_events_() { + bool had_work = false; + // Process any events from the USB task UsbEvent *event; while ((event = this->event_queue.pop()) != nullptr) { + had_work = true; switch (event->type) { case EVENT_DEVICE_NEW: this->on_opened(event->data.device_new.address); @@ -266,8 +272,17 @@ void USBClient::loop() { } if (this->state_ == USB_CLIENT_OPEN) { + had_work = true; this->handle_open_state_(); } + + return had_work; +} + +void USBClient::loop() { + if (!this->process_usb_events_()) { + this->disable_loop(); + } } void USBClient::handle_open_state_() { diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index edd01c26c6..5c2806c456 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -172,11 +172,12 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { } void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { - USBClient::loop(); + bool had_work = this->process_usb_events_(); // Process USB data from the lock-free queue UsbDataChunk *chunk; while ((chunk = this->usb_data_queue_.pop()) != nullptr) { + had_work = true; auto *channel = chunk->channel; #ifdef USE_UART_DEBUGGER @@ -198,6 +199,11 @@ void USBUartComponent::loop() { if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped); } + + // Disable loop when idle. Callbacks re-enable via enable_loop_soon_any_context(). + if (!had_work) { + this->disable_loop(); + } } void USBUartComponent::dump_config() { USBClient::dump_config(); @@ -264,6 +270,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + // Re-enable component loop to process the queued data + this->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB data instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); From 0e38acd67a178ca6490284093c7034e8bfde5532 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:56 -0600 Subject: [PATCH 43/89] [api] Warn when clients connect with outdated API version (#14145) --- esphome/components/api/api_connection.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5a7994a322..5b02bee537 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1534,6 +1534,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { 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_); + // TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal) + if (!this->client_supports_api_version(1, 14)) { + ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(), + this->client_api_version_major_, this->client_api_version_minor_); + } + HelloResponse resp; resp.api_version_major = 1; resp.api_version_minor = 14; From 8589f80d8fea4751aeac15134c91ca79fdf850a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 20:59:49 -0600 Subject: [PATCH 44/89] [api,ota,captive_portal] Fix fd leaks and clean up socket_ip_loop_monitored setup paths (#14167) --- esphome/components/api/api_server.cpp | 30 +++++++++---------- esphome/components/api/api_server.h | 9 +++++- .../captive_portal/dns_server_esp32_idf.cpp | 9 ++---- .../captive_portal/dns_server_esp32_idf.h | 9 ++++-- .../components/esphome/ota/ota_esphome.cpp | 26 +++++++++------- esphome/components/esphome/ota/ota_esphome.h | 3 +- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1a1d0b229b..5b096788f5 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -30,6 +30,12 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c APIServer::APIServer() { global_api_server = this; } +void APIServer::socket_failed_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); + this->destroy_socket_(); + this->mark_failed(); +} + void APIServer::setup() { ControllerRegistry::register_controller(this); @@ -48,22 +54,20 @@ void APIServer::setup() { #endif #endif - this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket"); - this->mark_failed(); + this->socket_failed_(LOG_STR("creation")); return; } int enable = 1; int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno); // we can still continue } err = this->socket_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); - this->mark_failed(); + this->socket_failed_(LOG_STR("nonblocking")); return; } @@ -71,22 +75,19 @@ void APIServer::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("set sockaddr")); return; } err = this->socket_->bind((struct sockaddr *) &server, sl); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("bind")); return; } err = this->socket_->listen(this->listen_backlog_); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("listen")); return; } @@ -622,10 +623,7 @@ void APIServer::on_shutdown() { this->shutting_down_ = true; // Close the listening socket to prevent new connections - if (this->socket_) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); // Change batch delay to 5ms for quick flushing during shutdown this->batch_delay_ = 5; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3b9ba0e23b..fed29016b3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -249,8 +249,15 @@ class APIServer : public Component, void add_state_subscription_(std::string entity_id, optional attribute, std::function f, bool once); #endif // USE_API_HOMEASSISTANT_STATES + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } + void socket_failed_(const LogString *msg); // Pointers and pointer-like types first (4 bytes each) - std::unique_ptr socket_ = nullptr; + socket::Socket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger client_connected_trigger_; #endif diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 5743cbd671..bd9989a40c 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -53,7 +53,7 @@ void DNSServer::start(const network::IPAddress &ip) { #endif // Create loop-monitored UDP socket - this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); + this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP).release(); if (this->socket_ == nullptr) { ESP_LOGE(TAG, "Socket create failed"); return; @@ -70,17 +70,14 @@ void DNSServer::start(const network::IPAddress &ip) { int err = this->socket_->bind((struct sockaddr *) &server_addr, addr_len); if (err != 0) { ESP_LOGE(TAG, "Bind failed: %d", errno); - this->socket_ = nullptr; + this->destroy_socket_(); return; } ESP_LOGV(TAG, "Bound to port %d", DNS_PORT); } void DNSServer::stop() { - if (this->socket_ != nullptr) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); ESP_LOGV(TAG, "Stopped"); } diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index 3e0ac07373..f8e4cfec84 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -1,7 +1,6 @@ #pragma once #ifdef USE_ESP32 -#include #include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/socket/socket.h" @@ -15,9 +14,15 @@ class DNSServer { void process_next_request(); protected: + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } static constexpr size_t DNS_BUFFER_SIZE = 192; - std::unique_ptr socket_{nullptr}; + socket::Socket *socket_{nullptr}; network::IPAddress server_ip_; uint8_t buffer_[DNS_BUFFER_SIZE]; }; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index df2ea98f2c..a1cdf59d2b 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -28,10 +28,9 @@ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer void ESPHomeOTAComponent::setup() { - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->server_ == nullptr) { - this->log_socket_error_(LOG_STR("creation")); - this->mark_failed(); + this->server_failed_(LOG_STR("creation")); return; } int enable = 1; @@ -42,8 +41,7 @@ void ESPHomeOTAComponent::setup() { } err = this->server_->setblocking(false); if (err != 0) { - this->log_socket_error_(LOG_STR("non-blocking")); - this->mark_failed(); + this->server_failed_(LOG_STR("nonblocking")); return; } @@ -51,22 +49,19 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - this->log_socket_error_(LOG_STR("set sockaddr")); - this->mark_failed(); + this->server_failed_(LOG_STR("set sockaddr")); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - this->log_socket_error_(LOG_STR("bind")); - this->mark_failed(); + this->server_failed_(LOG_STR("bind")); return; } err = this->server_->listen(1); // Only one client at a time if (err != 0) { - this->log_socket_error_(LOG_STR("listen")); - this->mark_failed(); + this->server_failed_(LOG_STR("listen")); return; } } @@ -455,6 +450,15 @@ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during)); } +void ESPHomeOTAComponent::server_failed_(const LogString *msg) { + this->log_socket_error_(msg); + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + delete this->server_; + this->server_ = nullptr; + this->mark_failed(); +} + bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) { if (read == -1 && this->would_block_(errno)) { return false; // No data yet, try again next loop diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e199b7e406..c9e89c82ba 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -66,6 +66,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { this->handshake_buf_pos_ = 0; // Reset buffer position for next state } + void server_failed_(const LogString *msg); void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); @@ -83,7 +84,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD - std::unique_ptr server_; + socket::Socket *server_{nullptr}; std::unique_ptr client_; std::unique_ptr backend_; From abe37c98413c6b78a7a01bec82f4b2f14e68cd77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:08:49 -0600 Subject: [PATCH 45/89] [uptime] Use scheduler millis_64() for rollover-safe uptime tracking (#14170) --- .../uptime/sensor/uptime_seconds_sensor.cpp | 27 +++++-------------- .../uptime/sensor/uptime_seconds_sensor.h | 9 ++----- .../uptime/sensor/uptime_timestamp_sensor.cpp | 6 ++--- .../uptime/sensor/uptime_timestamp_sensor.h | 6 ++--- .../uptime/text_sensor/uptime_text_sensor.cpp | 24 ++++------------- .../uptime/text_sensor/uptime_text_sensor.h | 8 ++---- esphome/core/scheduler.cpp | 2 ++ esphome/core/scheduler.h | 3 +++ 8 files changed, 24 insertions(+), 61 deletions(-) diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 54260d7e80..20e8ed8fda 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,30 +1,16 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint32_t ms = millis(); - const uint64_t ms_mask = (1ULL << 32) - 1ULL; - const uint32_t last_ms = this->uptime_ & ms_mask; - if (ms < last_ms) { - this->uptime_ += ms_mask + 1ULL; - ESP_LOGD(TAG, "Detected roll-over \xf0\x9f\xa6\x84"); - } - this->uptime_ &= ~ms_mask; - this->uptime_ |= ms; - - // Do separate second and milliseconds conversion to avoid floating point division errors - // Probably some IEEE standard already guarantees this division can be done without loss - // of precision in a single division, but let's do it like this to be sure. - const uint64_t seconds_int = this->uptime_ / 1000ULL; - const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f; + const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t seconds_int = uptime / 1000ULL; + const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); } float UptimeSecondsSensor::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -33,5 +19,4 @@ void UptimeSecondsSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Seconds"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h index 210195052f..1b80a4480a 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.h +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { public: @@ -12,10 +11,6 @@ class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { void dump_config() override; float get_setup_priority() const override; - - protected: - uint64_t uptime_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp index 69033be11c..4e0f06be1c 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -33,7 +32,6 @@ void UptimeTimestampSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Timestamp"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h index f38b5d53b4..912c0b7655 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/time/real_time_clock.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTimestampSensor : public sensor::Sensor, public Component { public: @@ -24,7 +23,6 @@ class UptimeTimestampSensor : public sensor::Sensor, public Component { time::RealTimeClock *time_; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index acd3980a1a..88ae53fbfc 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,11 +1,10 @@ #include "uptime_text_sensor.h" -#include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -17,22 +16,10 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep pos = buf_append_printf(buf, buf_size, pos, "%u%s", value, label); } -void UptimeTextSensor::setup() { - this->last_ms_ = millis(); - if (this->last_ms_ < 60 * 1000) - this->last_ms_ = 0; - this->update(); -} +void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - auto now = millis(); - // get whole seconds since last update. Note that even if the millis count has overflowed between updates, - // the difference will still be correct due to the way twos-complement arithmetic works. - uint32_t delta = now - this->last_ms_; - this->last_ms_ = now - delta % 1000; // save remainder for next update - delta /= 1000; - this->uptime_ += delta; - uint32_t uptime = this->uptime_; + uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units @@ -89,5 +76,4 @@ void UptimeTextSensor::update() { float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index 947d9c91e9..a97ba332bb 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -5,8 +5,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { public: @@ -35,9 +34,6 @@ class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent const char *seconds_text_; const char *separator_; bool expand_{}; - uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years - uint32_t last_ms_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3294f689e8..e82efcc520 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -675,6 +675,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } +uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } + uint64_t Scheduler::millis_64_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 394178a831..afe11aaca6 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -116,6 +116,9 @@ class Scheduler { ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(Component *component, uint32_t id); + /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) + uint64_t millis_64(); + // Calculate when the next scheduled item should run // @param now Fresh timestamp from millis() - must not be stale/cached // Returns the time in milliseconds until the next scheduled item, or nullopt if no items From f8f98bf428e1db199f4b5bf79a4b3362b7d5672d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:16:49 -0600 Subject: [PATCH 46/89] [logger] Reduce UART driver heap waste on ESP32 (#14168) --- esphome/components/logger/logger_esp32.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index dfa643d5e9..9d64771aec 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -77,9 +77,10 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; uart_param_config(uart_num, &uart_config); - const int uart_buffer_size = tx_buffer_size; - // Install UART driver using an event queue here - uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); + // The logger only writes to UART, never reads, so use the minimum RX buffer. + // ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes). + const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1; + uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0); } void Logger::pre_setup() { From f77da803c9b316c78a51e602563dcd6d916682f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:39:18 -0600 Subject: [PATCH 47/89] [api] Write protobuf encode output to pre-sized buffer directly (#14018) --- esphome/components/api/api_connection.cpp | 35 ++--- esphome/components/api/api_pb2.cpp | 180 +++++++++++----------- esphome/components/api/api_pb2.h | 176 ++++++++++----------- esphome/components/api/proto.cpp | 15 ++ esphome/components/api/proto.h | 108 ++++++++----- script/api_protobuf/api_protobuf.py | 12 +- 6 files changed, 286 insertions(+), 240 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5b02bee537..9fc263abbd 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -347,9 +347,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess #endif // Calculate size - ProtoSize size_calc; - msg.calculate_size(size_calc); - uint32_t calculated_size = size_calc.get_size(); + uint32_t calculated_size = msg.calculated_size(); // Cache frame sizes to avoid repeated virtual calls const uint8_t header_padding = conn->helper_->frame_header_padding(); @@ -377,19 +375,14 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess shared_buf.resize(current_size + footer_size + header_padding); } - // Encode directly into buffer - size_t size_before_encode = shared_buf.size(); - msg.encode({&shared_buf}); + // Pre-resize buffer to include payload, then encode through raw pointer + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + calculated_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); - // Calculate actual encoded size (not including header that was already added) - size_t actual_payload_size = shared_buf.size() - size_before_encode; - - // Return actual total size (header + actual payload + footer) - size_t actual_total_size = header_padding + actual_payload_size + footer_size; - - // Verify that calculate_size() returned the correct value - assert(calculated_size == actual_payload_size); - return static_cast(actual_total_size); + // Return total size (header + payload + footer) + return static_cast(header_padding + calculated_size + footer_size); } #ifdef USE_BINARY_SENSOR @@ -1854,12 +1847,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); + uint32_t payload_size = msg.calculated_size(); std::vector &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); + this->prepare_first_message_buffer(shared_buf, payload_size); + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + payload_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); + return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 743f51dac7..5c50a8aa5b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -31,7 +31,7 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } return true; } -void HelloResponse::encode(ProtoWriteBuffer buffer) const { +void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); buffer.encode_string(3, this->server_info); @@ -44,7 +44,7 @@ void HelloResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); } #ifdef USE_AREAS -void AreaInfo::encode(ProtoWriteBuffer buffer) const { +void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } @@ -54,7 +54,7 @@ void AreaInfo::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_DEVICES -void DeviceInfo::encode(ProtoWriteBuffer buffer) const { +void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); @@ -65,7 +65,7 @@ void DeviceInfo::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->area_id); } #endif -void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); buffer.encode_string(4, this->esphome_version); @@ -111,7 +111,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area); + buffer.encode_message(22, this->area, false); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -176,7 +176,7 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #endif } #ifdef USE_BINARY_SENSOR -void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -206,7 +206,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -224,7 +224,7 @@ void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_COVER -void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -260,7 +260,7 @@ void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { +void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); @@ -317,7 +317,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_FAN -void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -359,7 +359,7 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void FanStateResponse::encode(ProtoWriteBuffer buffer) const { +void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); @@ -443,7 +443,7 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LIGHT -void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -489,7 +489,7 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { size.add_uint32(2, this->device_id); #endif } -void LightStateResponse::encode(ProtoWriteBuffer buffer) const { +void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); @@ -635,7 +635,7 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SENSOR -void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -671,7 +671,7 @@ void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -689,7 +689,7 @@ void SensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_SWITCH -void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -719,7 +719,7 @@ void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { +void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -760,7 +760,7 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_TEXT_SENSOR -void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -788,7 +788,7 @@ void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -818,7 +818,7 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } -void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } @@ -839,11 +839,11 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } return true; } -void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { +void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } @@ -851,7 +851,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { size.add_length(1, this->key.size()); size.add_length(1, this->value.size()); } -void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { +void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { buffer.encode_message(2, it); @@ -924,7 +924,7 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); @@ -976,7 +976,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #ifdef USE_API_USER_DEFINED_ACTIONS -void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); } @@ -984,7 +984,7 @@ void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); size.add_uint32(1, static_cast(this->type)); } -void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { @@ -1103,7 +1103,7 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const { +void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->call_id); buffer.encode_bool(2, this->success); buffer.encode_string(3, this->error_message); @@ -1121,7 +1121,7 @@ void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_CAMERA -void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1147,7 +1147,7 @@ void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { +void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); @@ -1178,7 +1178,7 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } #endif #ifdef USE_CLIMATE -void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1276,7 +1276,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(2, this->feature_flags); } -void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { +void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); @@ -1407,7 +1407,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_WATER_HEATER -void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1449,7 +1449,7 @@ void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { } size.add_uint32(1, this->supported_features); } -void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const { +void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); @@ -1515,7 +1515,7 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_NUMBER -void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1553,7 +1553,7 @@ void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { +void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1596,7 +1596,7 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SELECT -void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1630,7 +1630,7 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { +void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1681,7 +1681,7 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SIREN -void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1719,7 +1719,7 @@ void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { +void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -1789,7 +1789,7 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LOCK -void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1823,7 +1823,7 @@ void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void LockStateResponse::encode(ProtoWriteBuffer buffer) const { +void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -1878,7 +1878,7 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_BUTTON -void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1930,7 +1930,7 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_MEDIA_PLAYER -void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->format); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); @@ -1944,7 +1944,7 @@ void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->purpose)); size.add_uint32(1, this->sample_bytes); } -void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1978,7 +1978,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(1, this->feature_flags); } -void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); @@ -2062,7 +2062,7 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_sint32(2, this->rssi); buffer.encode_uint32(3, this->address_type); @@ -2074,7 +2074,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->address_type); size.add_length(1, this->data_len); } -void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { buffer.encode_message(1, this->advertisements[i]); } @@ -2103,7 +2103,7 @@ bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) } return true; } -void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->connected); buffer.encode_uint32(3, this->mtu); @@ -2125,7 +2125,7 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI } return true; } -void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2141,7 +2141,7 @@ void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2163,7 +2163,7 @@ void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->descriptors); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2183,7 +2183,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->characteristics); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { buffer.encode_message(2, it); @@ -2193,7 +2193,7 @@ void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_repeated_message(1, this->services); } -void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } @@ -2210,7 +2210,7 @@ bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt valu } return true; } -void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2302,7 +2302,7 @@ bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt va } return true; } -void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2312,7 +2312,7 @@ void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_length(1, this->data_len_); } -void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); for (const auto &it : this->allocated) { @@ -2330,7 +2330,7 @@ void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { } } } -void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); @@ -2340,7 +2340,7 @@ void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_int32(1, this->error); } -void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2348,7 +2348,7 @@ void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2356,7 +2356,7 @@ void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); @@ -2366,7 +2366,7 @@ void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->paired); size.add_int32(1, this->error); } -void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2376,7 +2376,7 @@ void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2386,7 +2386,7 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_uint32(3, static_cast(this->configured_mode)); @@ -2421,7 +2421,7 @@ bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarIn } return true; } -void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); @@ -2431,11 +2431,11 @@ void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->auto_gain); size.add_float(1, this->volume_multiplier); } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings); + buffer.encode_message(4, this->audio_settings, false); buffer.encode_string(5, this->wake_word_phrase); } void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { @@ -2516,7 +2516,7 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } return true; } -void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } @@ -2587,9 +2587,9 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength } return true; } -void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } -void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); for (auto &it : this->trained_languages) { @@ -2656,7 +2656,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } return true; } -void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { buffer.encode_message(1, it); } @@ -2686,7 +2686,7 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt } #endif #ifdef USE_ALARM_CONTROL_PANEL -void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2718,7 +2718,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) cons size.add_uint32(1, this->device_id); #endif } -void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -2770,7 +2770,7 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit } #endif #ifdef USE_TEXT -void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2804,7 +2804,7 @@ void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -2855,7 +2855,7 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATE -void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2881,7 +2881,7 @@ void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); @@ -2934,7 +2934,7 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_TIME -void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2960,7 +2960,7 @@ void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); @@ -3013,7 +3013,7 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_EVENT -void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3049,7 +3049,7 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void EventResponse::encode(ProtoWriteBuffer buffer) const { +void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES @@ -3065,7 +3065,7 @@ void EventResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_VALVE -void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3099,7 +3099,7 @@ void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { +void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); @@ -3148,7 +3148,7 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATETIME -void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3174,7 +3174,7 @@ void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); @@ -3217,7 +3217,7 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_UPDATE -void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3245,7 +3245,7 @@ void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { +void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); @@ -3314,7 +3314,7 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -void ZWaveProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } +void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3338,7 +3338,7 @@ bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited va } return true; } -void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { +void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } @@ -3348,7 +3348,7 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_INFRARED -void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3419,7 +3419,7 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -void InfraredRFReceiveEvent::encode(ProtoWriteBuffer buffer) const { +void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d001f869c5..c90873d993 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -382,7 +382,7 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -447,7 +447,7 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -462,7 +462,7 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -527,7 +527,7 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -558,7 +558,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -575,7 +575,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -597,7 +597,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -615,7 +615,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -657,7 +657,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector *supported_preset_modes{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -677,7 +677,7 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -724,7 +724,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector *effects{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -751,7 +751,7 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -815,7 +815,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -832,7 +832,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -851,7 +851,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -867,7 +867,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -901,7 +901,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -918,7 +918,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -957,7 +957,7 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -990,7 +990,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1004,7 +1004,7 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1033,7 +1033,7 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1077,7 +1077,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1138,7 +1138,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1157,7 +1157,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1227,7 +1227,7 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1244,7 +1244,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1266,7 +1266,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1317,7 +1317,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1345,7 +1345,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1403,7 +1403,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1424,7 +1424,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1468,7 +1468,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1485,7 +1485,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1519,7 +1519,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_select_response"; } #endif const FixedVector *options{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1536,7 +1536,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1573,7 +1573,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1589,7 +1589,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1634,7 +1634,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1650,7 +1650,7 @@ class LockStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1687,7 +1687,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_button_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1719,7 +1719,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1737,7 +1737,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1755,7 +1755,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1811,7 +1811,7 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1828,7 +1828,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1865,7 +1865,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1893,7 +1893,7 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1908,7 +1908,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1922,7 +1922,7 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1939,7 +1939,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector services{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1955,7 +1955,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1994,7 +1994,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2089,7 +2089,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2107,7 +2107,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array allocated{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2125,7 +2125,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2142,7 +2142,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2159,7 +2159,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2177,7 +2177,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2195,7 +2195,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2213,7 +2213,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2231,7 +2231,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2277,7 +2277,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2297,7 +2297,7 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2359,7 +2359,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2417,7 +2417,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2430,7 +2430,7 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector trained_languages{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2480,7 +2480,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector available_wake_words{}; const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2515,7 +2515,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2531,7 +2531,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2570,7 +2570,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2587,7 +2587,7 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2621,7 +2621,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2640,7 +2640,7 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2675,7 +2675,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2694,7 +2694,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2731,7 +2731,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector *event_types{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2747,7 +2747,7 @@ class EventResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "event_response"; } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2768,7 +2768,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2785,7 +2785,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2820,7 +2820,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2837,7 +2837,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2871,7 +2871,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_update_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2895,7 +2895,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2930,7 +2930,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2949,7 +2949,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2969,7 +2969,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_infrared_response"; } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3016,7 +3016,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector *timings{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 764dd3f391..73a3bab12a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -70,6 +70,21 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +#ifdef ESPHOME_DEBUG_API +void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { + if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { + ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes, + this->pos_ - this->buffer_->data(), this->buffer_->size()); + abort(); + } +} +void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) { + ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id, + expected, actual); + abort(); +} +#endif + void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *ptr = buffer; const uint8_t *end = buffer + length; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 8ac79633cf..4522fc9665 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -217,21 +217,26 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} - void write(uint8_t value) { this->buffer_->push_back(value); } + ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(std::vector *buffer, size_t write_pos) + : buffer_(buffer), pos_(buffer->data() + write_pos) {} void encode_varint_raw(uint32_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } void encode_varint_raw_64(uint64_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } /** * Encode a field key (tag/wire type combination). @@ -245,23 +250,18 @@ class ProtoWriteBuffer { * * Following https://protobuf.dev/programming-guides/encoding/#structure */ - void encode_field_raw(uint32_t field_id, uint32_t type) { - uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); - this->encode_varint_raw(val); - } + void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); - - // Using resize + memcpy instead of insert provides significant performance improvement: - // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings - // as it avoids iterator checks and potential element moves that insert performs - size_t old_size = this->buffer_->size(); - this->buffer_->resize(old_size + len); - std::memcpy(this->buffer_->data() + old_size, string, len); + // Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks + // and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings. + this->debug_check_bounds_(len); + std::memcpy(this->pos_, string, len); + this->pos_ += len; } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); @@ -288,17 +288,26 @@ class ProtoWriteBuffer { if (!value && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->buffer_->push_back(value ? 0x01 : 0x00); + this->debug_check_bounds_(1); + *this->pos_++ = value ? 0x01 : 0x00; } - void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + // noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy + __attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); + this->debug_check_bounds_(4); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + // Protobuf fixed32 is little-endian, so direct copy works + std::memcpy(this->pos_, &value, 4); + this->pos_ += 4; +#else + *this->pos_++ = (value >> 0) & 0xFF; + *this->pos_++ = (value >> 8) & 0xFF; + *this->pos_++ = (value >> 16) & 0xFF; + *this->pos_++ = (value >> 24) & 0xFF; +#endif } // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally // not supported to reduce overhead on embedded systems. All ESPHome devices are @@ -334,11 +343,20 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); - void encode_message(uint32_t field_id, const ProtoMessage &value); + /// Encode a nested message field (force=true for repeated, false for singular) + void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); std::vector *get_buffer() const { return buffer_; } protected: +#ifdef ESPHOME_DEBUG_API + void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); + void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); +#else + void debug_check_bounds_([[maybe_unused]] size_t bytes) {} +#endif + std::vector *buffer_; + uint8_t *pos_; }; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -416,9 +434,11 @@ class ProtoMessage { public: virtual ~ProtoMessage() = default; // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer buffer) const {} + virtual void encode(ProtoWriteBuffer &buffer) const {} // Default implementation for messages with no fields virtual void calculate_size(ProtoSize &size) const {} + // Convenience: calculate and return size directly (defined after ProtoSize) + uint32_t calculated_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } @@ -877,6 +897,14 @@ class ProtoSize { } }; +// Implementation of methods that depend on ProtoSize being fully defined + +inline uint32_t ProtoMessage::calculated_size() const { + ProtoSize size; + this->calculate_size(size); + return size.get_size(); +} + // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector &values) { if (values.empty()) @@ -897,30 +925,30 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } // Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { // Calculate the message size first ProtoSize msg_size; value.calculate_size(msg_size); uint32_t msg_length_bytes = msg_size.get_size(); - // Calculate how many bytes the length varint needs - uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); + // Skip empty singular messages (matches add_message_field which skips when nested_size == 0) + // Repeated messages (force=true) are always encoded since an empty item is meaningful + if (msg_length_bytes == 0 && !force) + return; - // Reserve exact space for the length varint - size_t begin = this->buffer_->size(); - this->buffer_->resize(this->buffer_->size() + varint_length_bytes); + this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - // Write the length varint directly - 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); + // Write the length varint directly through pos_ + this->encode_varint_raw(msg_length_bytes); + // Encode nested message - pos_ advances directly through the reference #ifdef ESPHOME_DEBUG_API - // Verify that the encoded size matches what we calculated - assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); + uint8_t *start = this->pos_; + value.encode(*this); + if (static_cast(this->pos_ - start) != msg_length_bytes) + this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); +#else + value.encode(*this); #endif } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4fbee49dae..cc881caa5c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -689,6 +689,14 @@ class MessageType(TypeInfo): def encode_func(self) -> str: return "encode_message" + @property + def encode_content(self) -> str: + # Singular message fields pass force=false (skip empty messages) + # The default for encode_nested_message is force=true (for repeated fields) + return ( + f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" + ) + @property def decode_length(self) -> str: # Override to return None for message types because we can't use template-based @@ -2186,7 +2194,7 @@ def build_message_type( # Only generate encode method if this message needs encoding and has fields if needs_encode and encode: - o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" + o = f"void {desc.name}::encode(ProtoWriteBuffer &buffer) const {{" if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: o += f" {encode[0]} }}\n" else: @@ -2194,7 +2202,7 @@ def build_message_type( o += indent("\n".join(encode)) + "\n" o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer buffer) const override;" + prot = "void encode(ProtoWriteBuffer &buffer) const override;" public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used From 2eac106f119627e2ac58d39f4ebd92990895366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Sat, 21 Feb 2026 05:20:27 +0100 Subject: [PATCH 48/89] [mqtt] add missing precision in HA autodiscovery (#14010) --- esphome/components/mqtt/mqtt_const.h | 1 + esphome/components/mqtt/mqtt_sensor.cpp | 4 ++++ esphome/components/sensor/sensor.h | 2 ++ 3 files changed, 7 insertions(+) diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 221af00371..36a7dfb41a 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -243,6 +243,7 @@ X(MQTT_STATE_VALUE_TEMPLATE, "stat_val_tpl", "state_value_template") \ X(MQTT_STEP, "step", "step") \ X(MQTT_SUBTYPE, "stype", "subtype") \ + X(MQTT_SUGGESTED_DISPLAY_PRECISION, "sug_dsp_prc", "suggested_display_precision") \ X(MQTT_SUPPORTED_COLOR_MODES, "sup_clrm", "supported_color_modes") \ X(MQTT_SUPPORTED_FEATURES, "sup_feat", "supported_features") \ X(MQTT_SWING_MODE_COMMAND_TEMPLATE, "swing_mode_cmd_tpl", "swing_mode_command_template") \ diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index e83eab6732..a7d311d194 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -49,6 +49,10 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_DEVICE_CLASS] = device_class; } + if (this->sensor_->has_accuracy_decimals()) { + root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals(); + } + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f9a45cb1d0..80981b8e28 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -48,6 +48,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa int8_t get_accuracy_decimals(); /// Manually set the accuracy in decimals. void set_accuracy_decimals(int8_t accuracy_decimals); + /// Check if the accuracy in decimals has been manually set. + bool has_accuracy_decimals() const { return this->sensor_flags_.has_accuracy_override; } /// Get the state class, using the manual override if set. StateClass get_state_class(); From 518f08b9097bdb3a980483f1358e309987de7402 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:51:13 -1000 Subject: [PATCH 49/89] [mipi_dsi] Disallow swap_xy (#14124) --- esphome/components/mipi_dsi/display.py | 26 +++++-------------- .../mipi_dsi/fixtures/mipi_dsi.yaml | 7 ++++- .../mipi_dsi/test_mipi_dsi_config.py | 6 +++-- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index c288b33cd2..de3791b3a4 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -87,38 +87,24 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] + model.defaults[CONF_SWAP_XY] = cv.UNDEFINED transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), } ) - if model.get_default(CONF_SWAP_XY) != cv.UNDEFINED: - transform = transform.extend( - { - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by this model" - ) - } - ) - else: - transform = transform.extend( - { - cv.Required(CONF_SWAP_XY): cv.boolean, - } - ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) if model.initsequence is None else cv.Optional(CONF_INIT_SEQUENCE) ) - swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) - - # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden - cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required - ) + # Dimensions are optional if the model has a default width + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24") schema = display.FULL_DISPLAY_SCHEMA.extend( { diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 7d1fc84121..6de2bd5a77 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -15,8 +15,13 @@ esp_ldo: display: - platform: mipi_dsi + id: p4_nano model: WAVESHARE-P4-NANO-10.1 - + rotation: 90 + - platform: mipi_dsi + id: p4_86 + model: "WAVESHARE-P4-86-PANEL" + rotation: 180 i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index f8a9af0279..d465a8c81b 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,9 +119,11 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp + assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 6ecb01dedc00595aa2565bcf302cde3d7ad1d4e9 Mon Sep 17 00:00:00 2001 From: Sxt Fov <140247217+sxtfov@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:45:15 +0100 Subject: [PATCH 50/89] [cc1101] actions to change general and tuner settings (#14141) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/cc1101/__init__.py | 92 +++++++++++++++++++++++++ esphome/components/cc1101/cc1101.h | 78 ++++++++++++++++++++++ tests/components/cc1101/common.yaml | 96 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index fbdd7010b4..14b92a18a4 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_VALUE, CONF_WAIT_TIME, ) from esphome.core import ID @@ -333,3 +334,94 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) return var + + +# Setter action definitions: (setter_name, validator, template_type, enum_map) +_SETTER_ACTIONS = [ + ( + "set_frequency", + cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), + float, + None, + ), + ("set_output_power", cv.float_range(min=-30.0, max=11.0), float, None), + ("set_modulation_type", cv.enum(MODULATION, upper=False), Modulation, MODULATION), + ("set_symbol_rate", cv.float_range(min=600, max=500000), float, None), + ( + "set_rx_attenuation", + cv.enum(RX_ATTENUATION, upper=False), + RxAttenuation, + RX_ATTENUATION, + ), + ("set_dc_blocking_filter", cv.boolean, bool, None), + ("set_manchester", cv.boolean, bool, None), + ( + "set_filter_bandwidth", + cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), + float, + None, + ), + ( + "set_fsk_deviation", + cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), + float, + None, + ), + ("set_msk_deviation", cv.int_range(min=1, max=8), cg.uint8, None), + ("set_channel", cv.uint8_t, cg.uint8, None), + ( + "set_channel_spacing", + cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), + float, + None, + ), + ( + "set_if_frequency", + cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), + float, + None, + ), +] + + +def _register_setter_actions(): + for setter_name, validator, templ_type, enum_map in _SETTER_ACTIONS: + class_name = ( + "".join(word.capitalize() for word in setter_name.split("_")) + "Action" + ) + action_cls = ns.class_( + class_name, automation.Action, cg.Parented.template(CC1101Component) + ) + schema = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(CC1101Component), + cv.Required(CONF_VALUE): cv.templatable(validator), + }, + key=CONF_VALUE, + ) + + async def _setter_action_to_code( + config, + action_id, + template_arg, + args, + _setter=setter_name, + _type=templ_type, + _map=enum_map, + ): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_VALUE] + if cg.is_template(data): + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) + else: + cg.add(getattr(var, _setter)(_map[data] if _map else data)) + return var + + automation.register_action(f"cc1101.{setter_name}", action_cls, schema)( + _setter_action_to_code + ) + + +_register_setter_actions() diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index e55071e7e3..2efd9e082d 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -161,4 +161,82 @@ template class SendPacketAction : public Action, public P size_t data_static_len_{0}; }; +template class SetSymbolRateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, symbol_rate) + void play(const Ts &...x) override { this->parent_->set_symbol_rate(this->symbol_rate_.value(x...)); } +}; + +template class SetFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, frequency) + void play(const Ts &...x) override { this->parent_->set_frequency(this->frequency_.value(x...)); } +}; + +template class SetOutputPowerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, output_power) + void play(const Ts &...x) override { this->parent_->set_output_power(this->output_power_.value(x...)); } +}; + +template class SetModulationTypeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Modulation, modulation_type) + void play(const Ts &...x) override { this->parent_->set_modulation_type(this->modulation_type_.value(x...)); } +}; + +template class SetRxAttenuationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(RxAttenuation, rx_attenuation) + void play(const Ts &...x) override { this->parent_->set_rx_attenuation(this->rx_attenuation_.value(x...)); } +}; + +template class SetDcBlockingFilterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, dc_blocking_filter) + void play(const Ts &...x) override { this->parent_->set_dc_blocking_filter(this->dc_blocking_filter_.value(x...)); } +}; + +template class SetManchesterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, manchester) + void play(const Ts &...x) override { this->parent_->set_manchester(this->manchester_.value(x...)); } +}; + +template class SetFilterBandwidthAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, filter_bandwidth) + void play(const Ts &...x) override { this->parent_->set_filter_bandwidth(this->filter_bandwidth_.value(x...)); } +}; + +template class SetFskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, fsk_deviation) + void play(const Ts &...x) override { this->parent_->set_fsk_deviation(this->fsk_deviation_.value(x...)); } +}; + +template class SetMskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, msk_deviation) + void play(const Ts &...x) override { this->parent_->set_msk_deviation(this->msk_deviation_.value(x...)); } +}; + +template class SetChannelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + void play(const Ts &...x) override { this->parent_->set_channel(this->channel_.value(x...)); } +}; + +template class SetChannelSpacingAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, channel_spacing) + void play(const Ts &...x) override { this->parent_->set_channel_spacing(this->channel_spacing_.value(x...)); } +}; + +template class SetIfFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, if_frequency) + void play(const Ts &...x) override { this->parent_->set_if_frequency(this->if_frequency_.value(x...)); } +}; + } // namespace esphome::cc1101 diff --git a/tests/components/cc1101/common.yaml b/tests/components/cc1101/common.yaml index 42ec50911f..9784bfce8b 100644 --- a/tests/components/cc1101/common.yaml +++ b/tests/components/cc1101/common.yaml @@ -35,3 +35,99 @@ button: data: [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef] - cc1101.send_packet: !lambda |- return {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + + - cc1101.set_frequency: !lambda |- + return 433.91e6; + - cc1101.set_frequency: + value: "433.91MHz" + - cc1101.set_frequency: + value: 433911000 + - cc1101.set_frequency: 433912000 + + - cc1101.set_output_power: !lambda |- + return -29.9; + - cc1101.set_output_power: + value: "-28" + - cc1101.set_output_power: + value: 10 + - cc1101.set_output_power: 11 + + - cc1101.set_modulation_type: !lambda |- + return cc1101::Modulation::MODULATION_2_FSK; + - cc1101.set_modulation_type: + value: "4-FSK" + - cc1101.set_modulation_type: "GFSK" + + - cc1101.set_symbol_rate: !lambda |- + return 6000.0; + - cc1101.set_symbol_rate: + value: "7000.0" + - cc1101.set_symbol_rate: + value: 8000.0 + - cc1101.set_symbol_rate: 9000 + + - cc1101.set_rx_attenuation: !lambda |- + return cc1101::RxAttenuation::RX_ATTENUATION_0DB; + - cc1101.set_rx_attenuation: + value: "6dB" + - cc1101.set_rx_attenuation: "12dB" + + - cc1101.set_dc_blocking_filter: !lambda |- + return false; + - cc1101.set_dc_blocking_filter: + value: true + - cc1101.set_dc_blocking_filter: false + + - cc1101.set_manchester: !lambda |- + return false; + - cc1101.set_manchester: + value: true + - cc1101.set_manchester: false + + - cc1101.set_filter_bandwidth: !lambda |- + return 58e3; + - cc1101.set_filter_bandwidth: + value: "59kHz" + - cc1101.set_filter_bandwidth: + value: 60000 + - cc1101.set_filter_bandwidth: "61kHz" + + - cc1101.set_fsk_deviation: !lambda |- + return 1.5e3; + - cc1101.set_fsk_deviation: + value: "1.6kHz" + - cc1101.set_fsk_deviation: + value: 1700 + - cc1101.set_fsk_deviation: "1.8kHz" + + - cc1101.set_msk_deviation: !lambda |- + return 1; + - cc1101.set_msk_deviation: + value: "2" + - cc1101.set_msk_deviation: + value: 3 + - cc1101.set_msk_deviation: "4" + + - cc1101.set_channel: !lambda |- + return 0; + - cc1101.set_channel: + value: "1" + - cc1101.set_channel: + value: 3 + - cc1101.set_channel: 3 + + - cc1101.set_channel_spacing: !lambda |- + return 25e3; + - cc1101.set_channel_spacing: + value: "26kHz" + - cc1101.set_channel_spacing: + value: 27000 + - cc1101.set_channel_spacing: "28kHz" + + - cc1101.set_if_frequency: !lambda |- + return 25e3; + - cc1101.set_if_frequency: + value: "26kHz" + - cc1101.set_if_frequency: + value: 27000 + - cc1101.set_if_frequency: "28kHz" From 7fb09da7cf228b3a5d2bbdfd60b3e6fb571b0331 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:08:13 -0600 Subject: [PATCH 51/89] [dsmr] Add deprecated std::string overload for set_decryption_key (#14180) --- esphome/components/dsmr/dsmr.h | 3 +++ tests/components/dsmr/common.yaml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index fafcf62b87..dc81ba9b2a 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -64,6 +64,9 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; void set_decryption_key(const char *decryption_key); + // Remove before 2026.8.0 + ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") + void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index 038bf2806b..d11ce37d59 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -1,4 +1,13 @@ +esphome: + on_boot: + then: + - lambda: |- + // Test deprecated std::string overload still compiles + std::string key = "00112233445566778899aabbccddeeff"; + id(dsmr_instance).set_decryption_key(key); + dsmr: + id: dsmr_instance decryption_key: 00112233445566778899aabbccddeeff max_telegram_length: 1000 request_pin: ${request_pin} From 416b97311b800e9563e48da0e403ed22e717ff61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:12:35 -0600 Subject: [PATCH 52/89] [mqtt] Remove broken ESP8266 ssl_fingerprints option (#14182) --- esphome/__main__.py | 14 --------- esphome/components/mqtt/__init__.py | 21 -------------- .../components/mqtt/mqtt_backend_esp8266.h | 5 ---- .../components/mqtt/mqtt_backend_libretiny.h | 5 ---- esphome/components/mqtt/mqtt_client.cpp | 7 ----- esphome/components/mqtt/mqtt_client.h | 15 ---------- esphome/const.py | 1 - esphome/mqtt.py | 29 ++----------------- 8 files changed, 2 insertions(+), 95 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c86b5604e1..488955f503 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -944,12 +944,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None: return 0 -def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: - from esphome import mqtt - - return mqtt.get_fingerprint(config) - - def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 @@ -1237,7 +1231,6 @@ POST_CONFIG_ACTIONS = { "run": command_run, "clean": command_clean, "clean-mqtt": command_clean_mqtt, - "mqtt-fingerprint": command_mqtt_fingerprint, "idedata": command_idedata, "rename": command_rename, "discover": command_discover, @@ -1451,13 +1444,6 @@ def parse_args(argv): ) parser_wizard.add_argument("configuration", help="Your YAML configuration file.") - parser_fingerprint = subparsers.add_parser( - "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." - ) - parser_fingerprint.add_argument( - "configuration", help="Your YAML configuration file(s).", nargs="+" - ) - subparsers.add_parser("version", help="Print the ESPHome version and exit.") parser_clean = subparsers.add_parser( diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index fe153fedfa..44e8836487 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,5 +1,3 @@ -import re - from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -46,7 +44,6 @@ from esphome.const import ( CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_STATE_TOPIC, CONF_SUBSCRIBE_QOS, CONF_TOPIC, @@ -221,13 +218,6 @@ def validate_config(value): return out -def validate_fingerprint(value): - value = cv.string(value) - if re.match(r"^[0-9a-f]{40}$", value) is None: - raise cv.Invalid("fingerprint must be valid SHA1 hash") - return value - - def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: """Register socket needs for MQTT component.""" # MQTT needs 1 socket for the broker connection @@ -291,9 +281,6 @@ CONFIG_SCHEMA = cv.All( ), validate_message_just_topic, ), - cv.Optional(CONF_SSL_FINGERPRINTS): cv.All( - cv.only_on_esp8266, cv.ensure_list(validate_fingerprint) - ), cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -444,14 +431,6 @@ async def to_code(config): if CONF_LEVEL in log_topic: cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]])) - if CONF_SSL_FINGERPRINTS in config: - for fingerprint in config[CONF_SSL_FINGERPRINTS]: - arr = [ - cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2) - ] - cg.add(var.add_ssl_fingerprint(arr)) - cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") - cg.add(var.set_keep_alive(config[CONF_KEEPALIVE])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 470d1e6a8b..0bf5b510a4 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -21,11 +21,6 @@ class MQTTBackendESP8266 final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index 24bf018a90..5fa3406193 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -21,11 +21,6 @@ class MQTTBackendLibreTiny final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c433804dd9..1a03c5329e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -749,13 +749,6 @@ void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&call this->on_disconnect_.add(std::move(callback_copy)); } -#if ASYNC_TCP_SSL_ENABLED -void MQTTClientComponent::add_ssl_fingerprint(const std::array &fingerprint) { - this->mqtt_backend_.setSecure(true); - this->mqtt_backend_.addServerFingerprint(fingerprint.data()); -} -#endif - MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // MQTTMessageTrigger diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 21edd53eda..127e4073b0 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -137,21 +137,6 @@ class MQTTClientComponent : public Component { bool is_discovery_enabled() const; bool is_discovery_ip_enabled() const; -#if ASYNC_TCP_SSL_ENABLED - /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. - * - * To use this feature you first have to globally enable the `ASYNC_TCP_SSL_ENABLED` define flag. - * This function can be called multiple times and any certificate that matches any of the provided fingerprints - * will match. Calling this method will also automatically disable all non-ssl connections. - * - * @warning This is *not* secure and *not* how SSL is usually done. You'll have to add - * a separate fingerprint for every certificate you use. Additionally, the hashing - * algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure. - * - * @param fingerprint The SSL fingerprint as a 20 value long std::array. - */ - void add_ssl_fingerprint(const std::array &fingerprint); -#endif #ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } diff --git a/esphome/const.py b/esphome/const.py index f72cbc8893..0179aa129e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -943,7 +943,6 @@ CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" -CONF_SSL_FINGERPRINTS = "ssl_fingerprints" CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 042df12d67..cbf78bd3f6 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,6 +1,5 @@ import contextlib from datetime import datetime -import hashlib import json import logging import ssl @@ -22,14 +21,12 @@ from esphome.const import ( CONF_PASSWORD, CONF_PORT, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME, ) -from esphome.core import CORE, EsphomeError +from esphome.core import EsphomeError from esphome.helpers import get_int_env, get_str_env -from esphome.log import AnsiFore, color from esphome.types import ConfigType from esphome.util import safe_print @@ -102,9 +99,7 @@ def prepare( elif username: client.username_pw_set(username, password) - if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( - CONF_CERTIFICATE_AUTHORITY - ): + if config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY): context = ssl.create_default_context( cadata=config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY) ) @@ -283,23 +278,3 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): client.publish(msg.topic, None, retain=True) return initialize(config, [topic], on_message, None, username, password, client_id) - - -# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py -def get_fingerprint(config): - addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT]) - _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) - try: - cert_pem = ssl.get_server_certificate(addr) - except OSError as err: - _LOGGER.error("Unable to connect to server: %s", err) - return 1 - cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) - - sha1 = hashlib.sha1(cert_der).hexdigest() - - safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}") - safe_print( - f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" - ) - return 0 From 6ff17fbf7c3d4b24ae0e20103c0828823d49b1dc Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:17:54 -1000 Subject: [PATCH 53/89] [epaper_spi] Fix color mapping for weact (#14134) --- esphome/components/epaper_spi/epaper_spi.h | 2 +- .../components/epaper_spi/epaper_weact_3c.cpp | 43 ++++++++++++------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index a8c2fe9b56..a743985518 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -76,7 +76,7 @@ class EPaperBase : public Display, static uint8_t color_to_bit(Color color) { // It's always a shade of gray. Map to BLACK or WHITE. // We split the luminance at a suitable point - if ((static_cast(color.r) + color.g + color.b) > 512) { + if ((color.r + color.g + color.b) >= 382) { return 1; } return 0; diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp index bd83105dd7..d4dac7076c 100644 --- a/esphome/components/epaper_spi/epaper_weact_3c.cpp +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -5,9 +5,24 @@ namespace esphome::epaper_spi { static constexpr const char *const TAG = "epaper_weact_3c"; +enum class BwrState : uint8_t { + BWR_BLACK, + BWR_WHITE, + BWR_RED, +}; + +static BwrState color_to_bwr(Color color) { + if (color.r > color.g + color.b && color.r > 127) { + return BwrState::BWR_RED; + } + if (color.r + color.g + color.b >= 382) { + return BwrState::BWR_WHITE; + } + return BwrState::BWR_BLACK; +} // SSD1680 3-color display notes: // - Buffer uses 1 bit per pixel, 8 pixels per byte -// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer first half (black_offset): Black/White plane (0=black, 1=white) // - Buffer second half (red_offset): Red plane (1=red, 0=no red) // - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) // - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) @@ -23,20 +38,20 @@ void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { // Use luminance threshold for B/W mapping // Split at halfway point (382 = (255*3)/2) - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + auto bwr = color_to_bwr(color); // Update black/white plane (first half of buffer) - if (is_white) { - // White pixel - clear bit in black plane - this->buffer_[pos] &= ~bit; - } else { - // Black pixel - set bit in black plane + if (bwr == BwrState::BWR_WHITE) { + // White pixel - set bit in black plane this->buffer_[pos] |= bit; + } else { + // Black pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; } // Update red plane (second half of buffer) // Red if red component is dominant (r > g+b) - if (color.r > color.g + color.b) { + if (bwr == BwrState::BWR_RED) { // Red pixel - set bit in red plane this->buffer_[red_offset + pos] |= bit; } else { @@ -53,21 +68,20 @@ void EPaperWeAct3C::fill(Color color) { const size_t half_buffer = this->buffer_length_ / 2u; // Use luminance threshold for B/W mapping - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; - bool is_red = color.r > color.g + color.b; + auto bits = color_to_bwr(color); // Fill both planes - if (is_white) { - // White - both planes = 0x00 + if (bits == BwrState::BWR_BLACK) { + // Black - both planes = 0x00 this->buffer_.fill(0x00); - } else if (is_red) { + } else if (bits == BwrState::BWR_RED) { // Red - black plane = 0x00, red plane = 0xFF for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0x00; for (size_t i = 0; i < half_buffer; i++) this->buffer_[half_buffer + i] = 0xFF; } else { - // Black - black plane = 0xFF, red plane = 0x00 + // White - black plane = 0xFF, red plane = 0x00 for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0xFF; for (size_t i = 0; i < half_buffer; i++) @@ -112,7 +126,6 @@ bool HOT EPaperWeAct3C::transfer_data() { ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); // Use a local buffer for SPI transfers - static constexpr size_t MAX_TRANSFER_SIZE = 128; uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; // First, send the RED buffer (0x26 = WRITE_COLOR) From 48ba007c22d7c542e9de8740aacb8c94ac438a10 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 21 Feb 2026 20:16:20 +0100 Subject: [PATCH 54/89] [nrf52] print line number after crash in logs (#14165) --- esphome/__main__.py | 19 ++++++++++--- esphome/components/nrf52/__init__.py | 40 ++++++++++++++++++++++++++++ tests/unit_tests/test_main.py | 6 +++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 488955f503..ffedb90bde 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -431,6 +431,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: return 1 _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) + process_stacktrace = None + + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + process_stacktrace = getattr(module, "process_stacktrace") + except AttributeError: + pass + backtrace_state = False ser = serial.Serial() ser.baudrate = baud_rate @@ -472,9 +480,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: ) safe_print(parser.parse_line(line, time_str)) - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) + if process_stacktrace: + backtrace_state = process_stacktrace( + config, line, backtrace_state + ) + else: + backtrace_state = platformio_api.process_stacktrace( + config, line, backtrace_state=backtrace_state + ) except serial.SerialException: _LOGGER.error("Serial port closed!") return 0 diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 95e3670124..a12d1db1ab 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio import logging from pathlib import Path +import re +import subprocess from esphome import pins import esphome.codegen as cg @@ -380,3 +382,41 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: asyncio.run(logger_connect(address)) return True return False + + +def _addr2line(addr2line: str, elf: Path, addr: str) -> str: + try: + result = subprocess.run( + [addr2line, "-e", elf, addr], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip().splitlines()[0] + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Running command failed: %s", err) + return "" + + +def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool: + if "Last crash:" in line: + return True + if backtrace_state: + match = re.search(r"PC=(0x[0-9a-fA-F]+)\s+LR=(0x[0-9a-fA-F]+)", line) + if match: + pc = match.group(1) + lr = match.group(2) + from esphome.analyze_memory.toolchain import find_tool + + addr2line = find_tool("addr2line") + if addr2line is None: + return False + elf = CORE.relative_pioenvs_path(CORE.name, "firmware.elf") + if not elf.exists(): + _LOGGER.warning("%s does not exists", elf) + return False + _LOGGER.error("=== CRASH ===") + _LOGGER.error("PC: %s", _addr2line(addr2line, elf, pc)) + _LOGGER.error("LR: %s", _addr2line(addr2line, elf, lr)) + + return False diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index c9aa446323..cef561c54b 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2951,6 +2951,7 @@ def test_run_miniterm_batches_lines_with_same_timestamp( mock_serial = MockSerial([chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -2989,6 +2990,7 @@ def test_run_miniterm_different_chunks_different_timestamps( mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3019,6 +3021,7 @@ def test_run_miniterm_handles_split_lines() -> None: mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3057,6 +3060,7 @@ def test_run_miniterm_backtrace_state_maintained() -> None: mock_serial = MockSerial([backtrace_chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3122,6 +3126,7 @@ def test_run_miniterm_handles_empty_reads( mock_serial = MockSerial([b"", chunk, b"", MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3194,6 +3199,7 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None: mock_serial = MockSerial([large_data_no_newline, final_line, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, From 9571a979eb7ff1d881a4a833a639ff54c861174e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:53:45 -0600 Subject: [PATCH 55/89] [ci] Suggest StringRef instead of std::string_view (#14183) --- script/ci-custom.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 231f587068..df819e0f04 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -494,6 +494,22 @@ def lint_no_byte_datatype(fname, match): ) +@lint_re_check( + r"(?:std\s*::\s*string_view|#include\s*)" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_std_string_view(fname, match): + return ( + f"{highlight('std::string_view')} is not allowed in ESPHome. " + f"It pulls in significant STL template machinery that bloats flash on " + f"resource-constrained embedded targets, does not work well with ArduinoJson, " + f"and duplicates functionality already provided by {highlight('StringRef')}.\n" + f"Please use {highlight('StringRef')} from {highlight('esphome/core/string_ref.h')} " + f"for non-owning string references, or {highlight('const char *')} for simple cases.\n" + f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)" + ) + + @lint_post_check def lint_constants_usage(): errs = [] From 5a07908dfa9ef499b20e8a53f23e5ea94b9eb682 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:20 -0600 Subject: [PATCH 56/89] [api] Fix build error when lambda returns StringRef in homeassistant.event data (#14187) --- esphome/components/api/homeassistant_service.h | 2 ++ tests/components/homeassistant/common.yaml | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 2322d96eef..340699e1a6 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -36,6 +36,8 @@ template class TemplatableStringValue : public TemplatableValue() {} diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 9c6cb71b8b..60e3defd49 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -90,6 +90,19 @@ text_sensor: id: ha_hello_world_text2 attribute: some_attribute +event: + - platform: template + name: Test Event + id: test_event + event_types: + - test_event_type + on_event: + - homeassistant.event: + event: esphome.test_event + data: + event_name: !lambda |- + return event_type; + time: - platform: homeassistant on_time: From e521522b388dce7066ade34d96869187e7c47ca6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:43 -0600 Subject: [PATCH 57/89] [haier] Fix uninitialized HonSettings causing API connection failures (#14188) --- esphome/components/haier/hon_climate.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a567ab1d89..608d5e7f21 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -29,10 +29,10 @@ enum class CleaningState : uint8_t { enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; struct HonSettings { - hon_protocol::VerticalSwingMode last_vertiacal_swing; - hon_protocol::HorizontalSwingMode last_horizontal_swing; - bool beeper_state; - bool quiet_mode_state; + hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER}; + hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER}; + bool beeper_state{true}; + bool quiet_mode_state{false}; }; class HonClimate : public HaierClimateBase { @@ -189,7 +189,7 @@ class HonClimate : public HaierClimateBase { int big_data_sensors_{0}; esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; - HonSettings settings_; + HonSettings settings_{}; ESPPreferenceObject hon_rtc_; SwitchState quiet_mode_state_{SwitchState::OFF}; }; From 6f198adb0c79bcc8e14cdb07cbf484bb23aa1e52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 14:29:50 -0600 Subject: [PATCH 58/89] [scheduler] Reduce lock acquisitions in process_defer_queue_ (#14107) --- esphome/core/scheduler.cpp | 23 +++++++++++ esphome/core/scheduler.h | 79 ++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e82efcc520..b810df7e1c 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -421,6 +421,29 @@ void Scheduler::full_cleanup_removed_items_() { this->to_remove_ = 0; } +#ifndef ESPHOME_THREAD_SINGLE +void Scheduler::compact_defer_queue_locked_() { + // Rare case: new items were added during processing - compact the vector + // This only happens when: + // 1. A deferred callback calls defer() again, or + // 2. Another thread calls defer() while we're processing + // + // Move unprocessed items (added during this loop) to the front for next iteration + // + // SAFETY: Compacted items may include cancelled items (marked for removal via + // cancel_item_locked_() during execution). This is safe because should_skip_item_() + // checks is_item_removed_() before executing, so cancelled items will be skipped + // and recycled on the next loop iteration. + size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; + for (size_t i = 0; i < remaining; i++) { + this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); + } + // Use erase() instead of resize() to avoid instantiating _M_default_append + // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. + this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); +} +#endif /* not ESPHOME_THREAD_SINGLE */ + void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE this->process_defer_queue_(now); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index afe11aaca6..041c6f4687 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -387,41 +387,46 @@ class Scheduler { // No lock needed: single consumer (main loop), stale read just means we process less this iteration size_t defer_queue_end = this->defer_queue_.size(); + // Fast path: nothing to process, avoid lock entirely. + // Safe without lock: single consumer (main loop) reads front_, and a stale size() read + // from a concurrent push can only make us see fewer items — they'll be processed next loop. + if (this->defer_queue_front_ >= defer_queue_end) + return; + + // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), + // recycle each item after re-acquiring the lock for the next iteration (N+1 total). + // The lock is held across: recycle → loop condition → move-out, then released for execution. + std::unique_ptr item; + + this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { - std::unique_ptr item; - { - LockGuard lock(this->lock_); - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: - // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) - // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = std::move(this->defer_queue_[this->defer_queue_front_]); - this->defer_queue_front_++; - } + // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. + // This is intentional and safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 3. The lock protects concurrent access, but the nullptr remains until cleanup + item = std::move(this->defer_queue_[this->defer_queue_front_]); + this->defer_queue_front_++; + this->lock_.unlock(); // Execute callback without holding lock to prevent deadlocks // if the callback tries to call defer() again if (!this->should_skip_item_(item.get())) { now = this->execute_item_(item.get(), now); } - // Recycle the defer item after execution - { - LockGuard lock(this->lock_); - this->recycle_item_main_loop_(std::move(item)); - } - } - // If we've consumed all items up to the snapshot point, clean up the dead space - // Single consumer (main loop), so no lock needed for this check - if (this->defer_queue_front_ >= defer_queue_end) { - LockGuard lock(this->lock_); - this->cleanup_defer_queue_locked_(); + this->lock_.lock(); + this->recycle_item_main_loop_(std::move(item)); } + // Clean up the queue (lock already held from last recycle or initial acquisition) + this->cleanup_defer_queue_locked_(); + this->lock_.unlock(); } - // Helper to cleanup defer_queue_ after processing + // Helper to cleanup defer_queue_ after processing. + // Keeps the common clear() path inline, outlines the rare compaction to keep + // cold code out of the hot instruction cache lines. // IMPORTANT: Caller must hold the scheduler lock before calling this function. inline void cleanup_defer_queue_locked_() { // Check if new items were added by producers during processing @@ -429,27 +434,17 @@ class Scheduler { // Common case: no new items - clear everything this->defer_queue_.clear(); } else { - // Rare case: new items were added during processing - compact the vector - // This only happens when: - // 1. A deferred callback calls defer() again, or - // 2. Another thread calls defer() while we're processing - // - // Move unprocessed items (added during this loop) to the front for next iteration - // - // SAFETY: Compacted items may include cancelled items (marked for removal via - // cancel_item_locked_() during execution). This is safe because should_skip_item_() - // checks is_item_removed_() before executing, so cancelled items will be skipped - // and recycled on the next loop iteration. - size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; - for (size_t i = 0; i < remaining; i++) { - this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); - } - // Use erase() instead of resize() to avoid instantiating _M_default_append - // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. - this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); + // Rare case: new items were added during processing - outlined to keep cold code + // out of the hot instruction cache lines + this->compact_defer_queue_locked_(); } this->defer_queue_front_ = 0; } + + // Cold path for compacting defer_queue_ when new items were added during processing. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must not be inlined - rare path, outlined to keep it out of the hot instruction cache lines. + void __attribute__((noinline)) compact_defer_queue_locked_(); #endif /* not ESPHOME_THREAD_SINGLE */ // Helper to check if item is marked for removal (platform-specific) From 462ac2956312f21e506b1e2e13a6960e18d0d813 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 17:29:41 -0600 Subject: [PATCH 59/89] [scheduler] Use relaxed memory ordering for atomic reads under lock (#14140) --- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b810df7e1c..36b65f6ff7 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -406,7 +406,7 @@ void Scheduler::full_cleanup_removed_items_() { // 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 (!is_item_removed_locked_(this->items_[read].get())) { if (write != read) { this->items_[write] = std::move(this->items_[read]); } @@ -531,7 +531,7 @@ void HOT Scheduler::call(uint32_t now) { // Multi-threaded platforms without atomics: must take lock to safely read remove flag { LockGuard guard{this->lock_}; - if (is_item_removed_(item.get())) { + if (is_item_removed_locked_(item.get())) { this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; @@ -568,7 +568,7 @@ void HOT Scheduler::call(uint32_t now) { // during the function call and know if we were cancelled. auto executed_item = this->pop_raw_locked_(); - if (executed_item->remove) { + if (this->is_item_removed_locked_(executed_item.get())) { // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; this->recycle_item_main_loop_(std::move(executed_item)); @@ -595,7 +595,7 @@ void HOT Scheduler::call(uint32_t now) { void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { - if (is_item_removed_(it.get())) { + if (is_item_removed_locked_(it.get())) { // Recycle cancelled items this->recycle_item_main_loop_(std::move(it)); continue; @@ -628,7 +628,7 @@ size_t HOT Scheduler::cleanup_() { LockGuard guard{this->lock_}; while (!this->items_.empty()) { auto &item = this->items_[0]; - if (!item->remove) + if (!this->is_item_removed_locked_(item.get())) break; this->to_remove_--; this->recycle_item_main_loop_(this->pop_raw_locked_()); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 041c6f4687..384d76b6b0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -314,8 +314,8 @@ class Scheduler { // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) return false; - if (item->component != component || item->type != type || (skip_removed && item->remove) || - (match_retry && !item->is_retry)) { + if (item->component != component || item->type != type || + (skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) { return false; } // Name type must match @@ -463,6 +463,18 @@ class Scheduler { #endif } + // Helper to check if item is marked for removal when lock is already held. + // Uses relaxed ordering since the mutex provides all necessary synchronization. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + bool is_item_removed_locked_(SchedulerItem *item) const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Lock already held - relaxed is sufficient, mutex provides ordering + return item->remove.load(std::memory_order_relaxed); +#else + return item->remove; +#endif + } + // Helper to set item removal flag (platform-specific) // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this // function. Uses memory_order_release when setting to true (for cancellation synchronization), @@ -519,7 +531,7 @@ class Scheduler { // it will iterate over these nullptr items. This check prevents crashes. if (!item) continue; - if (is_item_removed_(item.get()) && + if (this->is_item_removed_locked_(item.get()) && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, match_retry, /* skip_removed= */ false)) { return true; From a4682615234ac61ff7825e2a922807b11e6772fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:26 -0600 Subject: [PATCH 60/89] [scheduler] De-template and consolidate scheduler helper functions (#14164) --- esphome/core/scheduler.cpp | 14 +++++++++---- esphome/core/scheduler.h | 41 +++++++------------------------------- 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 36b65f6ff7..e4e0751e10 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -119,10 +119,16 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { // 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); + for (auto *container : {&this->items_, &this->to_add_}) { + for (auto &item : *container) { + if (item && this->is_item_removed_locked_(item.get()) && + this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, + /* match_retry= */ true, /* skip_removed= */ false)) { + return true; + } + } + } + return false; } // Common implementation for both timeout and interval diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 384d76b6b0..16b0ded312 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -308,8 +308,8 @@ class Scheduler { SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. - // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and - // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check + // provides defense-in-depth: helper // functions should be safe regardless of caller behavior. // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) @@ -403,8 +403,7 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -497,19 +496,16 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - template - size_t mark_matching_items_removed_locked_(Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, - bool match_retry) { + size_t mark_matching_items_removed_locked_(std::vector> &container, + Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the // vector as nullptr until cleanup. Even though this function is called with lock held, // the vector can still contain nullptr items from the processing loop. This check prevents crashes. - if (!item) - continue; - if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + if (item && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { this->set_item_removed_(item.get(), true); count++; } @@ -517,29 +513,6 @@ class Scheduler { return count; } - // Template helper to check if any item in a container matches our criteria - // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id - // IMPORTANT: Must be called with scheduler lock held - template - bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, - bool match_retry) const { - for (const auto &item : container) { - // Skip nullptr items (can happen in defer_queue_ when items are being processed) - // The defer_queue_ uses index-based processing: items are std::moved out but left in the - // vector as nullptr until cleanup. If this function is called during defer queue processing, - // it will iterate over these nullptr items. This check prevents crashes. - if (!item) - continue; - if (this->is_item_removed_locked_(item.get()) && - this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, - match_retry, /* skip_removed= */ false)) { - return true; - } - } - return false; - } - Mutex lock_; std::vector> items_; std::vector> to_add_; From d5c9c56fdfcdd0112e1913f05f84e297908460a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:43 -0600 Subject: [PATCH 61/89] [platformio] Add exponential backoff and session reset to download retries (#14191) --- esphome/platformio_api.py | 41 ++++- tests/unit_tests/test_platformio_api.py | 196 +++++++++++++++++++++++- 2 files changed, 231 insertions(+), 6 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index d42f89d029..5d4065207f 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,6 +5,7 @@ import os from pathlib import Path import re import subprocess +import time from typing import Any from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE @@ -44,31 +45,61 @@ def patch_structhash(): def patch_file_downloader(): - """Patch PlatformIO's FileDownloader to retry on PackageException errors.""" + """Patch PlatformIO's FileDownloader to retry on PackageException errors. + + PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry + for 502/503 errors. We add retries with exponential backoff and close the + session between attempts to force a fresh TCP connection, which may route + to a different CDN edge node. + """ from platformio.package.download import FileDownloader from platformio.package.exception import PackageException + if getattr(FileDownloader.__init__, "_esphome_patched", False): + return + original_init = FileDownloader.__init__ def patched_init(self, *args: Any, **kwargs: Any) -> None: - max_retries = 3 + max_retries = 5 for attempt in range(max_retries): try: - return original_init(self, *args, **kwargs) + original_init(self, *args, **kwargs) + return except PackageException as e: if attempt < max_retries - 1: + # Exponential backoff: 2, 4, 8, 16 seconds + delay = 2 ** (attempt + 1) _LOGGER.warning( - "Package download failed: %s. Retrying... (attempt %d/%d)", + "Package download failed: %s. " + "Retrying in %d seconds... (attempt %d/%d)", str(e), + delay, attempt + 1, max_retries, ) + # Close the response and session to free resources + # and force a new TCP connection on retry, which may + # route to a different CDN edge node + # pylint: disable=protected-access,broad-except + try: + if ( + hasattr(self, "_http_response") + and self._http_response is not None + ): + self._http_response.close() + if hasattr(self, "_http_session"): + self._http_session.close() + except Exception: + pass + # pylint: enable=protected-access,broad-except + time.sleep(delay) else: # Final attempt - re-raise raise - return None + patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access FileDownloader.__init__ = patched_init diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 4d7b635e59..1686144277 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -6,7 +6,7 @@ import os from pathlib import Path import shutil from types import SimpleNamespace -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -673,6 +673,200 @@ def test_process_stacktrace_bad_alloc( assert state is False +def test_patch_file_downloader_succeeds_first_try() -> None: + """Test patch_file_downloader succeeds on first attempt.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + original_init = MagicMock() + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": original_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + original_init.assert_called_once() + + +def test_patch_file_downloader_retries_on_failure() -> None: + """Test patch_file_downloader retries with backoff on PackageException.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def failing_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise mock_exception_cls(f"502 error attempt {call_count}") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have been called 3 times (2 failures + 1 success) + assert call_count == 3 + + # Should have slept with exponential backoff: 2s, 4s + assert mock_sleep.call_count == 2 + mock_sleep.assert_any_call(2) + mock_sleep.assert_any_call(4) + + +def test_patch_file_downloader_raises_after_max_retries() -> None: + """Test patch_file_downloader raises after exhausting all retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + + def always_failing_init(self, *args, **kwargs): + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": always_failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + with pytest.raises(mock_exception_cls, match="502 error"): + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have slept 4 times (before attempts 2-5), not on final attempt + assert mock_sleep.call_count == 4 + mock_sleep.assert_has_calls([call(2), call(4), call(8), call(16)]) + + +def test_patch_file_downloader_closes_session_and_response_between_retries() -> None: + """Test patch_file_downloader closes HTTP session and response between retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + mock_session = MagicMock() + mock_response = MagicMock() + call_count = 0 + + def failing_init_with_session(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + self._http_session = mock_session + self._http_response = mock_response + if call_count < 2: + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", + (), + {"__init__": failing_init_with_session}, + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep"), + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Both response and session should have been closed between retries + mock_response.close.assert_called_once() + mock_session.close.assert_called_once() + + +def test_patch_file_downloader_idempotent() -> None: + """Test patch_file_downloader does not stack wrappers when called multiple times.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def counting_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": counting_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + # Patch multiple times + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should only be called once, not 3 times from stacked wrappers + assert call_count == 1 + + def test_platformio_log_filter_allows_non_platformio_messages() -> None: """Test that non-platformio logger messages are allowed through.""" log_filter = platformio_api.PlatformioLogFilter() From 49e4ae54beb20f7de57a24274c851c13a382d0e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 23:22:59 -0600 Subject: [PATCH 62/89] [bme68x_bsec2] Fix compilation on ESP32 Arduino (#14194) --- esphome/components/bme68x_bsec2/__init__.py | 5 ++++- tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index e421efb2d6..4200b2f0b8 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -178,8 +178,11 @@ async def to_code_base(config): bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) - # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library + # The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and + # SPI.h in their source files, so these libraries must be available even though + # ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones. if core.CORE.using_arduino: + cg.add_library("Wire", None) cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml From e013b4867519c44b570fd6cf05872a7324845009 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 22 Feb 2026 06:44:06 +0100 Subject: [PATCH 63/89] [nextion] Add error log for failed HTTP status during TFT upload (#14190) --- esphome/components/nextion/nextion_upload_arduino.cpp | 1 + esphome/components/nextion/nextion_upload_esp32.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 220c75f9d3..a433eff883 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -220,6 +220,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } if (code != 200 and code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", code); return this->upload_end_(false); } diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index c4e6ff7182..46352afd75 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -238,6 +238,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); return this->upload_end_(false); } From 1753074eef3ea117dff2cb02995330227b242379 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:01:07 -0600 Subject: [PATCH 64/89] [web_server_base] Remove unnecessary Component inheritance and modernize (#14204) --- esphome/components/web_server_base/__init__.py | 3 +-- .../web_server_base/web_server_base.cpp | 18 ++---------------- .../web_server_base/web_server_base.h | 18 +++++++----------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 7986ac964d..183b907ae6 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -20,7 +20,7 @@ def AUTO_LOAD(): web_server_base_ns = cg.esphome_ns.namespace("web_server_base") -WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component) +WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" CONFIG_SCHEMA = cv.Schema( @@ -33,7 +33,6 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.WEB_SERVER_BASE) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}")) if CORE.is_esp32: diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 6e7097338c..dbbcd10d8d 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,19 +1,11 @@ #include "web_server_base.h" #ifdef USE_NETWORK -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" -namespace esphome { -namespace web_server_base { - -static const char *const TAG = "web_server_base"; +namespace esphome::web_server_base { WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void WebServerBase::add_handler(AsyncWebHandler *handler) { - // remove all handlers - #ifdef USE_WEBSERVER_AUTH if (!credentials_.username.empty()) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); @@ -25,11 +17,5 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { } } -float WebServerBase::get_setup_priority() const { - // Before WiFi (captive portal) - return setup_priority::WIFI + 2.0f; -} - -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 0c25467f1b..54421c851e 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,11 +1,9 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK -#include #include #include -#include "esphome/core/component.h" #include "esphome/core/progmem.h" #if USE_ESP32 @@ -21,8 +19,7 @@ using PlatformString = std::string; using PlatformString = String; #endif -namespace esphome { -namespace web_server_base { +namespace esphome::web_server_base { class WebServerBase; extern WebServerBase *global_web_server_base; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -91,14 +88,14 @@ class AuthMiddlewareHandler : public MiddlewareHandler { } // namespace internal -class WebServerBase : public Component { +class WebServerBase { public: void init() { if (this->initialized_) { this->initialized_++; return; } - this->server_ = std::make_unique(this->port_); + this->server_ = new AsyncWebServer(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. // NOTE: Currently 1 header. If more are added, update in __init__.py: // cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) @@ -113,11 +110,11 @@ class WebServerBase : public Component { void deinit() { this->initialized_--; if (this->initialized_ == 0) { + delete this->server_; this->server_ = nullptr; } } - AsyncWebServer *get_server() const { return this->server_.get(); } - float get_setup_priority() const override; + AsyncWebServer *get_server() const { return this->server_; } #ifdef USE_WEBSERVER_AUTH void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); } @@ -132,13 +129,12 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::unique_ptr server_{nullptr}; + AsyncWebServer *server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; #endif }; -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif From 509f06afac19fc54b3fae2a67802771312b91133 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:56:09 -0600 Subject: [PATCH 65/89] [network] Improve IPAddress::str() deprecation warning with usage example (#14195) --- esphome/components/network/ip_address.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d0ac8164af..b2a2c563e2 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -61,7 +61,9 @@ struct IPAddress { IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); @@ -150,7 +152,9 @@ struct IPAddress { bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); From ede2da2fbc167814c5598adb6d866c239a0772c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:57:23 -0600 Subject: [PATCH 66/89] [core] Conditionally compile get_loop_priority with USE_LOOP_PRIORITY (#14210) --- esphome/components/ch422g/ch422g.cpp | 2 ++ esphome/components/ch422g/ch422g.h | 2 ++ esphome/components/ch423/ch423.cpp | 2 ++ esphome/components/ch423/ch423.h | 2 ++ esphome/components/deep_sleep/deep_sleep_component.cpp | 2 ++ esphome/components/deep_sleep/deep_sleep_component.h | 2 ++ esphome/components/pca9554/pca9554.cpp | 2 ++ esphome/components/pca9554/pca9554.h | 2 ++ esphome/components/pcf8574/pcf8574.cpp | 2 ++ esphome/components/pcf8574/pcf8574.h | 2 ++ esphome/components/status_led/light/status_led_light.h | 2 ++ esphome/components/status_led/status_led.cpp | 2 ++ esphome/components/status_led/status_led.h | 2 ++ esphome/components/wifi/__init__.py | 7 +++++++ esphome/components/wifi/wifi_component.cpp | 2 ++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 2 ++ esphome/core/component.h | 3 +++ esphome/core/defines.h | 1 + 20 files changed, 45 insertions(+) diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index eef95b9ba2..5f5e848c76 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -124,9 +124,11 @@ bool CH422GComponent::write_outputs_() { float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 8ed63db90a..1b96568209 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,7 +23,9 @@ class CH422GComponent : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp index 4abbbe7adf..805d8df877 100644 --- a/esphome/components/ch423/ch423.cpp +++ b/esphome/components/ch423/ch423.cpp @@ -129,9 +129,11 @@ bool CH423Component::write_outputs_() { float CH423Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h index 7adc7de6a1..d85648a8f9 100644 --- a/esphome/components/ch423/ch423.h +++ b/esphome/components/ch423/ch423.h @@ -22,7 +22,9 @@ class CH423Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 8066b411ff..0511518419 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -40,9 +40,11 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } +#ifdef USE_LOOP_PRIORITY float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } +#endif void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 3e6eda2257..1998b815f3 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -113,7 +113,9 @@ class DeepSleepComponent : public Component { void setup() override; void dump_config() override; void loop() override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif float get_setup_priority() const override; /// Helper to enter deep sleep mode diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index c574ce6593..d94767ef07 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -122,8 +122,10 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index bf752e50c9..6dd15ccb4b 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -23,7 +23,9 @@ class PCA9554Component : public Component, float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index b7d3848f0e..fa9496e7e4 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -99,8 +99,10 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 5203030142..23bccc26c9 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -26,7 +26,9 @@ class PCF8574Component : public Component, void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index bfa144526a..a5c98d90d4 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -30,7 +30,9 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override { return 50.0f; } +#endif protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index 344c1e3070..93a8d4b38e 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -28,7 +28,9 @@ void StatusLED::loop() { } } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float StatusLED::get_loop_priority() const { return 50.0f; } +#endif } // namespace status_led } // namespace esphome diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index 490557f3e7..f262eb260c 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -14,7 +14,9 @@ class StatusLED : public Component { void dump_config() override; void loop() override; float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif protected: GPIOPin *pin_; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 540d0a0ab1..8c1deb6217 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -536,6 +536,13 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_rp2040: cg.add_library("WiFi", None) + # RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart + # mDNS when the network interface reconnects. However, this callback is disabled + # in the arduino-pico framework. As a workaround, we block component setup until + # WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an + # active connection. This define enables the loop priority sorting infrastructure + # used during the setup blocking phase. + cg.add_define("USE_LOOP_PRIORITY") if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b3060c7c3..d5d0419395 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -968,9 +968,11 @@ void WiFiComponent::set_ap(const WiFiAP &ap) { } #endif // USE_WIFI_AP +#ifdef USE_LOOP_PRIORITY float WiFiComponent::get_loop_priority() const { return 10.0f; // before other loop components } +#endif void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 5f903e092a..984930c80c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -458,7 +458,9 @@ class WiFiComponent : public Component { void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif /// Reconnect WiFi if required. void loop() override; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b216233f9b..c6597897dc 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -109,9 +109,11 @@ void Application::setup() { if (component->can_proceed()) continue; +#ifdef USE_LOOP_PRIORITY // Sort components 0 through i by loop priority insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( this->components_.begin(), this->components_.begin() + i + 1); +#endif do { uint8_t new_app_state = STATUS_LED_WARNING; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b458ea2a84..b4a19a0776 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -85,7 +85,9 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +#ifdef USE_LOOP_PRIORITY float Component::get_loop_priority() const { return 0.0f; } +#endif float Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/core/component.h b/esphome/core/component.h index b99641a275..7ea9fdf3b3 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -117,7 +118,9 @@ class Component { * * @return The loop priority of this component */ +#ifdef USE_LOOP_PRIORITY virtual float get_loop_priority() const; +#endif void call(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5109dd36f4..02335e2745 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -312,6 +312,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_LOOP_PRIORITY #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC From ee1f52132573ab5498eaa566d3289d1103a4a186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 16:59:54 -0600 Subject: [PATCH 67/89] [http_request] Replace std::list
with std::vector in perform() chain (#14027) --- .../components/http_request/http_request.h | 89 +++++++++++++++---- .../http_request/http_request_arduino.cpp | 2 +- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 2 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 2 +- .../http_request/http_request_idf.h | 2 +- .../components/online_image/online_image.cpp | 2 +- 8 files changed, 79 insertions(+), 24 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 2b2d05c63f..8bdea470b5 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -330,38 +330,72 @@ class HttpRequestComponent : public Component { void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } - std::shared_ptr get(const std::string &url) { return this->start(url, "GET", "", {}); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + std::shared_ptr get(const std::string &url) { + return this->start(url, "GET", "", std::vector
{}); + } + std::shared_ptr get(const std::string &url, const std::vector
&request_headers) { return this->start(url, "GET", "", request_headers); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers, + std::shared_ptr get(const std::string &url, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { - return this->start(url, "POST", body, {}); + return this->start(url, "POST", body, std::vector
{}); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers) { + const std::vector
&request_headers) { return this->start(url, "POST", body, request_headers); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers, + const std::vector &collect_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers, + const std::vector &collect_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers) { + // Call perform() directly to avoid ambiguity with the deprecated overloads + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - // Call perform() directly to avoid ambiguity with the std::set overload - return this->perform(url, method, body, request_headers, {}); + return this->start(url, method, body, std::vector
(request_headers.begin(), request_headers.end())); } // Remove before 2027.1.0 ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::set &collect_headers) { std::vector lower; lower.reserve(collect_headers.size()); @@ -371,15 +405,39 @@ class HttpRequestComponent : public Component { return this->perform(url, method, body, request_headers, lower); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list, and collect_headers as " + "std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), lower); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), + lower_case_collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, - const std::string &body, const std::list
&request_headers, + const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; @@ -436,13 +494,10 @@ template class HttpRequestSendAction : public Action { auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); body = json::build_json(f); } - std::list
request_headers; - for (const auto &item : this->request_headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - request_headers.push_back(header); + std::vector
request_headers; + request_headers.reserve(this->request_headers_.size()); + for (const auto &[key, val] : this->request_headers_) { + request_headers.push_back({key, val.value(x...)}); } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 3f60b76b58..56b51e8b5a 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -26,7 +26,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index dbd61de364..d5ce5c0ff3 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -49,7 +49,7 @@ class HttpContainerArduino : public HttpContainer { class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; }; diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 714a73fc31..60ab4d68a0 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -18,7 +18,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 79f5b7e817..52be0e8a16 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -19,7 +19,7 @@ class HttpContainerHost : public HttpContainer { class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 0921c50b9f..dda61e2400 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -54,7 +54,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 9206ba6f5d..9ed1a97b1a 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -37,7 +37,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 6f5b82116d..da866599c9 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -49,7 +49,7 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); - std::list headers; + std::vector headers; // Add caching headers if we have them if (!this->etag_.empty()) { From 5fddce6638f47c296e1267bbeeadaf677277d635 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:02:05 -0600 Subject: [PATCH 68/89] [logger] Make tx_buffer_ compile-time sized (#14205) --- esphome/components/logger/__init__.py | 3 ++- esphome/components/logger/logger.cpp | 7 ++----- esphome/components/logger/logger.h | 9 +++++---- esphome/components/logger/logger_esp32.cpp | 6 +++--- esphome/components/logger/logger_host.cpp | 4 ++-- esphome/core/defines.h | 1 + tests/dummy_main.cpp | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index c8f3c52911..264197c175 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -329,10 +329,11 @@ async def to_code(config): level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + tx_buffer_size = config[CONF_TX_BUFFER_SIZE] + cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) log = cg.new_Pvariable( config[CONF_ID], baud_rate, - config[CONF_TX_BUFFER_SIZE], ) if CORE.is_esp32: cg.add(log.create_pthread_key()) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 22a95e4835..497809cd2e 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,10 +152,7 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } -Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { - // add 1 to buffer size for null terminator - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; +Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) @@ -196,7 +193,7 @@ void Logger::process_messages_() { 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_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; 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 diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8bf1edebb8..90722ee79c 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,7 +143,7 @@ enum UARTSelection : uint8_t { */ class Logger : public Component { public: - explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); + explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif @@ -281,7 +281,7 @@ class Logger : public Component { 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, const char *thread_name) { RecursionGuard guard(recursion_guard); - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_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); @@ -312,7 +312,6 @@ class Logger : public Component { // Group 4-byte aligned members first uint32_t baud_rate_; - char *tx_buffer_{nullptr}; #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *hw_serial_{nullptr}; #endif @@ -354,7 +353,6 @@ class Logger : public Component { #endif // Group smaller types together at the end - 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) UARTSelection uart_{UART_SELECTION_UART0}; @@ -371,6 +369,9 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif + // Large buffer placed last to keep frequently-accessed member offsets small + char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator + // --- get_thread_name_ overloads (per-platform) --- #if defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 9d64771aec..d6ad77ff4f 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -89,16 +89,16 @@ void Logger::pre_setup() { switch (this->uart_) { case UART_SELECTION_UART0: this->uart_num_ = UART_NUM_0; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; case UART_SELECTION_UART1: this->uart_num_ = UART_NUM_1; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #ifdef USE_ESP32_VARIANT_ESP32 case UART_SELECTION_UART2: this->uart_num_ = UART_NUM_2; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #endif #ifdef USE_LOGGER_USB_CDC diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index be12b6df6a..fe094f6e9e 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -5,8 +5,8 @@ namespace esphome::logger { 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]; + static constexpr size_t HEADROOM = 128; // Extra space for ANSI codes, newline, etc. + char buffer[TIMESTAMP_LEN + ESPHOME_LOGGER_TX_BUFFER_SIZE + HEADROOM]; time_t rawtime; time(&rawtime); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 02335e2745..1128df94f0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -21,6 +21,7 @@ // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define ESPHOME_LOGGER_TX_BUFFER_SIZE 512 #define USE_LOG_LISTENERS #define ESPHOME_LOG_MAX_LISTENERS 8 diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index e6fe733807..52f1fbd319 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -13,7 +13,7 @@ using namespace esphome; void setup() { App.pre_setup("livingroom", "LivingRoom", false); - auto *log = new logger::Logger(115200, 512); // NOLINT + auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component(log); From b539a5aa51e08db837d30be5310b917fa61f631b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:07:56 -0600 Subject: [PATCH 69/89] [water_heater] Fix device_id missing from state responses (#14212) --- esphome/components/api/api_connection.cpp | 3 +- .../fixtures/device_id_in_state.yaml | 115 ++++++++++++ tests/integration/test_device_id_in_state.py | 173 ++++++++++++------ 3 files changed, 232 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9fc263abbd..7bc9c45c05 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1346,9 +1346,8 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_low = wh->get_target_temperature_low(); resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); diff --git a/tests/integration/fixtures/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml index c8548617b8..a5dfb7ee45 100644 --- a/tests/integration/fixtures/device_id_in_state.yaml +++ b/tests/integration/fixtures/device_id_in_state.yaml @@ -46,6 +46,7 @@ sensor: binary_sensor: - platform: template + id: motion_detected name: Motion Detected device_id: motion_sensor lambda: return true; @@ -82,3 +83,117 @@ output: write_action: - lambda: |- ESP_LOGD("test", "Light output: %d", state); + +cover: + - platform: template + name: Garage Door + device_id: motion_sensor + optimistic: true + +fan: + - platform: template + name: Ceiling Fan + device_id: humidity_monitor + speed_count: 3 + has_oscillating: false + has_direction: false + +lock: + - platform: template + name: Front Door Lock + device_id: motion_sensor + optimistic: true + +number: + - platform: template + name: Target Temperature + device_id: temperature_monitor + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: Mode Select + device_id: humidity_monitor + optimistic: true + options: + - "Auto" + - "Manual" + +text: + - platform: template + name: Device Label + device_id: temperature_monitor + optimistic: true + mode: text + +valve: + - platform: template + name: Water Valve + device_id: humidity_monitor + optimistic: true + +globals: + - id: global_away + type: bool + initial_value: "false" + - id: global_is_on + type: bool + initial_value: "true" + +water_heater: + - platform: template + name: Test Boiler + device_id: temperature_monitor + 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);" + supported_modes: + - "off" + - electric + visual: + min_temperature: 30.0 + max_temperature: 85.0 + target_temperature_step: 0.5 + set_action: + - lambda: |- + ESP_LOGD("test", "Water heater set"); + +alarm_control_panel: + - platform: template + name: House Alarm + device_id: motion_sensor + codes: + - "1234" + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: motion_detected + +datetime: + - platform: template + name: Schedule Date + device_id: temperature_monitor + type: date + optimistic: true + - platform: template + name: Schedule Time + device_id: humidity_monitor + type: time + optimistic: true + - platform: template + name: Schedule DateTime + device_id: motion_sensor + type: datetime + optimistic: true + +event: + - platform: template + name: Doorbell + device_id: motion_sensor + event_types: + - "press" + - "double_press" diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index 51088bcbf7..48de94a85a 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -4,11 +4,80 @@ from __future__ import annotations import asyncio -from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState +from aioesphomeapi import ( + AlarmControlPanelEntityState, + BinarySensorState, + CoverState, + DateState, + DateTimeState, + EntityState, + FanState, + LightState, + LockEntityState, + NumberState, + SelectState, + SensorState, + SwitchState, + TextSensorState, + TextState, + TimeState, + ValveState, + WaterHeaterState, +) import pytest from .types import APIClientConnectedFactory, RunCompiledFunction +# Mapping of entity name to device name for all entities with device_id +ENTITY_TO_DEVICE = { + # Original entities + "Temperature": "Temperature Monitor", + "Humidity": "Humidity Monitor", + "Motion Detected": "Motion Sensor", + "Temperature Monitor Power": "Temperature Monitor", + "Temperature Status": "Temperature Monitor", + "Motion Light": "Motion Sensor", + # New entity types + "Garage Door": "Motion Sensor", + "Ceiling Fan": "Humidity Monitor", + "Front Door Lock": "Motion Sensor", + "Target Temperature": "Temperature Monitor", + "Mode Select": "Humidity Monitor", + "Device Label": "Temperature Monitor", + "Water Valve": "Humidity Monitor", + "Test Boiler": "Temperature Monitor", + "House Alarm": "Motion Sensor", + "Schedule Date": "Temperature Monitor", + "Schedule Time": "Humidity Monitor", + "Schedule DateTime": "Motion Sensor", + "Doorbell": "Motion Sensor", +} + +# Entities without device_id (should have device_id 0) +NO_DEVICE_ENTITIES = {"No Device Sensor"} + +# State types that should have non-zero device_id, mapped by their aioesphomeapi class +EXPECTED_STATE_TYPES = [ + (SensorState, "sensor"), + (BinarySensorState, "binary_sensor"), + (SwitchState, "switch"), + (TextSensorState, "text_sensor"), + (LightState, "light"), + (CoverState, "cover"), + (FanState, "fan"), + (LockEntityState, "lock"), + (NumberState, "number"), + (SelectState, "select"), + (TextState, "text"), + (ValveState, "valve"), + (WaterHeaterState, "water_heater"), + (AlarmControlPanelEntityState, "alarm_control_panel"), + (DateState, "date"), + (TimeState, "time"), + (DateTimeState, "datetime"), + # Event is stateless (no initial state sent on subscribe) +] + @pytest.mark.asyncio async def test_device_id_in_state( @@ -40,34 +109,35 @@ async def test_device_id_in_state( entity_device_mapping: dict[int, int] = {} for entity in all_entities: - # All entities have name and key attributes - if entity.name == "Temperature": - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Humidity": - entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] - elif entity.name == "Motion Detected": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name in {"Temperature Monitor Power", "Temperature Status"}: - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Motion Light": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "No Device Sensor": - # Entity without device_id should have device_id 0 + if entity.name in ENTITY_TO_DEVICE: + expected_device = ENTITY_TO_DEVICE[entity.name] + entity_device_mapping[entity.key] = device_ids[expected_device] + elif entity.name in NO_DEVICE_ENTITIES: entity_device_mapping[entity.key] = 0 - assert len(entity_device_mapping) >= 6, ( - f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}" + expected_count = len(ENTITY_TO_DEVICE) + len(NO_DEVICE_ENTITIES) + assert len(entity_device_mapping) >= expected_count, ( + f"Expected at least {expected_count} mapped entities, " + f"got {len(entity_device_mapping)}. " + f"Missing: {set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES - {e.name for e in all_entities}}" + ) + + # Subscribe to states and wait for all mapped entities + # Event entities are stateless (no initial state on subscribe), + # so exclude them from the expected count + stateless_keys = {e.key for e in all_entities if e.name == "Doorbell"} + stateful_count = len(entity_device_mapping) - len( + stateless_keys & entity_device_mapping.keys() ) - # Subscribe to states loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} states_future: asyncio.Future[bool] = loop.create_future() def on_state(state: EntityState) -> None: - states[state.key] = state - # Check if we have states for all mapped entities - if len(states) >= len(entity_device_mapping) and not states_future.done(): + if state.key in entity_device_mapping: + states[state.key] = state + if len(states) >= stateful_count and not states_future.done(): states_future.set_result(True) client.subscribe_states(on_state) @@ -76,9 +146,16 @@ async def test_device_id_in_state( try: await asyncio.wait_for(states_future, timeout=10.0) except TimeoutError: + received_names = {e.name for e in all_entities if e.key in states} + missing_names = ( + (set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES) + - received_names + - {"Doorbell"} + ) pytest.fail( f"Did not receive all entity states within 10 seconds. " - f"Received {len(states)} states, expected {len(entity_device_mapping)}" + f"Received {len(states)} states. " + f"Missing: {missing_names}" ) # Verify each state has the correct device_id @@ -86,51 +163,33 @@ async def test_device_id_in_state( for key, expected_device_id in entity_device_mapping.items(): if key in states: state = states[key] + entity_name = next( + (e.name for e in all_entities if e.key == key), f"key={key}" + ) assert state.device_id == expected_device_id, ( - f"State for key {key} has device_id {state.device_id}, " - f"expected {expected_device_id}" + f"State for '{entity_name}' (type={type(state).__name__}) " + f"has device_id {state.device_id}, expected {expected_device_id}" ) verified_count += 1 - assert verified_count >= 6, ( - f"Only verified {verified_count} states, expected at least 6" + # All stateful entities should be verified (everything except Doorbell event) + expected_verified = expected_count - 1 # exclude Doorbell + assert verified_count >= expected_verified, ( + f"Only verified {verified_count} states, expected at least {expected_verified}" ) - # Test specific state types to ensure device_id is present - # Find a sensor state with device_id - sensor_state = next( - ( + # Verify each expected state type has at least one instance with non-zero device_id + for state_type, type_name in EXPECTED_STATE_TYPES: + matching = [ s for s in states.values() - if isinstance(s, SensorState) - and isinstance(s.state, float) - and s.device_id != 0 - ), - None, - ) - assert sensor_state is not None, "No sensor state with device_id found" - assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id" - - # Find a binary sensor state - binary_sensor_state = next( - (s for s in states.values() if isinstance(s, BinarySensorState)), - None, - ) - assert binary_sensor_state is not None, "No binary sensor state found" - assert binary_sensor_state.device_id > 0, ( - "Binary sensor state should have non-zero device_id" - ) - - # Find a text sensor state - text_sensor_state = next( - (s for s in states.values() if isinstance(s, TextSensorState)), - None, - ) - assert text_sensor_state is not None, "No text sensor state found" - assert text_sensor_state.device_id > 0, ( - "Text sensor state should have non-zero device_id" - ) + if isinstance(s, state_type) and s.device_id != 0 + ] + assert matching, ( + f"No {type_name} state (type={state_type.__name__}) " + f"with non-zero device_id found" + ) # Verify the "No Device Sensor" has device_id = 0 no_device_key = next( From ded457c2c1a5b27aba9af05b7b29a38dd832e7fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 18:52:46 -0600 Subject: [PATCH 70/89] [libretiny] Tune oversized lwIP defaults for ESPHome (#14186) Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/__init__.py | 4 +- esphome/components/captive_portal/__init__.py | 6 +- esphome/components/esp32/__init__.py | 59 +++---- .../esp32_camera_web_server/__init__.py | 6 +- esphome/components/esphome/ota/__init__.py | 5 +- esphome/components/libretiny/__init__.py | 153 +++++++++++++++++- esphome/components/mdns/__init__.py | 2 +- esphome/components/socket/__init__.py | 90 ++++++++++- esphome/components/udp/__init__.py | 2 +- esphome/components/web_server/__init__.py | 6 +- .../components/web_server_base/__init__.py | 25 ++- esphome/components/wifi/__init__.py | 20 +++ .../socket/test_wake_loop_threadsafe.py | 8 +- 13 files changed, 327 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 9bff9f5635..3f7cafb485 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType: # API needs 1 listening socket + typically 3 concurrent client connections # (not max_connections, which is the upper limit rarely reached) - sockets_needed = 1 + 3 - socket.consume_sockets(sockets_needed, "api")(config) + socket.consume_sockets(3, "api")(config) + socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 049618219e..6c190814c0 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -76,13 +76,15 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # - 3 TCP sockets for captive portal detection probes + configuration requests # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. # Need headroom for actual user configuration requests. # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + # The listening socket is registered by web_server_base (shared HTTP server). from esphome.components import socket - socket.consume_sockets(4, "captive_portal")(config) + socket.consume_sockets(3, "captive_portal")(config) + socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 06677006ea..4c211b2f2a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1258,21 +1258,15 @@ def _configure_lwip_max_sockets(conf: dict) -> None: This function runs in to_code() after all components have registered their socket needs. User-provided sdkconfig_options take precedence. """ - from esphome.components.socket import KEY_SOCKET_CONSUMERS + from esphome.components.socket import get_socket_counts # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") - socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) - total_sockets = sum(socket_consumers.values()) - - # Early return if no sockets registered and no user override - if total_sockets == 0 and user_max_sockets is None: - return - - components_list = ", ".join( - f"{name}={count}" for name, count in sorted(socket_consumers.items()) - ) + # CONFIG_LWIP_MAX_SOCKETS is a single VFS socket pool shared by all socket + # types (TCP clients, TCP listeners, and UDP). Include all three counts. + sc = get_socket_counts() + total_sockets = sc.tcp + sc.udp + sc.tcp_listen # User specified their own value - respect it but warn if insufficient if user_max_sockets is not None: @@ -1281,22 +1275,23 @@ def _configure_lwip_max_sockets(conf: dict) -> None: user_max_sockets, ) - # Warn if user's value is less than what components need - if total_sockets > 0: - user_sockets_int = 0 - with contextlib.suppress(ValueError, TypeError): - user_sockets_int = int(user_max_sockets) + user_sockets_int = 0 + with contextlib.suppress(ValueError, TypeError): + user_sockets_int = int(user_max_sockets) - if user_sockets_int < total_sockets: - _LOGGER.warning( - "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " - "needs %d sockets (registered: %s). You may experience socket " - "exhaustion errors. Consider increasing to at least %d.", - user_sockets_int, - total_sockets, - components_list, - total_sockets, - ) + if user_sockets_int < total_sockets: + _LOGGER.warning( + "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " + "needs %d sockets (%d TCP + %d UDP + %d TCP_LISTEN). You may " + "experience socket exhaustion errors. Consider increasing to " + "at least %d.", + user_sockets_int, + total_sockets, + sc.tcp, + sc.udp, + sc.tcp_listen, + total_sockets, + ) # User's value already added via sdkconfig_options processing return @@ -1305,11 +1300,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None: max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets) log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG + sock_min = " (min)" if max_sockets > total_sockets else "" _LOGGER.log( log_level, - "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)", + "Setting CONFIG_LWIP_MAX_SOCKETS to %d%s " + "(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])", max_sockets, - components_list, + sock_min, + sc.tcp, + sc.tcp_details, + sc.udp, + sc.udp_details, + sc.tcp_listen, + sc.tcp_listen_details, ) add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index ed1aaa2e07..da260ad7a1 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -20,8 +20,10 @@ def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # Each camera web server instance needs 1 listening socket + 2 client connections - sockets_needed = 3 - socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config) + socket.consume_sockets(2, "esp32_camera_web_server")(config) + socket.consume_sockets(1, "esp32_camera_web_server", socket.SocketType.TCP_LISTEN)( + config + ) return config diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 2f637d714d..337064dd27 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -97,8 +97,9 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType: """Register socket needs for OTA component.""" from esphome.components import socket - # OTA needs 1 listening socket (client connections are temporary during updates) - socket.consume_sockets(1, "ota")(config) + # OTA needs 1 listening socket. The active transfer connection during an update + # uses a TCP PCB from the general pool, covered by MIN_TCP_SOCKETS headroom. + socket.consume_sockets(1, "ota", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 01445da7ee..2291114d9a 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -275,6 +275,146 @@ BASE_SCHEMA.add_extra(_detect_variant) BASE_SCHEMA.add_extra(_update_core_data) +def _configure_lwip(config: dict) -> None: + """Configure lwIP options for LibreTiny platforms. + + The BK/RTL/LN SDKs each ship different lwIP defaults. BK72XX defaults are + wildly oversized for ESPHome's IoT use case, causing OOM on BK7231N. + RTL87XX and LN882H have more conservative defaults but still need tuning + for ESPHome's socket usage patterns. + + See https://github.com/esphome/esphome/issues/14095 + + Comparison of SDK defaults vs ESPHome targets (TCP_MSS=1460 on all LT): + + Setting ESP8266 ESP32 BK SDK RTL SDK LN SDK New + ──────────────────────────────────────────────────────────────────────────── + TCP_SND_BUF 2×MSS 4×MSS 10×MSS 5×MSS 7×MSS 4×MSS + TCP_WND 4×MSS 4×MSS 3/10×MSS 2×MSS 3×MSS 4×MSS + MEM_LIBC_MALLOC 1 1 0 0 1 1 + MEMP_MEM_MALLOC 1 1 0 0 0 1 + MEM_SIZE N/A* N/A* 16/32KB 5KB N/A* N/A* BK + PBUF_POOL_SIZE 10 16 3/10 20 20 10 BK + MAX_SOCKETS_TCP 5 16 12 —** —** dynamic + MAX_SOCKETS_UDP 4 16 22 —** —** dynamic + TCP_SND_QUEUELEN ~8 17 20 20 35 17 + MEMP_NUM_TCP_SEG 10 16 40 20 =qlen 17 + MEMP_NUM_TCP_PCB 5 16 12 10 8 =TCP + MEMP_NUM_TCP_PCB_LISTEN 4 16 4 5 3 dynamic + MEMP_NUM_UDP_PCB 4 16 25*** 7**** 7**** =UDP + MEMP_NUM_NETCONN 0 10 38 4***** =sum =sum + MEMP_NUM_NETBUF 0 2 16 2***** 8 4 + MEMP_NUM_TCPIP_MSG_INPKT 4 8 16 8***** 12 8 + + * ESP8266/ESP32/LN882H use MEM_LIBC_MALLOC=1 (system heap, no dedicated pool). + ESP8266/ESP32 also use MEMP_MEM_MALLOC=1 (MEMP pools from heap, not static). + ** RTL/LN SDKs don't define MAX_SOCKETS_TCP/UDP (LibreTiny-specific). + *** BK LT overlay: MAX_SOCKETS_UDP+2+1 = 25. + **** RTL/LN LT overlay overrides to flat 7. + ***** Not defined in RTL SDK — lwIP opt.h defaults shown. + "dynamic" = auto-calculated from component socket registrations via + socket.get_socket_counts() with minimums of 8 TCP / 6 UDP. + """ + from esphome.components.socket import ( + MIN_TCP_LISTEN_SOCKETS, + MIN_TCP_SOCKETS, + MIN_UDP_SOCKETS, + get_socket_counts, + ) + + sc = get_socket_counts() + # Apply platform minimums — ensure headroom for ESPHome's needs + tcp_sockets = max(MIN_TCP_SOCKETS, sc.tcp) + udp_sockets = max(MIN_UDP_SOCKETS, sc.udp) + # Listening sockets — registered by components (api, ota, web_server_base, etc.) + # Not all components register yet, so ensure a minimum for baseline operation. + listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, sc.tcp_listen) + + # TCP_SND_BUF: ESPAsyncWebServer allocates malloc(tcp_sndbuf()) per + # response chunk. At 10×MSS=14.6KB (BK default) this causes OOM (#14095). + # 4×MSS=5,840 matches ESP32. RTL(5×) and LN(7×) are close already. + tcp_snd_buf = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 5×MSS, LN: 7×MSS + + # TCP_WND: receive window. 4×MSS matches ESP32. + # RTL SDK uses only 2×MSS; increasing to 4× is safe and improves throughput. + tcp_wnd = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 2×MSS, LN: 3×MSS + + # TCP_SND_QUEUELEN: max pbufs queued for send buffer + # ESP-IDF formula: (4 * TCP_SND_BUF + (TCP_MSS - 1)) / TCP_MSS + # With 4×MSS: (4*5840 + 1459) / 1460 = 17 — match ESP32 + tcp_snd_queuelen = 17 # BK: 20, RTL: 20, LN: 35 + # MEMP_NUM_TCP_SEG: segment pool, must be >= TCP_SND_QUEUELEN (lwIP sanity check) + memp_num_tcp_seg = tcp_snd_queuelen # BK: 40, RTL: 20, LN: =qlen + + lwip_opts: list[str] = [ + # Disable statistics — not needed for production, saves RAM + "LWIP_STATS=0", # BK: 1, RTL: 0 already, LN: 0 already + "MEM_STATS=0", + "MEMP_STATS=0", + # TCP send buffer — 4×MSS matches ESP32 + f"TCP_SND_BUF={tcp_snd_buf}", + # TCP receive window — 4×MSS matches ESP32 + f"TCP_WND={tcp_wnd}", + # Socket counts — auto-calculated from component registrations + f"MAX_SOCKETS_TCP={tcp_sockets}", + f"MAX_SOCKETS_UDP={udp_sockets}", + # Listening sockets — BK SDK uses this to derive MEMP_NUM_TCP_PCB_LISTEN; + # RTL/LN don't use it, but we set MEMP_NUM_TCP_PCB_LISTEN explicitly below. + f"MAX_LISTENING_SOCKETS_TCP={listening_tcp}", + # Queued segment limits — derived from 4×MSS buffer size + f"TCP_SND_QUEUELEN={tcp_snd_queuelen}", + f"MEMP_NUM_TCP_SEG={memp_num_tcp_seg}", # must be >= queuelen + # PCB pools — active connections + listening sockets + f"MEMP_NUM_TCP_PCB={tcp_sockets}", # BK: 12, RTL: 10, LN: 8 + f"MEMP_NUM_TCP_PCB_LISTEN={listening_tcp}", # BK: =MAX_LISTENING, RTL: 5, LN: 3 + # UDP PCB pool — includes wifi.lwip_internal (DHCP + DNS) + f"MEMP_NUM_UDP_PCB={udp_sockets}", # BK: 25, RTL/LN: 7 via LT + # Netconn pool — each socket (active + listening) needs a netconn + f"MEMP_NUM_NETCONN={tcp_sockets + udp_sockets + listening_tcp}", + # Netbuf pool + "MEMP_NUM_NETBUF=4", # BK: 16, RTL: 2 (opt.h), LN: 8 + # Inbound message pool + "MEMP_NUM_TCPIP_MSG_INPKT=8", # BK: 16, RTL: 8 (opt.h), LN: 12 + ] + + # Use system heap for all lwIP allocations on all LibreTiny platforms. + # - MEM_LIBC_MALLOC=1: Use system heap instead of dedicated lwIP heap. + # LN882H already ships with this. BK SDK defaults to a 16/32KB dedicated + # pool that fragments during OTA. RTL SDK defaults to a 5KB pool. + # All three SDKs wire malloc → pvPortMalloc (FreeRTOS thread-safe heap). + # - MEMP_MEM_MALLOC=1: Allocate MEMP pools from heap on demand instead + # of static arrays. Saves ~20KB RAM on BK72XX. Safe because WiFi + # receive paths run in task context, not ISR context. ESP32 and ESP8266 + # both ship with MEMP_MEM_MALLOC=1. + lwip_opts.append("MEM_LIBC_MALLOC=1") + lwip_opts.append("MEMP_MEM_MALLOC=1") + + # BK72XX-specific: PBUF_POOL_SIZE override + # BK SDK "reduced plan" sets this to only 3 — too few for multiple + # concurrent connections (API + web_server + OTA). BK default plan + # uses 10; match that. RTL(20) and LN(20) need no override. + # With MEMP_MEM_MALLOC=1, this is a max count (allocated on demand). + if CORE.is_bk72xx: + lwip_opts.append("PBUF_POOL_SIZE=10") + + tcp_min = " (min)" if tcp_sockets > sc.tcp else "" + udp_min = " (min)" if udp_sockets > sc.udp else "" + listen_min = " (min)" if listening_tcp > sc.tcp_listen else "" + _LOGGER.info( + "Configuring lwIP: TCP=%d%s [%s], UDP=%d%s [%s], TCP_LISTEN=%d%s [%s]", + tcp_sockets, + tcp_min, + sc.tcp_details, + udp_sockets, + udp_min, + sc.udp_details, + listening_tcp, + listen_min, + sc.tcp_listen_details, + ) + cg.add_platformio_option("custom_options.lwip", lwip_opts) + + # pylint: disable=use-dict-literal async def component_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -389,11 +529,12 @@ async def component_to_code(config): "custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS ) - # Disable LWIP statistics to save RAM - not needed in production - # Must explicitly disable all sub-stats to avoid redefinition warnings - cg.add_platformio_option( - "custom_options.lwip", - ["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"], - ) + # Tune lwIP for ESPHome's actual needs. + # The SDK defaults (TCP_SND_BUF=10*MSS, MAX_SOCKETS_TCP=12, MEM_SIZE=32KB) + # are wildly oversized for an IoT device. ESPAsyncWebServer allocates + # malloc(tcp_sndbuf()) per response chunk — at 14.6KB this causes silent + # OOM on memory-constrained chips like BK7231N. + # See https://github.com/esphome/esphome/issues/14095 + _configure_lwip(config) await cg.register_component(var, config) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index f87f929615..420e6a60e3 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -56,7 +56,7 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # mDNS needs 2 sockets (IPv4 + IPv6 multicast) - socket.consume_sockets(2, "mdns")(config) + socket.consume_sockets(2, "mdns", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e364da78f8..d82f0c7aba 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,9 +1,14 @@ from collections.abc import Callable, MutableMapping +from dataclasses import dataclass +from enum import StrEnum +import logging import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@esphome/core"] CONF_IMPLEMENTATION = "implementation" @@ -13,33 +18,110 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Socket tracking infrastructure # Components register their socket needs and platforms read this to configure appropriately -KEY_SOCKET_CONSUMERS = "socket_consumers" +KEY_SOCKET_CONSUMERS_TCP = "socket_consumers_tcp" +KEY_SOCKET_CONSUMERS_UDP = "socket_consumers_udp" +KEY_SOCKET_CONSUMERS_TCP_LISTEN = "socket_consumers_tcp_listen" + +# Recommended minimum socket counts. +# Platforms should apply these (or their own) on top of get_socket_counts(). +# These cover minimal configs (e.g. api-only without web_server). +# When web_server is present, its 5 registered sockets push past the TCP minimum. +MIN_TCP_SOCKETS = 8 +MIN_UDP_SOCKETS = 6 +# Minimum listening sockets — at least api + ota baseline. +MIN_TCP_LISTEN_SOCKETS = 2 # Wake loop threadsafe support tracking KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" +class SocketType(StrEnum): + TCP = "tcp" + UDP = "udp" + TCP_LISTEN = "tcp_listen" + + +_SOCKET_TYPE_KEYS = { + SocketType.TCP: KEY_SOCKET_CONSUMERS_TCP, + SocketType.UDP: KEY_SOCKET_CONSUMERS_UDP, + SocketType.TCP_LISTEN: KEY_SOCKET_CONSUMERS_TCP_LISTEN, +} + + def consume_sockets( - value: int, consumer: str + value: int, consumer: str, socket_type: SocketType = SocketType.TCP ) -> Callable[[MutableMapping], MutableMapping]: """Register socket usage for a component. Args: value: Number of sockets needed by the component consumer: Name of the component consuming the sockets + socket_type: Type of socket (SocketType.TCP, SocketType.UDP, or SocketType.TCP_LISTEN) Returns: A validator function that records the socket usage """ + typed_key = _SOCKET_TYPE_KEYS[socket_type] def _consume_sockets(config: MutableMapping) -> MutableMapping: - consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {}) + consumers: dict[str, int] = CORE.data.setdefault(typed_key, {}) consumers[consumer] = consumers.get(consumer, 0) + value return config return _consume_sockets +def _format_consumers(consumers: dict[str, int]) -> str: + """Format consumer dict as 'name=count, ...' or 'none'.""" + if not consumers: + return "none" + return ", ".join(f"{name}={count}" for name, count in sorted(consumers.items())) + + +@dataclass(frozen=True) +class SocketCounts: + """Socket counts and component details for platform configuration.""" + + tcp: int + udp: int + tcp_listen: int + tcp_details: str + udp_details: str + tcp_listen_details: str + + +def get_socket_counts() -> SocketCounts: + """Return socket counts and component details for platform configuration. + + Platforms call this during code generation to configure lwIP socket limits. + All components will have registered their needs by then. + + Platforms should apply their own minimums on top of these values. + """ + tcp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP, {}) + udp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_UDP, {}) + tcp_listen_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP_LISTEN, {}) + tcp = sum(tcp_consumers.values()) + udp = sum(udp_consumers.values()) + tcp_listen = sum(tcp_listen_consumers.values()) + + tcp_details = _format_consumers(tcp_consumers) + udp_details = _format_consumers(udp_consumers) + tcp_listen_details = _format_consumers(tcp_listen_consumers) + _LOGGER.debug( + "Socket counts: TCP=%d (%s), UDP=%d (%s), TCP_LISTEN=%d (%s)", + tcp, + tcp_details, + udp, + udp_details, + tcp_listen, + tcp_listen_details, + ) + return SocketCounts( + tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details + ) + + def require_wake_loop_threadsafe() -> None: """Mark that wake_loop_threadsafe support is required by a component. @@ -66,7 +148,7 @@ def require_wake_loop_threadsafe() -> None: CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") # Consume 1 socket for the shared wake notification socket - consume_sockets(1, "socket.wake_loop_threadsafe")({}) + consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) CONFIG_SCHEMA = cv.Schema( diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index c9586d0b95..37dd871a6c 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -73,7 +73,7 @@ def _consume_udp_sockets(config: ConfigType) -> ConfigType: # UDP uses up to 2 sockets: 1 broadcast + 1 listen # Whether each is used depends on code generation, so register worst case - socket.consume_sockets(2, "udp")(config) + socket.consume_sockets(2, "udp", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 9305a2de61..84910b6f90 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,11 +144,11 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 5 concurrent client connections + # Web server needs typically 5 concurrent client connections # (browser opens connections for page resources, SSE event stream, and POST # requests for entity control which may linger before closing) - sockets_needed = 6 - socket.consume_sockets(sockets_needed, "web_server")(config) + # The listening socket is registered by web_server_base (shared with captive_portal) + socket.consume_sockets(5, "web_server")(config) return config diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 183b907ae6..b587841dfd 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -23,10 +23,27 @@ web_server_base_ns = cg.esphome_ns.namespace("web_server_base") WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(WebServerBase), - } + + +def _consume_web_server_base_sockets(config): + """Register the shared listening socket for the HTTP server. + + web_server_base is the shared HTTP server used by web_server and captive_portal. + The listening socket is registered here rather than in each consumer. + """ + from esphome.components import socket + + socket.consume_sockets(1, "web_server_base", socket.SocketType.TCP_LISTEN)(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServerBase), + } + ), + _consume_web_server_base_sockets, ) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8c1deb6217..b89245b10e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -58,6 +58,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType from . import wpa2_eap @@ -269,9 +270,28 @@ def final_validate(config): ) +def _consume_wifi_sockets(config: ConfigType) -> ConfigType: + """Register UDP PCBs used internally by lwIP for DHCP and DNS. + + Only needed on LibreTiny where we directly set MEMP_NUM_UDP_PCB (the raw + PCB pool shared by both application sockets and lwIP internals like DHCP/DNS). + On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket layer — + DHCP/DNS use raw udp_new() which bypasses it entirely. + """ + if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x): + return config + from esphome.components import socket + + # lwIP allocates UDP PCBs for DHCP client and DNS resolver internally. + # These are not application sockets but consume MEMP_NUM_UDP_PCB pool entries. + socket.consume_sockets(2, "wifi.lwip_internal", socket.SocketType.UDP)(config) + return config + + FINAL_VALIDATE_SCHEMA = cv.All( final_validate, validate_variant, + _consume_wifi_sockets, ) diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index b4bc95176d..a40b6068a8 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -66,12 +66,12 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - CORE.config = {"logger": {}} # Track initial socket consumer state - initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) + initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) # Call require_wake_loop_threadsafe socket.require_wake_loop_threadsafe() # Verify no socket was consumed - consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) - assert "socket.wake_loop_threadsafe" not in consumers - assert consumers == initial_consumers + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert "socket.wake_loop_threadsafe" not in udp_consumers + assert udp_consumers == initial_udp From 263fff0ba2fc5ea7b9c849da88600f17852a86aa Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:35:00 +0100 Subject: [PATCH 71/89] Move CONF_OUTPUT_POWER into const.py (#14201) --- esphome/components/cc1101/__init__.py | 2 +- esphome/components/wifi/__init__.py | 2 +- esphome/const.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 14b92a18a4..e2e5986daf 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_OUTPUT_POWER, CONF_VALUE, CONF_WAIT_TIME, ) @@ -22,7 +23,6 @@ ns = cg.esphome_ns.namespace("cc1101") CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice) # Config keys -CONF_OUTPUT_POWER = "output_power" CONF_RX_ATTENUATION = "rx_attenuation" CONF_DC_BLOCKING_FILTER = "dc_blocking_filter" CONF_IF_FREQUENCY = "if_frequency" diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b89245b10e..2aa63b87cc 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -42,6 +42,7 @@ from esphome.const import ( CONF_ON_CONNECT, CONF_ON_DISCONNECT, CONF_ON_ERROR, + CONF_OUTPUT_POWER, CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_PRIORITY, @@ -344,7 +345,6 @@ def _validate(config): return config -CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( cv.Schema( diff --git a/esphome/const.py b/esphome/const.py index 0179aa129e..ccc9d56dbb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -748,6 +748,7 @@ CONF_OTA = "ota" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTPUT = "output" CONF_OUTPUT_ID = "output_id" +CONF_OUTPUT_POWER = "output_power" CONF_OUTPUT_SPEAKER = "output_speaker" CONF_OUTPUTS = "outputs" CONF_OVERSAMPLING = "oversampling" From 6e70987451dfce381645e06ec61e87fab745b900 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:30 -0600 Subject: [PATCH 72/89] [binary_sensor] Conditionally compile filter infrastructure (#14215) --- esphome/components/binary_sensor/__init__.py | 1 + esphome/components/binary_sensor/binary_sensor.cpp | 6 ++++++ esphome/components/binary_sensor/binary_sensor.h | 6 ++++++ esphome/components/binary_sensor/filter.cpp | 5 +++++ esphome/components/binary_sensor/filter.h | 5 +++++ esphome/core/defines.h | 1 + 6 files changed, 24 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c38d6b78d3..4500168cb1 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -562,6 +562,7 @@ async def setup_binary_sensor_core_(var, config): if inverted := config.get(CONF_INVERTED): cg.add(var.set_inverted(inverted)) if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) cg.add(var.add_filters(filters)) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 7c3b06970d..c4d3a29a1e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -18,11 +18,15 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi } void BinarySensor::publish_state(bool new_state) { +#ifdef USE_BINARY_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->send_state_internal(new_state); +#ifdef USE_BINARY_SENSOR_FILTER } else { this->filter_list_->input(new_state); } +#endif } void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); @@ -47,6 +51,7 @@ bool BinarySensor::set_new_state(const optional &new_state) { return false; } +#ifdef USE_BINARY_SENSOR_FILTER void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; if (this->filter_list_ == nullptr) { @@ -63,6 +68,7 @@ void BinarySensor::add_filters(std::initializer_list filters) { this->add_filter(filter); } } +#endif // USE_BINARY_SENSOR_FILTER bool BinarySensor::is_status_binary_sensor() const { return false; } } // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 83c992bfed..4b655e1bd1 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -2,7 +2,9 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_BINARY_SENSOR_FILTER #include "esphome/components/binary_sensor/filter.h" +#endif #include @@ -45,8 +47,10 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl */ void publish_initial_state(bool new_state); +#ifdef USE_BINARY_SENSOR_FILTER void add_filter(Filter *filter); void add_filters(std::initializer_list filters); +#endif // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -60,7 +64,9 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl bool state{}; protected: +#ifdef USE_BINARY_SENSOR_FILTER Filter *filter_list_{nullptr}; +#endif bool set_new_state(const optional &new_state) override; }; diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index d69671c5bf..25a69c413a 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "filter.h" #include "binary_sensor.h" @@ -142,3 +145,5 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 59bc43eeba..2735a32ab0 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -138,3 +141,5 @@ class SettleFilter : public Filter, public Component { }; } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1128df94f0..99e80785ec 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -29,6 +29,7 @@ #define USE_ALARM_CONTROL_PANEL #define USE_AREAS #define USE_BINARY_SENSOR +#define USE_BINARY_SENSOR_FILTER #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE From 93ce582ad30d1059cf59335882a6bd231a93127e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:51 -0600 Subject: [PATCH 73/89] [sensor] Conditionally compile filter infrastructure (#14214) Co-authored-by: Claude Opus 4.6 --- esphome/components/bme680_bsec/bme680_bsec.h | 1 + esphome/components/haier/hon_climate.h | 1 + esphome/components/kamstrup_kmp/kamstrup_kmp.h | 1 + esphome/components/sensor/__init__.py | 1 + esphome/components/sensor/filter.cpp | 5 +++++ esphome/components/sensor/filter.h | 6 +++++- esphome/components/sensor/sensor.cpp | 6 ++++++ esphome/components/sensor/sensor.h | 8 ++++++++ esphome/components/sound_level/sound_level.h | 1 + esphome/components/wireguard/wireguard.h | 1 + esphome/core/defines.h | 1 + 11 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index ec919f31df..22aa2789e6 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -7,6 +7,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/defines.h" #include +#include #ifdef USE_BSEC #include diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 608d5e7f21..4565ed2981 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -1,6 +1,7 @@ #pragma once #include +#include #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index f84e360132..725cf20abf 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ebbe0fbccc..b0e0c28bda 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -903,6 +903,7 @@ async def setup_sensor_core_(var, config): if config[CONF_FORCE_UPDATE]: cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index ea0e2f0d7c..cd4db98457 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include "filter.h" #include #include "esphome/core/application.h" @@ -580,3 +583,5 @@ void StreamingMovingAverageFilter::reset_batch() { } } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 573b916a5d..8bfcdb37cf 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -1,6 +1,8 @@ #pragma once -#include +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include #include #include "esphome/core/automation.h" @@ -638,3 +640,5 @@ class StreamingMovingAverageFilter : public StreamingFilter { }; } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index ae2ee3e3d1..a7af6403ef 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -68,11 +68,15 @@ void Sensor::publish_state(float state) { ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); +#ifdef USE_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->internal_send_state_to_frontend(state); +#ifdef USE_SENSOR_FILTER } else { this->filter_list_->input(state); } +#endif } void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } @@ -80,6 +84,7 @@ void Sensor::add_on_raw_state_callback(std::function &&callback) { this->raw_callback_.add(std::move(callback)); } +#ifdef USE_SENSOR_FILTER void Sensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -109,6 +114,7 @@ void Sensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_SENSOR_FILTER float Sensor::get_state() const { return this->state; } float Sensor::get_raw_state() const { return this->raw_state; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 80981b8e28..54e75ee2a1 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -4,13 +4,17 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_SENSOR_FILTER #include "esphome/components/sensor/filter.h" +#endif #include #include namespace esphome::sensor { +class Sensor; + void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj); #define LOG_SENSOR(prefix, type, obj) log_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -67,6 +71,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Set force update mode. void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } +#ifdef USE_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -87,6 +92,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Clear the entire filter chain. void clear_filters(); +#endif /// Getter-syntax for .state. float get_state() const; @@ -130,7 +136,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif // Group small members together to avoid padding int8_t accuracy_decimals_{-1}; ///< Accuracy in decimals (-1 = not set) diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index dc35f69fe2..a1021eb1e8 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -6,6 +6,7 @@ #include "esphome/components/microphone/microphone_source.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/ring_buffer.h" diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index e8470c75cd..c11d592cd1 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -4,6 +4,7 @@ #include #include +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/time/real_time_clock.h" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 99e80785ec..ff32edff16 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -112,6 +112,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SENSOR_FILTER #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR From d239a2400dddfb9854ea5e6c7f476002b6bf22c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:36:21 -0600 Subject: [PATCH 74/89] [text_sensor] Conditionally compile filter infrastructure (#14213) --- esphome/components/text_sensor/__init__.py | 1 + esphome/components/text_sensor/filter.cpp | 5 +++++ esphome/components/text_sensor/filter.h | 5 +++++ esphome/components/text_sensor/text_sensor.cpp | 18 +++++++++++++----- esphome/components/text_sensor/text_sensor.h | 8 ++++++++ esphome/core/defines.h | 1 + 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 0d22400a8e..2e8edb43c9 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -204,6 +204,7 @@ async def setup_text_sensor_core_(var, config): cg.add(var.set_device_class(device_class)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_TEXT_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index 4ee12e8602..f6552c7c66 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "filter.h" #include "text_sensor.h" #include "esphome/core/log.h" @@ -106,3 +109,5 @@ bool MapFilter::new_value(std::string &value) { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 1922b503ca..f88e8645cc 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -164,3 +167,5 @@ class MapFilter : public Filter { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c48bdf4b82..c66d08ec40 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -24,7 +24,9 @@ void TextSensor::publish_state(const std::string &state) { this->publish_state(s void TextSensor::publish_state(const char *state) { this->publish_state(state, strlen(state)); } void TextSensor::publish_state(const char *state, size_t len) { +#ifdef USE_TEXT_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif // No filters: raw_state == state, store once and use for both callbacks // Only assign if changed to avoid heap allocation if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { @@ -33,6 +35,7 @@ void TextSensor::publish_state(const char *state, size_t len) { this->raw_callback_.call(this->state); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->state.c_str()); this->notify_frontend_(); +#ifdef USE_TEXT_SENSOR_FILTER } else { // Has filters: need separate raw storage #pragma GCC diagnostic push @@ -46,8 +49,10 @@ void TextSensor::publish_state(const char *state, size_t len) { this->filter_list_->input(this->raw_state); #pragma GCC diagnostic pop } +#endif } +#ifdef USE_TEXT_SENSOR_FILTER void TextSensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -77,6 +82,7 @@ void TextSensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_TEXT_SENSOR_FILTER void TextSensor::add_on_state_callback(std::function callback) { this->callback_.add(std::move(callback)); @@ -87,14 +93,16 @@ void TextSensor::add_on_raw_state_callback(std::functionstate; } const std::string &TextSensor::get_raw_state() const { - if (this->filter_list_ == nullptr) { - return this->state; // No filters, raw == filtered - } -// Suppress deprecation warning - get_raw_state() is the replacement API +#ifdef USE_TEXT_SENSOR_FILTER + if (this->filter_list_ != nullptr) { + // Suppress deprecation warning - get_raw_state() is the replacement API #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->raw_state; + return this->raw_state; #pragma GCC diagnostic pop + } +#endif + return this->state; // No filters, raw == filtered } void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->internal_send_state_to_frontend(state.data(), state.size()); diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 1352a8c1e4..97373dc716 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -3,7 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_TEXT_SENSOR_FILTER #include "esphome/components/text_sensor/filter.h" +#endif #include #include @@ -11,6 +13,8 @@ namespace esphome { namespace text_sensor { +class TextSensor; + void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj); #define LOG_TEXT_SENSOR(prefix, type, obj) log_text_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -45,6 +49,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void publish_state(const char *state); void publish_state(const char *state, size_t len); +#ifdef USE_TEXT_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -56,6 +61,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { /// Clear the entire filter chain. void clear_filters(); +#endif void add_on_state_callback(std::function callback); /// Add a callback that will be called every time the sensor sends a raw value. @@ -73,7 +79,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_TEXT_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif }; } // namespace text_sensor diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ff32edff16..a1e3d1707f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -119,6 +119,7 @@ #define USE_SWITCH #define USE_TEXT #define USE_TEXT_SENSOR +#define USE_TEXT_SENSOR_FILTER #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER From 5c388a5200b65a9e4470d4f4a681e41c49517915 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:39:36 +0100 Subject: [PATCH 75/89] [openthread_info] Optimize: Devirtualize/unify (#14208) --- .../openthread_info_text_sensor.h | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h index ac5623e0c1..10e83281f0 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.h +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -25,7 +25,7 @@ class OpenThreadInstancePollingComponent : public PollingComponent { virtual void update_instance(otInstance *instance) = 0; }; -class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::TextSensor { +class IPAddressOpenThreadInfo final : public PollingComponent, public text_sensor::TextSensor { public: void update() override { std::optional address = openthread::global_openthread_component->get_omr_address(); @@ -48,7 +48,7 @@ class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::Tex std::string last_ip_; }; -class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class RoleOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otDeviceRole role = otThreadGetDeviceRole(instance); @@ -64,7 +64,7 @@ class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public tex otDeviceRole last_role_; }; -class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Rloc16OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint16_t rloc16 = otThreadGetRloc16(instance); @@ -75,14 +75,13 @@ class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public t this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_rloc16_; }; -class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ExtAddrOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { const auto *extaddr = otLinkGetExtendedAddress(instance); @@ -93,14 +92,13 @@ class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_extaddr_{}; }; -class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Eui64OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otExtAddress addr; @@ -113,14 +111,13 @@ class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_eui64_{}; }; -class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ChannelOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint8_t channel = otLinkGetChannel(instance); @@ -131,7 +128,6 @@ class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: @@ -153,7 +149,7 @@ class DatasetOpenThreadInfo : public OpenThreadInstancePollingComponent { virtual void update_dataset(otOperationalDataset *dataset) = 0; }; -class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkNameOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (this->last_network_name_ != dataset->mNetworkName.m8) { @@ -161,14 +157,13 @@ class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sens this->publish_state(this->last_network_name_); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::string last_network_name_; }; -class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkKeyOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_key_.begin(), this->last_key_.end(), dataset->mNetworkKey.m8)) { @@ -178,14 +173,13 @@ class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_senso this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_key_{}; }; -class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class PanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { uint16_t panid = dataset->mPanId; @@ -196,14 +190,13 @@ class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::Te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_panid_; }; -class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class ExtPanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_extpanid_.begin(), this->last_extpanid_.end(), dataset->mExtendedPanId.m8)) { @@ -214,7 +207,6 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: From 680160453355210018b073e0036c256d45a143ce Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:40:40 +0100 Subject: [PATCH 76/89] [openthread] Add Thread version DEBUG trace (#14196) --- esphome/components/openthread/openthread_esp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index ec212d1f68..4f8db0634a 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -104,6 +104,8 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + ESP_LOGD(TAG, "Thread Version: %" PRIu16, otThreadGetVersion()); + otLinkModeConfig link_mode_config{}; #if CONFIG_OPENTHREAD_FTD link_mode_config.mRxOnWhenIdle = true; From ee94bc471541a460c989b51cf317530b7ffa4611 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:43:42 +0100 Subject: [PATCH 77/89] [openthread] Refactor to optimize and match code rules (#14156) --- esphome/components/openthread/openthread.cpp | 4 +-- esphome/components/openthread/openthread.h | 10 +++---- .../components/openthread/openthread_esp.cpp | 29 ++++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 90da17e2d3..d22a14aeae 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -35,9 +35,9 @@ void OpenThreadComponent::dump_config() { #elif CONFIG_OPENTHREAD_MTD ESP_LOGCONFIG(TAG, " Device Type: MTD"); // TBD: Synchronized Sleepy End Device - if (this->poll_period > 0) { + if (this->poll_period_ > 0) { ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); - uint32_t duration = this->poll_period / 1000; + uint32_t duration = this->poll_period_ / 1000; ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); } else { ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 3c60acaadd..9e429f289b 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -36,22 +36,22 @@ class OpenThreadComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); #if CONFIG_OPENTHREAD_MTD - void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } + void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif protected: std::optional get_omr_address_(InstanceLock &lock); + std::function factory_reset_external_callback_; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period_{0}; +#endif bool teardown_started_{false}; bool teardown_complete_{false}; - std::function factory_reset_external_callback_; private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; -#if CONFIG_OPENTHREAD_MTD - uint32_t poll_period{0}; -#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 4f8db0634a..bb70701471 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -81,9 +81,11 @@ void OpenThreadComponent::ot_main() { // Initialize the OpenThread stack // otLoggingSetLevel(OT_LOG_LEVEL_DEBG); ESP_ERROR_CHECK(esp_openthread_init(&config)); + // Fetch OT instance once to avoid repeated call into OT stack + otInstance *instance = esp_openthread_get_instance(); #if CONFIG_OPENTHREAD_STATE_INDICATOR_ENABLE - ESP_ERROR_CHECK(esp_openthread_state_indicator_init(esp_openthread_get_instance())); + ESP_ERROR_CHECK(esp_openthread_state_indicator_init(instance)); #endif #if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC @@ -112,28 +114,29 @@ void OpenThreadComponent::ot_main() { link_mode_config.mDeviceType = true; link_mode_config.mNetworkData = true; #elif CONFIG_OPENTHREAD_MTD - if (this->poll_period > 0) { - if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + if (this->poll_period_ > 0) { + if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set pollperiod"); } - uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance)); } - link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0; link_mode_config.mDeviceType = false; link_mode_config.mNetworkData = false; #endif - if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set linkmode"); } - link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); +#ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG + link_mode_config = otThreadGetLinkMode(instance); ESP_LOGD(TAG, "Link Mode Device Type: %s\n" "Link Mode Network Data: %s\n" "Link Mode RX On When Idle: %s", - link_mode_config.mDeviceType ? "true" : "false", link_mode_config.mNetworkData ? "true" : "false", - link_mode_config.mRxOnWhenIdle ? "true" : "false"); + TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), + TRUEFALSE(link_mode_config.mRxOnWhenIdle)); +#endif // Run the main loop #if CONFIG_OPENTHREAD_CLI @@ -144,7 +147,7 @@ void OpenThreadComponent::ot_main() { #ifndef USE_OPENTHREAD_FORCE_DATASET // Check if openthread has a valid dataset from a previous execution - otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); + otError error = otDatasetGetActiveTlvs(instance, &dataset); if (error != OT_ERROR_NONE) { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; From 417f4535af983e90d5ac2407847a29f15cb4905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 22:25:20 -0600 Subject: [PATCH 78/89] [logger] Use subtraction-based line number formatting to avoid division (#14219) --- esphome/components/logger/log_buffer.h | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index 3d87278248..a56276f732 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -75,18 +75,13 @@ struct LogBuffer { *p++ = ':'; - // Format line number without modulo operations + // Format line number using subtraction loops (no division - important for ESP8266 which lacks hardware divider) if (line > 999) [[unlikely]] { - int thousands = line / 1000; - *p++ = '0' + thousands; - line -= thousands * 1000; + write_digit(p, line, 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); + write_digit(p, line, 100); + write_digit(p, line, 10); + *p++ = '0' + line; *p++ = ']'; #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) @@ -162,6 +157,15 @@ struct LogBuffer { this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); } #endif + // Extract one decimal digit via subtraction (no division - important for ESP8266) + static inline void ESPHOME_ALWAYS_INLINE write_digit(char *&p, int &value, int divisor) { + char d = '0'; + while (value >= divisor) { + d++; + value -= divisor; + } + *p++ = d; + } // 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) { From fb6c7d81d5b3832c19eb1dd7b61c70a944742d8a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:08:40 -0500 Subject: [PATCH 79/89] [core] Fix multiline log continuations without leading whitespace (#14217) Co-authored-by: Claude Opus 4.6 --- .../absolute_humidity/absolute_humidity.cpp | 5 +- esphome/components/anova/anova.cpp | 6 +- esphome/components/as3935/as3935.cpp | 6 +- esphome/components/at581x/at581x.cpp | 16 ++--- .../components/binary_sensor/automation.cpp | 6 +- esphome/components/bl0940/bl0940.cpp | 5 +- .../current_based/current_based_cover.cpp | 8 +-- esphome/components/debug/debug_esp32.cpp | 60 ++++++++++--------- esphome/components/debug/debug_esp8266.cpp | 18 +++--- esphome/components/debug/debug_libretiny.cpp | 14 +++-- esphome/components/debug/debug_zephyr.cpp | 42 +++++++------ .../components/dfrobot_sen0395/commands.cpp | 16 ++--- esphome/components/emmeti/emmeti.cpp | 19 ++---- .../components/ens160_base/ens160_base.cpp | 13 ++-- esphome/components/es8388/es8388.cpp | 7 ++- .../esp32_ble_client/ble_client_base.cpp | 6 +- .../update/esp32_hosted_update.cpp | 11 ++-- .../packet_transport/espnow_transport.cpp | 8 +-- .../ethernet/ethernet_component.cpp | 5 +- esphome/components/ezo_pmp/ezo_pmp.cpp | 6 +- esphome/components/gcja5/gcja5.cpp | 8 +-- .../graphical_display_menu.cpp | 14 ++--- .../components/honeywellabp/honeywellabp.cpp | 5 +- .../http_request/ota/ota_http_request.cpp | 3 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +- esphome/components/ld2410/ld2410.cpp | 7 +-- esphome/components/ld2412/ld2412.cpp | 5 +- esphome/components/ledc/ledc_output.cpp | 6 +- esphome/components/max6956/max6956.cpp | 6 +- .../components/mqtt/mqtt_backend_esp32.cpp | 5 +- .../nextion/nextion_upload_arduino.cpp | 22 ++----- .../nextion/nextion_upload_esp32.cpp | 30 ++-------- .../components/openthread/openthread_esp.cpp | 8 +-- .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 7 ++- esphome/components/pipsolar/pipsolar.cpp | 3 +- esphome/components/pn532/pn532.cpp | 5 +- esphome/components/pn7150/pn7150.cpp | 15 +++-- esphome/components/pn7160/pn7160.cpp | 16 +++-- esphome/components/qmp6988/qmp6988.cpp | 26 ++++---- .../remote_base/pronto_protocol.cpp | 5 +- esphome/components/safe_mode/safe_mode.cpp | 10 ++-- esphome/components/sim800l/sim800l.cpp | 2 +- esphome/components/sonoff_d1/sonoff_d1.cpp | 10 +--- esphome/components/sun/sun.cpp | 29 ++++----- .../components/usb_host/usb_host_client.cpp | 44 +++++++------- esphome/components/usb_uart/cp210x.cpp | 6 +- .../voice_assistant/voice_assistant.cpp | 4 +- esphome/components/wl_134/wl_134.cpp | 13 ++-- esphome/components/xgzp68xx/xgzp68xx.cpp | 6 +- script/ci-custom.py | 33 ++++++++++ 50 files changed, 291 insertions(+), 345 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 9c66531d05..40676f8655 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -92,10 +92,7 @@ void AbsoluteHumidityComponent::loop() { // Calculate absolute humidity const float absolute_humidity = vapor_density(es, hr, temperature_k); - ESP_LOGD(TAG, - "Saturation vapor pressure %f kPa\n" - "Publishing absolute humidity %f g/m³", - es, absolute_humidity); + ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); // Publish absolute humidity this->status_clear_warning(); diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 5054488089..2693224a97 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -67,10 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGW(TAG, - "[%s] No control service found at device, not an Anova..?\n" - "[%s] Note, this component does not currently support Anova Nano.", - this->get_name().c_str(), this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); break; } this->char_handle_ = chr->handle; diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index dd0ab714f7..c4dc0466a0 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() { uint8_t tune_val = this->read_capacitance(); ESP_LOGI(TAG, "Starting antenna tuning\n" - "Division Ratio is set to: %d\n" - "Internal Capacitor is set to: %d\n" - "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", + " Division Ratio is set to: %d\n" + " Internal Capacitor is set to: %d\n" + " Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", div_ratio, tune_val); this->display_oscillator(true, ANTFREQ); } diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 6804a7f4b5..728fbe20c6 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } bool AT581XComponent::i2c_write_config() { ESP_LOGCONFIG(TAG, "Writing new config for AT581X\n" - "Frequency: %dMHz\n" - "Sensing distance: %d\n" - "Power: %dµA\n" - "Gain: %d\n" - "Trigger base time: %dms\n" - "Trigger keep time: %dms\n" - "Protect time: %dms\n" - "Self check time: %dms", + " Frequency: %dMHz\n" + " Sensing distance: %d\n" + " Power: %dµA\n" + " Gain: %d\n" + " Trigger base time: %dms\n" + " Trigger keep time: %dms\n" + " Protect time: %dms\n" + " Self check time: %dms", this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_, this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_); diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index faebe7e88f..7e43d42357 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -29,10 +29,8 @@ void MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, - "START min=%" PRIu32 " max=%" PRIu32 "\n" - "Multi Click: Starting multi click action!", - evt.min_length, evt.max_length); + ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); + ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 42e20eb69b..31625ebf6d 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -182,7 +182,10 @@ void BL0940::recalibrate_() { ESP_LOGD(TAG, "Recalibrated reference values:\n" - "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + " Voltage: %f\n" + " Current: %f\n" + " Power: %f\n" + " Energy: %f", this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, this->energy_reference_cal_); } diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 5dfaeeff39..58ae7cbc34 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -148,14 +148,14 @@ void CurrentBasedCover::dump_config() { } ESP_LOGCONFIG(TAG, " Close Duration: %.1fs\n" - "Obstacle Rollback: %.1f%%", + " Obstacle Rollback: %.1f%%", this->close_duration_ / 1e3f, this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { - ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Maximum duration: %.1fs", this->max_duration_ / 1e3f); } ESP_LOGCONFIG(TAG, - "Start sensing delay: %.1fs\n" - "Malfunction detection: %s", + " Start sensing delay: %.1fs\n" + " Malfunction detection: %s", this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index aad4c7426c..6898621dd0 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -79,7 +79,6 @@ const char *DebugComponent::get_reset_reason_(std::span } uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT - ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, flash_mode); #endif @@ -194,39 +191,46 @@ size_t DebugComponent::get_device_info_(std::span if (info.features != 0) { pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); } - ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; - ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - // Framework detection -#ifdef USE_ARDUINO - ESP_LOGD(TAG, "Framework: Arduino"); - pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); -#elif defined(USE_ESP32) - ESP_LOGD(TAG, "Framework: ESP-IDF"); - pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); -#else - ESP_LOGW(TAG, "Framework: UNKNOWN"); - pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN"); -#endif - - ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); - - uint8_t mac[6]; - get_mac_address_raw(mac); - ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], - mac[4], mac[5]); - char reason_buffer[RESET_REASON_BUFFER_SIZE]; const char *reset_reason = get_reset_reason_(std::span(reason_buffer)); - pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); - const char *wakeup_cause = get_wakeup_cause_(std::span(reason_buffer)); + + uint8_t mac[6]; + get_mac_address_raw(mac); + + ESP_LOGD(TAG, + "ESP32 debug info:\n" + " Chip: %s\n" + " Cores: %u\n" + " Revision: %u\n" + " CPU Frequency: %" PRIu32 " MHz\n" + " ESP-IDF Version: %s\n" + " EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X\n" + " Reset Reason: %s\n" + " Wakeup Cause: %s", + model, info.cores, info.revision, cpu_freq_mhz, esp_get_idf_version(), mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5], reset_reason, wakeup_cause); +#if defined(USE_ARDUINO) + ESP_LOGD(TAG, " Flash: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); +#endif + // Framework detection +#ifdef USE_ARDUINO + ESP_LOGD(TAG, " Framework: Arduino"); + pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); +#else + ESP_LOGD(TAG, " Framework: ESP-IDF"); + pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); +#endif + + pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); + pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); return pos; diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 1a07ec4f3a..4df4aaa851 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -128,14 +128,16 @@ size_t DebugComponent::get_device_info_(std::span // NOLINTEND(readability-static-accessed-through-instance) ESP_LOGD(TAG, - "Chip ID: 0x%08" PRIX32 "\n" - "SDK Version: %s\n" - "Core Version: %s\n" - "Boot Version=%u Mode=%u\n" - "CPU Frequency: %u\n" - "Flash Chip ID=0x%08" PRIX32 "\n" - "Reset Reason: %s\n" - "Reset Info: %s", + "ESP8266 debug info:\n" + " Chip ID: 0x%08" PRIX32 "\n" + " SDK Version: %s\n" + " Core Version: %s\n" + " Boot Version: %u\n" + " Boot Mode: %u\n" + " CPU Frequency: %u\n" + " Flash Chip ID: 0x%08" PRIX32 "\n" + " Reset Reason: %s\n" + " Reset Info: %s", chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq, flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason)); diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 14bbdb945a..39269d6f2f 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -27,12 +27,14 @@ size_t DebugComponent::get_device_info_(std::span uint32_t mac_id = lt_cpu_get_mac_id(); ESP_LOGD(TAG, - "LibreTiny Version: %s\n" - "Chip: %s (%04x) @ %u MHz\n" - "Chip ID: 0x%06" PRIX32 "\n" - "Board: %s\n" - "Flash: %" PRIu32 " KiB / RAM: %" PRIu32 " KiB\n" - "Reset Reason: %s", + "LibreTiny debug info:\n" + " Version: %s\n" + " Chip: %s (%04x) @ %u MHz\n" + " Chip ID: 0x%06" PRIX32 "\n" + " Board: %s\n" + " Flash: %" PRIu32 " KiB\n" + " RAM: %" PRIu32 " KiB\n" + " Reset Reason: %s", lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_board_code(), flash_kib, ram_kib, reset_reason); diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index ecca7150bd..bd6432e949 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -79,13 +79,13 @@ static void fa_cb(const struct flash_area *fa, void *user_data) { void DebugComponent::log_partition_info_() { #if CONFIG_FLASH_MAP_LABELS ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Label | Offset | Size\n" - "--------------------------------------------" + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" "-----------------------------------------------"); #else ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Offset | Size\n" - "-----------------------------------------" + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" "------------------------------"); #endif flash_area_foreach(fa_cb, nullptr); @@ -284,11 +284,12 @@ size_t DebugComponent::get_device_info_(std::span char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; get_mac_address_pretty_into_buffer(mac_pretty); ESP_LOGD(TAG, - "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" - "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" - "Device address type: %s, address: %s\n" - "Part code: nRF%x, version: %c%c%c%c, package: %s\n" - "RAM: %ukB, Flash: %ukB, production test: %sdone", + "nRF debug info:\n" + " Code page size: %u, code size: %u, device id: 0x%08x%08x\n" + " Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" + " Device address type: %s, address: %s\n" + " Part code: nRF%x, version: %c%c%c%c, package: %s\n" + " RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART, @@ -299,23 +300,22 @@ size_t DebugComponent::get_device_info_(std::span (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( - TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", + TAG, " GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO(n_reset_enabled)); if (n_reset_enabled) { uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; - ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + ESP_LOGD(TAG, " nRESET port P%u.%02u", port, pin); } #ifdef USE_BOOTLOADER_MCUBOOT - ESP_LOGD(TAG, "bootloader: mcuboot"); + ESP_LOGD(TAG, " Bootloader: mcuboot"); #else - ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, + ESP_LOGD(TAG, " Bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); - ESP_LOGD(TAG, - "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n" - "MBR param page addr 0x%08x, UICR param page addr 0x%08x", - read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR), + ESP_LOGD(TAG, " MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), + NRF_UICR->NRFFW[0]); + ESP_LOGD(TAG, " MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); if (is_sd_present()) { uint32_t const sd_id = sd_id_get(); @@ -326,7 +326,7 @@ size_t DebugComponent::get_device_info_(std::span ver[1] = (sd_version - ver[0] * 1000000) / 1000; ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000); - ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); + ESP_LOGD(TAG, " SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); #ifdef USE_SOFTDEVICE_ID #ifdef USE_SOFTDEVICE_VERSION if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) { @@ -352,10 +352,8 @@ size_t DebugComponent::get_device_info_(std::span } return res; }; - ESP_LOGD(TAG, - "NRFFW %s\n" - "NRFHW %s", - uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); return pos; } diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 2c44c6fba9..0f69c82f39 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -187,18 +187,18 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated detection area config:\n" - "Detection area 1 from %.02fm to %.02fm.", + " Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_); if (this->min2_ >= 0 && this->max2_ >= 0) { - ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); + ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); } if (this->min3_ >= 0 && this->max3_ >= 0) { - ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); + ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); } if (this->min4_ >= 0 && this->max4_ >= 0) { - ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); + ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); } - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet. @@ -222,10 +222,10 @@ uint8_t SetLatencyCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated output latency config:\n" - "Signal that someone was detected is delayed by %.03f s.\n" - "Signal that nobody is detected anymore is delayed by %.03f s.", + " Signal that someone was detected is delayed by %.03f s.\n" + " Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_detection_, this->delay_after_disappear_); - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index 5286f962b8..d3e923cbef 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -153,10 +153,7 @@ void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::Remote bool EmmetiClimate::check_checksum_(uint8_t checksum) { uint8_t expected = this->gen_checksum_(); - ESP_LOGV(TAG, - "Expected checksum: %X\n" - "Checksum received: %X", - expected, checksum); + ESP_LOGV(TAG, "Expected checksum: %X, Checksum received: %X", expected, checksum); return checksum == expected; } @@ -266,10 +263,7 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Swing: %d\n" - "Sleep: %d", - (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); + ESP_LOGD(TAG, "Swing: %d, Sleep: %d", (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); for (size_t pos = 0; pos < 4; pos++) { if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { @@ -295,13 +289,8 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Turbo: %d\n" - "Light: %d\n" - "Tree: %d\n" - "Blow: %d", - (curr_state.bitmap >> 3) & 0x01, (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, - (curr_state.bitmap >> 6) & 0x01); + ESP_LOGD(TAG, "Turbo: %d, Light: %d, Tree: %d, Blow: %d", (curr_state.bitmap >> 3) & 0x01, + (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, (curr_state.bitmap >> 6) & 0x01); uint16_t control_data = 0; for (size_t pos = 0; pos < 11; pos++) { diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 785b053f04..e1cee5005c 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -152,12 +152,13 @@ void ENS160Component::update() { // verbose status logging ESP_LOGV(TAG, - "Status: ENS160 STATAS bit 0x%x\n" - "Status: ENS160 STATER bit 0x%x\n" - "Status: ENS160 VALIDITY FLAG 0x%02x\n" - "Status: ENS160 NEWDAT bit 0x%x\n" - "Status: ENS160 NEWGPR bit 0x%x", - (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, + "ENS160 Status Register: 0x%02x\n" + " STATAS bit 0x%x\n" + " STATER bit 0x%x\n" + " VALIDITY FLAG 0x%02x\n" + " NEWDAT bit 0x%x\n" + " NEWGPR bit 0x%x", + status_value, (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER, (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2, (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT, diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 9deb29416f..72026a2a84 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -209,9 +209,10 @@ bool ES8388::set_dac_output(DacOutputLine line) { }; ESP_LOGV(TAG, - "Setting ES8388_DACPOWER to 0x%02X\n" - "Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X\n" - "Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X", + "DAC output config:\n" + " DACPOWER: 0x%02X\n" + " DACCONTROL24/25: 0x%02X\n" + " DACCONTROL26/27: 0x%02X", dac_power, reg_out1, reg_out2); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL24, reg_out1)); // LOUT1VOL diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 3f0eeeab4a..e6a85c784a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -423,10 +423,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ for (auto &svc : this->services_) { char uuid_buf[espbt::UUID_STR_LEN]; svc->uuid.to_str(uuid_buf); - ESP_LOGV(TAG, - "[%d] [%s] Service UUID: %s\n" - "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", - this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_, + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, svc->start_handle, svc->end_handle); } #endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index c8e2e879d4..dcd6e643c2 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -106,11 +106,12 @@ void Esp32HostedUpdate::setup() { esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { ESP_LOGD(TAG, - "Firmware version: %s\n" - "Project name: %s\n" - "Build date: %s\n" - "Build time: %s\n" - "IDF version: %s", + "ESP32 Hosted firmware:\n" + " Firmware version: %s\n" + " Project name: %s\n" + " Build date: %s\n" + " Build time: %s\n" + " IDF version: %s", app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver); this->update_info_.latest_version = app_desc->version; if (this->update_info_.latest_version != this->update_info_.current_version) { diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 3d16f28c7d..6e4f606466 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -21,11 +21,9 @@ void ESPNowTransport::setup() { return; } - ESP_LOGI(TAG, - "Registering ESP-NOW handlers\n" - "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", - this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], - this->peer_address_[4], this->peer_address_[5]); + ESP_LOGI(TAG, "Registering ESP-NOW handlers, peer: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], + this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], + this->peer_address_[5]); // Register received handler this->parent_->register_received_handler(this); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f9d98ad51b..fcd32223e4 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -866,10 +866,7 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi } #endif - ESP_LOGD(TAG, - "Writing to PHY Register Address: 0x%02" PRIX32 "\n" - "Writing to PHY Register Value: 0x%04" PRIX32, - register_data.address, register_data.value); + ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value); err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 9d2f4fc687..bf6e3926b8 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -150,9 +150,9 @@ void EzoPMP::read_command_result_() { if (current_char == '\0') { ESP_LOGV(TAG, "Read Response from device: %s\n" - "First Component: %s\n" - "Second Component: %s\n" - "Third Component: %s", + " First Component: %s\n" + " Second Component: %s\n" + " Third Component: %s", (char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer, (char *) third_parameter_buffer); diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index f7f7f8d02c..43b2fa20d3 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -97,10 +97,10 @@ void GCJA5Component::parse_data_() { ESP_LOGI(TAG, "GCJA5 Status\n" - "Overall Status : %i\n" - "PD Status : %i\n" - "LD Status : %i\n" - "Fan Status : %i", + " Overall Status : %i\n" + " PD Status : %i\n" + " LD Status : %i\n" + " Fan Status : %i", (status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03); } } diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 2b120a746f..cf1672f217 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -38,13 +38,13 @@ void GraphicalDisplayMenu::setup() { void GraphicalDisplayMenu::dump_config() { ESP_LOGCONFIG(TAG, "Graphical Display Menu\n" - "Has Display: %s\n" - "Popup Mode: %s\n" - "Advanced Drawing Mode: %s\n" - "Has Font: %s\n" - "Mode: %s\n" - "Active: %s\n" - "Menu items:", + " Has Display: %s\n" + " Popup Mode: %s\n" + " Advanced Drawing Mode: %s\n" + " Has Font: %s\n" + " Mode: %s\n" + " Active: %s\n" + " Menu items:", YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr), YESNO(this->font_ != nullptr), this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_)); diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index c204325dfc..58c5df230f 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -35,10 +35,7 @@ uint8_t HONEYWELLABPSensor::readsensor_() { pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); - ESP_LOGV(TAG, - "Sensor pressure_count_ %d\n" - "Sensor temperature_count_ %d", - pressure_count_, temperature_count_); + ESP_LOGV(TAG, "Sensor pressure_count_ %d, temperature_count_ %d", pressure_count_, temperature_count_); } return status_; } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 882def4d7f..d77a768211 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -105,8 +105,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it md5_receive.init(); - ESP_LOGV(TAG, "MD5Digest initialized\n" - "OTA backend begin"); + ESP_LOGV(TAG, "MD5Digest initialized, OTA backend begin"); auto backend = ota::make_ota_backend(); auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index de01c99a19..8a20192c1e 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -362,10 +362,8 @@ bool INA2XX::configure_shunt_() { ESP_LOGW(TAG, "Shunt value too high"); } this->shunt_cal_ &= 0x7FFF; - ESP_LOGV(TAG, - "Given Rshunt=%f Ohm and Max_current=%.3f\n" - "New CURRENT_LSB=%f, SHUNT_CAL=%u", - this->shunt_resistance_ohm_, this->max_current_a_, this->current_lsb_, this->shunt_cal_); + ESP_LOGV(TAG, "Rshunt=%f Ohm, max current=%.3f A, current LSB=%f, shunt cal=%u", this->shunt_resistance_ohm_, + this->max_current_a_, this->current_lsb_, this->shunt_cal_); return this->write_unsigned_16_(RegisterMap::REG_SHUNT_CAL, this->shunt_cal_); } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index f8f782f804..dd1d53857d 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -489,11 +489,8 @@ bool LD2410Component::handle_ack_data_() { this->out_pin_level_ = this->buffer_data_[12]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u\n" - "Out pin level: %s", - light_function_str, this->light_threshold_, out_pin_level_str); + ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_, + out_pin_level_str); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 95e19e0d5f..484d5bd281 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -530,10 +530,7 @@ bool LD2412Component::handle_ack_data_() { this->light_function_ = this->buffer_data_[10]; this->light_threshold_ = this->buffer_data_[11]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u", - light_function_str, this->light_threshold_); + ESP_LOGV(TAG, "Light function: %s, threshold: %u", light_function_str, this->light_threshold_); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a203dde115..a01d42ac8b 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -130,10 +130,8 @@ void LEDCOutput::setup() { } int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); - ESP_LOGV(TAG, - "Configured frequency %f with a bit depth of %u bits\n" - "Angle of %.1f° results in hpoint %u", - this->frequency_, this->bit_depth_, this->phase_angle_, hpoint); + ESP_LOGV(TAG, "Configured frequency %f with bit depth %u, angle %.1f° hpoint %u", this->frequency_, this->bit_depth_, + this->phase_angle_, hpoint); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = this->pin_->get_pin(); diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index 6ba17f11d1..a350e66ee0 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -146,11 +146,11 @@ void MAX6956::dump_config() { if (brightness_mode_ == MAX6956CURRENTMODE::GLOBAL) { ESP_LOGCONFIG(TAG, - "current mode: global\n" - "global brightness: %u", + " Current mode: global\n" + " Brightness: %u", global_brightness_); } else { - ESP_LOGCONFIG(TAG, "current mode: segment"); + ESP_LOGCONFIG(TAG, " Current mode: segment"); } } diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index c12c79499f..8a7fb965e9 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -165,10 +165,7 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); if (event.error_handle.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { - ESP_LOGE(TAG, - "Last error code reported from esp-tls: 0x%x\n" - "Last tls stack error number: 0x%x\n" - "Last captured errno : %d (%s)", + ESP_LOGE(TAG, "Last esp-tls error: 0x%x, tls stack error: 0x%x, socket errno: %d (%s)", event.error_handle.esp_tls_last_esp_err, event.error_handle.esp_tls_stack_err, event.error_handle.esp_transport_sock_errno, strerror(event.error_handle.esp_transport_sock_errno)); } else if (event.error_handle.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index a433eff883..46a04c1b2e 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -25,11 +25,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGE(TAG, "Invalid range"); - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -138,11 +134,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -172,10 +164,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - EspClass::getFreeHeap()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap()); HTTPClient http_client; http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along @@ -262,10 +251,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->reset_(false); delay(250); // NOLINT - ESP_LOGV(TAG, - "Heap: %" PRIu32 "\n" - "Upload cmd: %s", - EspClass::getFreeHeap(), command); + ESP_LOGV(TAG, "Heap: %" PRIu32 ", upload cmd: %s", EspClass::getFreeHeap(), command); this->send_command_(command); if (baud_rate != this->original_baud_rate_) { diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 46352afd75..43f59a8d4b 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -27,11 +27,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); - ESP_LOGE(TAG, "Invalid range"); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -159,11 +155,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -193,10 +185,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, @@ -220,10 +209,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Perform the HTTP request - ESP_LOGV(TAG, - "Check connection\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check connection, heap: %" PRIu32, esp_get_free_heap_size()); err = esp_http_client_perform(http_client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP failed: %s", esp_err_to_name(err)); @@ -232,10 +218,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Check the HTTP Status Code - ESP_LOGV(TAG, - "Check status\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check status, heap: %" PRIu32, esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); @@ -344,8 +327,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, esp_get_free_heap_size(), this->content_length_); } - ESP_LOGD(TAG, "TFT upload complete\n" - "Close HTTP"); + ESP_LOGD(TAG, "TFT upload complete, closing HTTP"); esp_http_client_close(http_client); esp_http_client_cleanup(http_client); ESP_LOGV(TAG, "Connection closed"); diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index bb70701471..9dd68a1ccc 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -130,10 +130,7 @@ void OpenThreadComponent::ot_main() { } #ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG link_mode_config = otThreadGetLinkMode(instance); - ESP_LOGD(TAG, - "Link Mode Device Type: %s\n" - "Link Mode Network Data: %s\n" - "Link Mode RX On When Idle: %s", + ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s", TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), TRUEFALSE(link_mode_config.mRxOnWhenIdle)); #endif @@ -152,8 +149,7 @@ void OpenThreadComponent::ot_main() { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; } else { - ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration\n" - "(set force_dataset: true to override)"); + ESP_LOGI(TAG, "Found existing dataset, ignoring config (force_dataset: true to override)"); } #endif diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index fdff11dedb..9247e114f0 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -144,9 +144,10 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, - "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + "Wrote GPIO config:\n" + " modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pull enable: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), BYTE_TO_BINARY(this->pull_enable_mask_)); #endif diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index e6831ad19e..f95bf4aedb 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -748,8 +748,7 @@ esphome::optional Pipsolar::get_bit_(std::string bits, uint8_t bit_pos) { } void Pipsolar::dump_config() { - ESP_LOGCONFIG(TAG, "Pipsolar:\n" - "enabled polling commands:"); + ESP_LOGCONFIG(TAG, "Pipsolar enabled polling commands:"); for (auto &enabled_polling_command : this->enabled_polling_commands_) { if (enabled_polling_command.length != 0) { ESP_LOGCONFIG(TAG, "%s", enabled_polling_command.command); diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 5366aab54e..1ab0da3df7 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -31,10 +31,7 @@ void PN532::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, - "Found chip PN5%02X\n" - "Firmware ver. %d.%d", - version_data[0], version_data[1], version_data[2]); + ESP_LOGD(TAG, "Found chip PN5%02X, Firmware v%d.%d", version_data[0], version_data[1], version_data[2]); if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 7bec1e08a9..8c76c8b88c 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -243,9 +243,7 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); @@ -274,11 +272,12 @@ uint8_t PN7150::init_core_() { uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; ESP_LOGD(TAG, - "Manufacturer ID: 0x%02X\n" - "Hardware version: 0x%02X\n" - "ROM code version: 0x%02X\n" - "FLASH major version: 0x%02X\n" - "FLASH minor version: 0x%02X", + "PN7150 chip info:\n" + " Manufacturer ID: 0x%02X\n" + " Hardware version: 0x%02X\n" + " ROM code version: 0x%02X\n" + " FLASH major version: 0x%02X\n" + " FLASH minor version: 0x%02X", manf_id, hw_version, rom_code_version, flash_major_version, flash_minor_version); return rx.get_simple_status_response(); diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 28907b8e30..3fcd1221a7 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -265,10 +265,7 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s\n" - "Manufacturer ID: 0x%02X", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s, Manufacturer ID: 0x%02X", rx.get_message()[4] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0", rx.get_message()[6]); rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); @@ -301,11 +298,12 @@ uint8_t PN7160::init_core_() { char feat_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGD(TAG, - "Hardware version: %u\n" - "ROM code version: %u\n" - "FLASH major version: %u\n" - "FLASH minor version: %u\n" - "Features: %s", + "PN7160 chip info:\n" + " Hardware version: %u\n" + " ROM code version: %u\n" + " FLASH major version: %u\n" + " FLASH minor version: %u\n" + " Features: %s", hw_version, rom_code_version, flash_major_version, flash_minor_version, nfc::format_bytes_to(feat_buf, features)); diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 4e1ef27d5e..24fe34e785 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -128,15 +128,14 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]); ESP_LOGV(TAG, - "<-----------calibration data-------------->\n" - "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]", + "Calibration data:\n" + " COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\n" + " COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\n" + " COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]", qmp6988_data_.qmp6988_cali.COE_a0, qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, - qmp6988_data_.qmp6988_cali.COE_b00); - ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1, - qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11); - ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2, + qmp6988_data_.qmp6988_cali.COE_b00, qmp6988_data_.qmp6988_cali.COE_bt1, qmp6988_data_.qmp6988_cali.COE_bt2, + qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11, qmp6988_data_.qmp6988_cali.COE_bp2, qmp6988_data_.qmp6988_cali.COE_b12, qmp6988_data_.qmp6988_cali.COE_b21, qmp6988_data_.qmp6988_cali.COE_bp3); - ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4 qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4 @@ -153,14 +152,13 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, - "<----------- int calibration data -------------->\n" - "a0[%d] a1[%d] a2[%d] b00[%d]", - qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00); - ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2, - qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11); - ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, + "Int calibration data:\n" + " a0[%d] a1[%d] a2[%d] b00[%d]\n" + " bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\n" + " bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]", + qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00, qmp6988_data_.ik.bt1, + qmp6988_data_.ik.bt2, qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11, qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, qmp6988_data_.ik.b21, qmp6988_data_.ik.bp3); - ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); return true; } diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index cff3145199..43029cbc2f 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -104,10 +104,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vectorlabel); + ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label); + ESP_LOGW(TAG, "The device reset before the boot was marked successful"); if (esp_reset_reason() == ESP_RST_BROWNOUT) { - ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" - "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!"); + ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); } } #endif diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 251e18648b..2115c72cef 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -326,7 +326,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (ok || message.compare(0, 6, "+CMGL:") == 0) { ESP_LOGD(TAG, "Received SMS from: %s\n" - "%s", + " %s", this->sender_.c_str(), this->message_.c_str()); this->sms_received_callback_.call(this->message_, this->sender_); this->state_ = STATE_RECEIVED_SMS; diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index 7b99086546..03586b6398 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -93,10 +93,7 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { if (this->read_array(cmd, 6)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(6)]; - ESP_LOGV(TAG, - "[%04d] Reading from dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); + ESP_LOGV(TAG, "[%04d] Reading from dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); #endif if (cmd[0] != 0xAA || cmd[1] != 0x55) { @@ -190,10 +187,7 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a do { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; - ESP_LOGV(TAG, - "[%04d] Writing to the dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); + ESP_LOGV(TAG, "[%04d] Writing to the dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); #endif this->write_array(cmd, len); this->write_count_++; diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index e8fc4e44d1..d55a14f192 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -174,20 +174,21 @@ struct SunAtTime { // debug output like in example 25.a, p. 165 auto eq = equatorial_coordinate(); ESP_LOGV(TAG, - "jde: %f\n" - "T: %f\n" - "L_0: %f\n" - "M: %f\n" - "e: %f\n" - "C: %f\n" - "Odot: %f\n" - "Omega: %f\n" - "lambda: %f\n" - "epsilon_0: %f\n" - "epsilon: %f\n" - "v: %f\n" - "right_ascension: %f\n" - "declination: %f", + "Sun position:\n" + " jde: %f\n" + " T: %f\n" + " L_0: %f\n" + " M: %f\n" + " e: %f\n" + " C: %f\n" + " Odot: %f\n" + " Omega: %f\n" + " lambda: %f\n" + " epsilon_0: %f\n" + " epsilon: %f\n" + " v: %f\n" + " right_ascension: %f\n" + " declination: %f", jde, t, mean_longitude(), mean_anomaly(), eccentricity(), equation_of_center(), true_longitude(), omega(), apparent_longitude(), mean_obliquity(), true_obliquity(), true_anomaly(), eq.right_ascension, eq.declination); diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index a9be38fb03..422d74095c 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -70,14 +70,14 @@ static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) { static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) { ESP_LOGV(TAG, "*** Configuration descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "wTotalLength %d\n" - "bNumInterfaces %d\n" - "bConfigurationValue %d\n" - "iConfiguration %d\n" - "bmAttributes 0x%x\n" - "bMaxPower %dmA", + " bLength %d\n" + " bDescriptorType %d\n" + " wTotalLength %d\n" + " bNumInterfaces %d\n" + " bConfigurationValue %d\n" + " iConfiguration %d\n" + " bmAttributes 0x%x\n" + " bMaxPower %dmA", cfg_desc->bLength, cfg_desc->bDescriptorType, cfg_desc->wTotalLength, cfg_desc->bNumInterfaces, cfg_desc->bConfigurationValue, cfg_desc->iConfiguration, cfg_desc->bmAttributes, cfg_desc->bMaxPower * 2); } @@ -89,20 +89,20 @@ static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_des ESP_LOGV(TAG, "*** Device descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "bcdUSB %d.%d0\n" - "bDeviceClass 0x%x\n" - "bDeviceSubClass 0x%x\n" - "bDeviceProtocol 0x%x\n" - "bMaxPacketSize0 %d\n" - "idVendor 0x%x\n" - "idProduct 0x%x\n" - "bcdDevice %d.%d0\n" - "iManufacturer %d\n" - "iProduct %d\n" - "iSerialNumber %d\n" - "bNumConfigurations %d", + " bLength %d\n" + " bDescriptorType %d\n" + " bcdUSB %d.%d0\n" + " bDeviceClass 0x%x\n" + " bDeviceSubClass 0x%x\n" + " bDeviceProtocol 0x%x\n" + " bMaxPacketSize0 %d\n" + " idVendor 0x%x\n" + " idProduct 0x%x\n" + " bcdDevice %d.%d0\n" + " iManufacturer %d\n" + " iProduct %d\n" + " iSerialNumber %d\n" + " bNumConfigurations %d", devc_desc->bLength, devc_desc->bDescriptorType, ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF), devc_desc->bDeviceClass, devc_desc->bDeviceSubClass, devc_desc->bDeviceProtocol, devc_desc->bMaxPacketSize0, devc_desc->idVendor, devc_desc->idProduct, diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index 483286560a..fa8c2331b2 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -58,10 +58,8 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev ESP_LOGE(TAG, "get_active_config_descriptor failed"); return {}; } - ESP_LOGD(TAG, - "bDeviceClass: %u, bDeviceSubClass: %u\n" - "bNumInterfaces: %u", - device_desc->bDeviceClass, device_desc->bDeviceSubClass, config_desc->bNumInterfaces); + ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u, bNumInterfaces: %u", device_desc->bDeviceClass, + device_desc->bDeviceSubClass, config_desc->bNumInterfaces); if (device_desc->bDeviceClass != 0) { ESP_LOGE(TAG, "bDeviceClass != 0"); return {}; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 641d4d6ff8..d6cbfd4b21 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -434,8 +434,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr 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)", + " Current client: %s (%s)\n" + " New client: %s (%s)", this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(), client->get_peername_to(new_peername)); return; diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index a589f71c84..a902adfddd 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -70,12 +70,13 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1); ESP_LOGV(TAG, - "Tag id: %012lld\n" - "Country: %03d\n" - "isData: %s\n" - "isAnimal: %s\n" - "Reserved0: %d\n" - "Reserved1: %" PRId32, + "RFID134 Tag:\n" + " Tag id: %012lld\n" + " Country: %03d\n" + " isData: %s\n" + " isAnimal: %s\n" + " Reserved0: %d\n" + " Reserved1: %" PRId32, reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false", reading.reserved0, reading.reserved1); diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index b5b786c105..5e816469ac 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -72,10 +72,8 @@ void XGZP68XXComponent::update() { temperature_raw = encode_uint16(data[3], data[4]); // Convert the pressure data to hPa - ESP_LOGV(TAG, - "Got raw pressure=%" PRIu32 ", raw temperature=%u\n" - "K value is %u", - pressure_raw, temperature_raw, this->k_value_); + ESP_LOGV(TAG, "Got raw pressure=%" PRIu32 ", raw temperature=%u, K value=%u", pressure_raw, temperature_raw, + this->k_value_); // Sign extend the pressure float pressure_in_pa = (float) (((int32_t) pressure_raw << 8) >> 8); diff --git a/script/ci-custom.py b/script/ci-custom.py index df819e0f04..85a446ba0d 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -841,6 +841,39 @@ def lint_no_scanf(fname, match): ) +LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) +LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') +LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') + + +@lint_content_check(include=cpp_include) +def lint_log_multiline_continuation(fname, content): + errs = [] + for log_match in LOG_MULTILINE_RE.finditer(content): + log_text = log_match.group(0) + for bad_match in LOG_BAD_CONTINUATION_RE.finditer(log_text): + # %s may expand to a whitespace prefix at runtime, skip those + if LOG_PERCENT_S_CONTINUATION_RE.match(log_text, bad_match.start()): + continue + # Calculate line number from position in full content + abs_pos = log_match.start() + bad_match.start() + lineno = content.count("\n", 0, abs_pos) + 1 + col = abs_pos - content.rfind("\n", 0, abs_pos) + errs.append( + ( + lineno, + col, + "Multi-line log message has a continuation line that does " + "not start with a space. The log viewer uses leading " + "whitespace to detect continuation lines and re-add the " + f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" + "Either start the continuation with a space/indent, or " + "split into separate ESP_LOG* calls.", + ) + ) + return errs + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 1f945a334a472420d3f3e62c453f3ab3c5246c27 Mon Sep 17 00:00:00 2001 From: Joshua Sing Date: Tue, 24 Feb 2026 04:01:23 +1100 Subject: [PATCH 80/89] [hdc302x] Add new component (#10160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> 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> --- CODEOWNERS | 1 + esphome/components/hdc302x/__init__.py | 1 + esphome/components/hdc302x/hdc302x.cpp | 171 ++++++++++++++++++ esphome/components/hdc302x/hdc302x.h | 68 +++++++ esphome/components/hdc302x/sensor.py | 135 ++++++++++++++ tests/components/hdc302x/common.yaml | 19 ++ tests/components/hdc302x/test.esp32-idf.yaml | 4 + .../components/hdc302x/test.esp8266-ard.yaml | 4 + tests/components/hdc302x/test.rp2040-ard.yaml | 4 + 9 files changed, 407 insertions(+) create mode 100644 esphome/components/hdc302x/__init__.py create mode 100644 esphome/components/hdc302x/hdc302x.cpp create mode 100644 esphome/components/hdc302x/hdc302x.h create mode 100644 esphome/components/hdc302x/sensor.py create mode 100644 tests/components/hdc302x/common.yaml create mode 100644 tests/components/hdc302x/test.esp32-idf.yaml create mode 100644 tests/components/hdc302x/test.esp8266-ard.yaml create mode 100644 tests/components/hdc302x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 2aa0656343..6728e76bba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -213,6 +213,7 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb +esphome/components/hdc302x/* @joshuasing esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal diff --git a/esphome/components/hdc302x/__init__.py b/esphome/components/hdc302x/__init__.py new file mode 100644 index 0000000000..d8165281bf --- /dev/null +++ b/esphome/components/hdc302x/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@joshuasing"] diff --git a/esphome/components/hdc302x/hdc302x.cpp b/esphome/components/hdc302x/hdc302x.cpp new file mode 100644 index 0000000000..b50d34169a --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.cpp @@ -0,0 +1,171 @@ +#include "hdc302x.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::hdc302x { + +static const char *const TAG = "hdc302x.sensor"; + +// Commands (per datasheet Table 7-4) +static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2}; +static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41}; + +static const uint8_t HDC302X_CMD_TRIGGER_MSB = 0x24; + +static const uint8_t HDC302X_CMD_HEATER_ENABLE[2] = {0x30, 0x6d}; +static const uint8_t HDC302X_CMD_HEATER_DISABLE[2] = {0x30, 0x66}; +static const uint8_t HDC302X_CMD_HEATER_CONFIGURE[2] = {0x30, 0x6e}; + +void HDC302XComponent::setup() { + // Soft reset the device + if (this->write(HDC302X_CMD_SOFT_RESET, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Soft reset failed")); + return; + } + // Delay SensorRR (reset ready), per datasheet, 6.5. + delay(3); + + // Clear status register + if (this->write(HDC302X_CMD_CLEAR_STATUS_REGISTER, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Clear status failed")); + return; + } +} + +void HDC302XComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "HDC302x:\n" + " Heater: %s", + this->heater_active_ ? "active" : "inactive"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temp_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC302XComponent::update() { + uint8_t cmd[] = { + HDC302X_CMD_TRIGGER_MSB, + this->power_mode_, + }; + if (this->write(cmd, 2) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Read data after ADC conversion has completed + this->set_timeout(this->conversion_delay_ms_(), [this]() { this->read_data_(); }); +} + +void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) { + if (!this->disable_heater_()) { + ESP_LOGD(TAG, "Heater disable before start failed"); + } + if (!this->configure_heater_(power) || !this->enable_heater_()) { + ESP_LOGW(TAG, "Heater start failed"); + return; + } + this->heater_active_ = true; + this->cancel_timeout("heater_off"); + if (duration_ms > 0) { + this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); }); + } +} + +void HDC302XComponent::stop_heater() { + this->cancel_timeout("heater_off"); + if (!this->disable_heater_()) { + ESP_LOGW(TAG, "Heater stop failed"); + } + this->heater_active_ = false; +} + +bool HDC302XComponent::enable_heater_() { + if (this->write(HDC302X_CMD_HEATER_ENABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Enable heater failed"); + return false; + } + return true; +} + +bool HDC302XComponent::configure_heater_(uint16_t power_level) { + if (power_level > 0x3fff) { + ESP_LOGW(TAG, "Heater power 0x%04x exceeds max 0x3fff", power_level); + return false; + } + + // Heater current level config. + uint8_t config[] = { + static_cast((power_level >> 8) & 0xff), // MSB + static_cast(power_level & 0xff) // LSB + }; + + // Configure level of heater current (per datasheet 7.5.7.8). + uint8_t cmd[] = { + HDC302X_CMD_HEATER_CONFIGURE[0], HDC302X_CMD_HEATER_CONFIGURE[1], config[0], config[1], + crc8(config, 2, 0xff, 0x31, true), + }; + if (this->write(cmd, sizeof(cmd)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Configure heater failed"); + return false; + } + + return true; +} + +bool HDC302XComponent::disable_heater_() { + if (this->write(HDC302X_CMD_HEATER_DISABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Disable heater failed"); + return false; + } + return true; +} + +void HDC302XComponent::read_data_() { + uint8_t buf[6]; + if (this->read(buf, 6) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Check checksums + if (crc8(buf, 2, 0xff, 0x31, true) != buf[2] || crc8(buf + 3, 2, 0xff, 0x31, true) != buf[5]) { + this->status_set_warning(LOG_STR("Read data: invalid CRC")); + return; + } + + this->status_clear_warning(); + + if (this->temp_sensor_ != nullptr) { + uint16_t raw_t = encode_uint16(buf[0], buf[1]); + // Calculate temperature in Celsius per datasheet section 7.3.3. + float temp = -45 + 175 * (float(raw_t) / 65535.0f); + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + uint16_t raw_rh = encode_uint16(buf[3], buf[4]); + // Calculate RH% per datasheet section 7.3.3. + float humidity = 100 * (float(raw_rh) / 65535.0f); + this->humidity_sensor_->publish_state(humidity); + } +} + +uint32_t HDC302XComponent::conversion_delay_ms_() { + // ADC conversion delay per datasheet, Table 7-5. - Trigger on Demand + switch (this->power_mode_) { + case HDC302XPowerMode::BALANCED: + return 8; + case HDC302XPowerMode::LOW_POWER: + return 5; + case HDC302XPowerMode::ULTRA_LOW_POWER: + return 4; + case HDC302XPowerMode::HIGH_ACCURACY: + default: + return 13; + } +} + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/hdc302x.h b/esphome/components/hdc302x/hdc302x.h new file mode 100644 index 0000000000..6afea0a8c0 --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::hdc302x { + +enum HDC302XPowerMode : uint8_t { + HIGH_ACCURACY = 0x00, + BALANCED = 0x0b, + LOW_POWER = 0x16, + ULTRA_LOW_POWER = 0xff, +}; + +/** + HDC302x Temperature and humidity sensor. + + Datasheet: + https://www.ti.com/lit/ds/symlink/hdc3020.pdf + */ +class HDC302XComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + + void start_heater(uint16_t power, uint32_t duration_ms); + void stop_heater(); + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void set_power_mode(HDC302XPowerMode power_mode) { this->power_mode_ = power_mode; } + + protected: + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + HDC302XPowerMode power_mode_{HDC302XPowerMode::HIGH_ACCURACY}; + bool heater_active_{false}; + + bool enable_heater_(); + bool configure_heater_(uint16_t power_level); + bool disable_heater_(); + void read_data_(); + uint32_t conversion_delay_ms_(); +}; + +template class HeaterOnAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, power) + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto power_val = this->power_.value(x...); + auto duration_val = this->duration_.value(x...); + this->parent_->start_heater(power_val, duration_val); + } +}; + +template class HeaterOffAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->stop_heater(); } +}; + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py new file mode 100644 index 0000000000..7215a4cfb7 --- /dev/null +++ b/esphome/components/hdc302x/sensor.py @@ -0,0 +1,135 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DURATION, + CONF_HUMIDITY, + CONF_ID, + CONF_POWER, + CONF_POWER_MODE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc302x_ns = cg.esphome_ns.namespace("hdc302x") +HDC302XComponent = hdc302x_ns.class_( + "HDC302XComponent", cg.PollingComponent, i2c.I2CDevice +) + +HDC302XPowerMode = hdc302x_ns.enum("HDC302XPowerMode") +POWER_MODE_OPTIONS = { + "HIGH_ACCURACY": HDC302XPowerMode.HIGH_ACCURACY, + "BALANCED": HDC302XPowerMode.BALANCED, + "LOW_POWER": HDC302XPowerMode.LOW_POWER, + "ULTRA_LOW_POWER": HDC302XPowerMode.ULTRA_LOW_POWER, +} + +# Actions +HeaterOnAction = hdc302x_ns.class_("HeaterOnAction", automation.Action) +HeaterOffAction = hdc302x_ns.class_("HeaterOffAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC302XComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_MODE, default="HIGH_ACCURACY"): cv.enum( + POWER_MODE_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) # Default address per datasheet, Table 7-2. +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temp_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_config) + cg.add(var.set_temp_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + + +# HDC302x heater power configs, per datasheet Table 7-15. +HDC302X_HEATER_POWER_MAP = { + "QUARTER": 0x009F, + "HALF": 0x03FF, + "FULL": 0x3FFF, +} + + +def heater_power_value(value): + """Accept enum names or raw uint16 values""" + if isinstance(value, cv.Lambda): + return value + if isinstance(value, str): + upper = value.upper() + if upper in HDC302X_HEATER_POWER_MAP: + return HDC302X_HEATER_POWER_MAP[upper] + raise cv.Invalid( + f"Unknown heater power preset: {value}. Use QUARTER, HALF, FULL, or a raw value 0-16383" + ) + return cv.int_range(min=0, max=0x3FFF)(value) + + +HDC302X_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(HDC302XComponent)}) + +HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HDC302XComponent), + cv.Optional(CONF_POWER, default="QUARTER"): cv.templatable(heater_power_value), + cv.Optional(CONF_DURATION, default="5s"): cv.templatable( + cv.positive_time_period_milliseconds + ), + } +) + + +@automation.register_action( + "hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA +) +async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_POWER], args, cg.uint16) + cg.add(var.set_power(template_)) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA +) +async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/tests/components/hdc302x/common.yaml b/tests/components/hdc302x/common.yaml new file mode 100644 index 0000000000..56bb31effe --- /dev/null +++ b/tests/components/hdc302x/common.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - hdc302x.heater_on: + id: hdc302x_sensor + power: QUARTER + duration: 5s + - hdc302x.heater_off: + id: hdc302x_sensor + +sensor: + - platform: hdc302x + id: hdc302x_sensor + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc302x/test.esp32-idf.yaml b/tests/components/hdc302x/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/hdc302x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.esp8266-ard.yaml b/tests/components/hdc302x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/hdc302x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.rp2040-ard.yaml b/tests/components/hdc302x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/hdc302x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From e199145f1c3b168e8584c4613f1d814c1694a4f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 12:20:55 -0600 Subject: [PATCH 81/89] [core] Avoid expensive modulo in LockFreeQueue for non-power-of-2 sizes (#14221) --- esphome/core/lock_free_queue.h | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index e96b739b58..522fbd36e1 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -38,13 +38,27 @@ template class LockFreeQueue { } protected: + // Advance ring buffer index by one, wrapping at SIZE. + // Power-of-2 sizes use modulo (compiler emits single mask instruction). + // Non-power-of-2 sizes use comparison to avoid expensive multiply-shift sequences. + static constexpr uint8_t next_index(uint8_t index) { + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (index + 1) % SIZE; + } else { + uint8_t next = index + 1; + if (next >= SIZE) [[unlikely]] + next = 0; + return next; + } + } + // Internal push that reports queue state - for use by derived classes bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) { if (element == nullptr) return false; uint8_t current_tail = tail_.load(std::memory_order_relaxed); - uint8_t next_tail = (current_tail + 1) % SIZE; + uint8_t next_tail = next_index(current_tail); // Read head before incrementing tail uint8_t head_before = head_.load(std::memory_order_acquire); @@ -73,14 +87,21 @@ template class LockFreeQueue { } T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); + head_.store(next_index(current_head), std::memory_order_release); return element; } size_t size() const { uint8_t tail = tail_.load(std::memory_order_acquire); uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (tail - head + SIZE) % SIZE; + } else { + int diff = static_cast(tail) - static_cast(head); + if (diff < 0) + diff += SIZE; + return static_cast(diff); + } } uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } @@ -90,7 +111,7 @@ template class LockFreeQueue { bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } bool full() const { - uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + uint8_t next_tail = next_index(tail_.load(std::memory_order_relaxed)); return next_tail == head_.load(std::memory_order_acquire); } From 0d32a5321ca20943eea2d5c9c9d3b3aeac44b568 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:46:53 -0500 Subject: [PATCH 82/89] [remote_transmitter/remote_receiver] Rename _esp32.cpp to _rmt.cpp (#14226) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/remote_receiver/__init__.py | 2 +- .../{remote_receiver_esp32.cpp => remote_receiver_rmt.cpp} | 0 esphome/components/remote_transmitter/__init__.py | 2 +- ...{remote_transmitter_esp32.cpp => remote_transmitter_rmt.cpp} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename esphome/components/remote_receiver/{remote_receiver_esp32.cpp => remote_receiver_rmt.cpp} (100%) rename esphome/components/remote_transmitter/{remote_transmitter_esp32.cpp => remote_transmitter_rmt.cpp} (100%) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 362f6e99db..53a0f8fb77 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -237,7 +237,7 @@ async def to_code(config): FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "remote_receiver_esp32.cpp": { + "remote_receiver_rmt.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp similarity index 100% rename from esphome/components/remote_receiver/remote_receiver_esp32.cpp rename to esphome/components/remote_receiver/remote_receiver_rmt.cpp diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index fc772f88b2..371dbb685f 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -171,7 +171,7 @@ async def to_code(config): FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "remote_transmitter_esp32.cpp": { + "remote_transmitter_rmt.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp similarity index 100% rename from esphome/components/remote_transmitter/remote_transmitter_esp32.cpp rename to esphome/components/remote_transmitter/remote_transmitter_rmt.cpp From daee71a2c17871b1f23e782059ae9e906abeb9f2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:21:29 -0500 Subject: [PATCH 83/89] [http_request] Retry update check on startup until network is ready (#14228) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../update/http_request_update.cpp | 24 ++++++++++++++++++- .../http_request/update/http_request_update.h | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 85609bd31f..1900f69a69 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -24,8 +24,29 @@ namespace http_request { static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000; +static constexpr uint8_t INITIAL_CHECK_MAX_ATTEMPTS = 6; -void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); } +void HttpRequestUpdate::setup() { + this->ota_parent_->add_state_listener(this); + + // Check periodically until network is ready + // Only if update interval is > total retry window to avoid redundant checks + if (this->get_update_interval() != SCHEDULER_DONT_RUN && + this->get_update_interval() > INITIAL_CHECK_INTERVAL_MS * INITIAL_CHECK_MAX_ATTEMPTS) { + this->initial_check_remaining_ = INITIAL_CHECK_MAX_ATTEMPTS; + this->set_interval(INITIAL_CHECK_INTERVAL_ID, INITIAL_CHECK_INTERVAL_MS, [this]() { + bool connected = network::is_connected(); + if (--this->initial_check_remaining_ == 0 || connected) { + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); + if (connected) { + this->update(); + } + } + }); + } +} void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { if (state == ota::OTAState::OTA_IN_PROGRESS) { @@ -45,6 +66,7 @@ void HttpRequestUpdate::update() { ESP_LOGD(TAG, "Network not connected, skipping update check"); return; } + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index cf34ace18e..b8350346f9 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -40,6 +40,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo #ifdef USE_ESP32 TaskHandle_t update_task_handle_{nullptr}; #endif + uint8_t initial_check_remaining_{0}; }; } // namespace http_request From 063c6a9e45576eb5b22c94977cddac1f1eab1aef Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 23 Feb 2026 21:06:20 +0100 Subject: [PATCH 84/89] [esp32,core] Move CONF_ENABLE_OTA_ROLLBACK to core (#14231) --- esphome/components/esp32/__init__.py | 2 +- esphome/const.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4c211b2f2a..62367443da 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_BOARD, CONF_COMPONENTS, CONF_DISABLED, + CONF_ENABLE_OTA_ROLLBACK, CONF_ESPHOME, CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, @@ -90,7 +91,6 @@ CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" -CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" diff --git a/esphome/const.py b/esphome/const.py index ccc9d56dbb..0b1037d091 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -354,6 +354,7 @@ CONF_ELSE = "else" CONF_ENABLE_BTM = "enable_btm" CONF_ENABLE_IPV6 = "enable_ipv6" CONF_ENABLE_ON_BOOT = "enable_on_boot" +CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_ENABLE_PIN = "enable_pin" CONF_ENABLE_PRIVATE_NETWORK_ACCESS = "enable_private_network_access" CONF_ENABLE_RRM = "enable_rrm" From 918bbfb0d3c73be7616fc7e7ab57238f27bc35ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:22:32 +0000 Subject: [PATCH 85/89] Bump aioesphomeapi from 44.0.0 to 44.1.0 (#14232) 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 be3445dceb..d22097b3ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 -aioesphomeapi==44.0.0 +aioesphomeapi==44.1.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 02c37bb6d63d9ccb457a13f35b6f2e3c80c35cc9 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 23 Feb 2026 21:23:40 +0100 Subject: [PATCH 86/89] [nrf52,logger] generate crash magic in python (#14173) --- esphome/components/logger/logger_zephyr.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index c2d24d6efc..6b46b93c61 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -20,8 +20,6 @@ __attribute__((weak)) void print_coredump() {} namespace esphome::logger { -static const uint32_t CRASH_MAGIC = 0xDEADBEEF; - __attribute__((section(".noinit"))) struct { uint32_t magic; uint32_t reason; @@ -152,7 +150,7 @@ static const char *reason_to_str(unsigned int reason, char *buf) { void Logger::dump_crash_() { ESP_LOGD(TAG, "Crash buffer address %p", &crash_buf); - if (crash_buf.magic == CRASH_MAGIC) { + if (crash_buf.magic == App.get_config_hash()) { char reason_buf[REASON_BUF_SIZE]; ESP_LOGE(TAG, "Last crash:"); ESP_LOGE(TAG, "Reason=%s PC=0x%08x LR=0x%08x", reason_to_str(crash_buf.reason, reason_buf), crash_buf.pc, @@ -164,7 +162,7 @@ void Logger::dump_crash_() { } void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { - crash_buf.magic = CRASH_MAGIC; + crash_buf.magic = App.get_config_hash(); crash_buf.reason = reason; if (esf) { crash_buf.pc = esf->basic.pc; From 4a529003527b3d475742b29ae2fed82e55b0066c Mon Sep 17 00:00:00 2001 From: James Myatt Date: Mon, 23 Feb 2026 21:16:32 +0000 Subject: [PATCH 87/89] [nfc] Fix logging tag for nfc helpers (#14235) --- esphome/components/nfc/nfc_helpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp index bfaed6e486..fb0954a833 100644 --- a/esphome/components/nfc/nfc_helpers.cpp +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -39,7 +39,7 @@ std::string get_random_ha_tag_ndef() { for (int i = 0; i < 12; i++) { uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; } - ESP_LOGD("pn7160", "Payload to be written: %s", uri.c_str()); + ESP_LOGD(TAG, "Payload to be written: %s", uri.c_str()); return uri; } From 869678953dfbe22f2b784e8ec927bfa394676671 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 17:03:24 -0600 Subject: [PATCH 88/89] [core] Add pow10_int helper, replace powf in normalize_accuracy and sensor filters (#14114) Co-authored-by: Claude Opus 4.6 --- esphome/components/sensor/filter.cpp | 5 +++-- esphome/core/helpers.cpp | 11 +++++++++-- esphome/core/helpers.h | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index cd4db98457..0fe1effe17 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -5,6 +5,7 @@ #include #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "sensor.h" @@ -240,7 +241,7 @@ ValueListFilter::ValueListFilter(std::initializer_list> bool ValueListFilter::value_matches_any_(float sensor_value) { int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = powf(10.0f, accuracy); + float accuracy_mult = pow10_int(accuracy); float rounded_sensor = roundf(accuracy_mult * sensor_value); for (auto &filter_value : this->values_) { @@ -472,7 +473,7 @@ optional ClampFilter::new_value(float value) { RoundFilter::RoundFilter(uint8_t precision) : precision_(precision) {} optional RoundFilter::new_value(float value) { if (std::isfinite(value)) { - float accuracy_mult = powf(10.0f, this->precision_); + float accuracy_mult = pow10_int(this->precision_); return roundf(accuracy_mult * value) / accuracy_mult; } return value; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 9f850b5df8..6d801e7ebc 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -468,8 +468,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { - auto multiplier = powf(10.0f, accuracy_decimals); - value = roundf(value * multiplier) / multiplier; + float divisor; + if (accuracy_decimals == -1) { + divisor = 10.0f; + } else if (accuracy_decimals == -2) { + divisor = 100.0f; + } else { + divisor = pow10_int(-accuracy_decimals); + } + value = roundf(value / divisor) * divisor; accuracy_decimals = 0; } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 298b93fbc4..b606e68df3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -439,6 +439,21 @@ template class SmallBufferWithHeapFallb /// @name Mathematics ///@{ +/// Compute 10^exp using iterative multiplication/division. +/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. +/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. +inline float pow10_int(int8_t exp) { + float result = 1.0f; + if (exp >= 0) { + for (int8_t i = 0; i < exp; i++) + result *= 10.0f; + } else { + for (int8_t i = exp; i < 0; i++) + result /= 10.0f; + } + return result; +} + /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out). template T remap(U value, U min, U max, T min_out, T max_out) { return (value - min) * (max_out - min_out) / (max - min) + min_out; From ebf1047da79805e86ec7a7e1058a2c50c55e42b0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:51:56 -0500 Subject: [PATCH 89/89] [core] Move build_info_data.h out of application.h to fix incremental rebuilds (#14230) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/version/version_text_sensor.cpp | 1 + esphome/components/web_server/web_server.cpp | 2 +- esphome/core/application.cpp | 11 +++++++++++ esphome/core/application.h | 18 ++++++++---------- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 2e5686008b..74bb4c76e8 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,5 +1,6 @@ #include "version_text_sensor.h" #include "esphome/core/application.h" +#include "esphome/core/build_info_data.h" #include "esphome/core/log.h" #include "esphome/core/version.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4b572417c1..682008c40e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -375,7 +375,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { JsonObject root = builder.root(); root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str(); - char comment_buffer[ESPHOME_COMMENT_SIZE]; + char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX]; App.get_comment_string(comment_buffer); root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c6597897dc..1cb7dc0075 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -749,4 +749,15 @@ void Application::get_build_time_string(std::span buf buffer[buffer.size() - 1] = '\0'; } +void Application::get_comment_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, ESPHOME_COMMENT_SIZE); + buffer[ESPHOME_COMMENT_SIZE - 1] = '\0'; +} + +uint32_t Application::get_config_hash() { return ESPHOME_CONFIG_HASH; } + +uint32_t Application::get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); } + +time_t Application::get_build_time() { return ESPHOME_BUILD_TIME; } + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 5b3e3dfed6..cd275bb97f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -6,7 +6,6 @@ #include #include #include -#include "esphome/core/build_info_data.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -274,16 +273,15 @@ class Application { return ""; } + /// Maximum size of the comment buffer (including null terminator) + static constexpr size_t ESPHOME_COMMENT_SIZE_MAX = 256; + /// Copy the comment string into the provided buffer - /// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced) - void get_comment_string(std::span buffer) { - ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size()); - buffer[buffer.size() - 1] = '\0'; - } + void get_comment_string(std::span buffer); /// Get the comment of this Application as a string std::string get_comment() { - char buffer[ESPHOME_COMMENT_SIZE]; + char buffer[ESPHOME_COMMENT_SIZE_MAX]; this->get_comment_string(buffer); return std::string(buffer); } @@ -294,13 +292,13 @@ class Application { static constexpr size_t BUILD_TIME_STR_SIZE = 26; /// Get the config hash as a 32-bit integer - constexpr uint32_t get_config_hash() { return ESPHOME_CONFIG_HASH; } + uint32_t get_config_hash(); /// Get the config hash extended with ESPHome version - constexpr uint32_t get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); } + uint32_t get_config_version_hash(); /// Get the build time as a Unix timestamp - constexpr time_t get_build_time() { return ESPHOME_BUILD_TIME; } + time_t get_build_time(); /// Copy the build time string into the provided buffer /// Buffer must be BUILD_TIME_STR_SIZE bytes (compile-time enforced)