From eb759efb3d6503dd76f8e7e414250775b533b19b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:48:02 +1000 Subject: [PATCH 001/102] [font] Store glyph data in flash only (#11926) --- esphome/components/font/__init__.py | 29 ++++++---------- esphome/components/font/font.cpp | 46 ++++++++++++-------------- esphome/components/font/font.h | 44 ++++++++++++------------ esphome/components/lvgl/font.cpp | 4 +-- esphome/components/lvgl/lvgl_esphome.h | 4 +-- esphome/core/helpers.h | 17 ++++++++++ 6 files changed, 75 insertions(+), 69 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index ddcee14635..32e803f405 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -36,7 +36,6 @@ from esphome.const import ( CONF_WEIGHT, ) from esphome.core import CORE, HexInt -from esphome.helpers import cpp_string_escape from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -50,7 +49,6 @@ font_ns = cg.esphome_ns.namespace("font") Font = font_ns.class_("Font") Glyph = font_ns.class_("Glyph") -GlyphData = font_ns.struct("GlyphData") CONF_BPP = "bpp" CONF_EXTRAS = "extras" @@ -463,7 +461,7 @@ FONT_SCHEMA = cv.Schema( ) ), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), + cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph), }, ) @@ -583,22 +581,15 @@ async def to_code(config): # Create the glyph table that points to data in the above array. glyph_initializer = [ - cg.StructInitializer( - GlyphData, - ( - "a_char", - cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"), - ), - ( - "data", - cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"), - ), - ("advance", x.advance), - ("offset_x", x.offset_x), - ("offset_y", x.offset_y), - ("width", x.width), - ("height", x.height), - ) + [ + x.glyph, + prog_arr + (y - len(x.bitmap_data)), + x.advance, + x.offset_x, + x.offset_y, + x.width, + x.height, + ] for (x, y) in zip( glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args])) ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 8b2420ac07..add403fe98 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -9,20 +9,19 @@ namespace font { static const char *const TAG = "font"; -const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; } // Compare the char at the string position with this char. // Return true if this char is less than or equal the other. bool Glyph::compare_to(const uint8_t *str) const { // 1 -> this->char_ // 2 -> str for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') + if (this->a_char[i] == '\0') return true; if (str[i] == '\0') return false; - if (this->glyph_data_->a_char[i] > str[i]) + if (this->a_char[i] > str[i]) return false; - if (this->glyph_data_->a_char[i] < str[i]) + if (this->a_char[i] < str[i]) return true; } // this should not happen @@ -30,35 +29,32 @@ bool Glyph::compare_to(const uint8_t *str) const { } int Glyph::match_length(const uint8_t *str) const { for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') + if (this->a_char[i] == '\0') return i; - if (str[i] != this->glyph_data_->a_char[i]) + if (str[i] != this->a_char[i]) return 0; } // this should not happen return 0; } void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->glyph_data_->offset_x; - *y1 = this->glyph_data_->offset_y; - *width = this->glyph_data_->width; - *height = this->glyph_data_->height; + *x1 = this->offset_x; + *y1 = this->offset_y; + *width = this->width; + *height = this->height; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, +Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp) - : baseline_(baseline), + : glyphs_(ConstVector(data, data_nr)), + baseline_(baseline), height_(height), descender_(descender), linegap_(height - baseline - descender), xheight_(xheight), capheight_(capheight), - bpp_(bpp) { - glyphs_.reserve(data_nr); - for (int i = 0; i < data_nr; ++i) - glyphs_.emplace_back(&data[i]); -} -int Font::match_next_glyph(const uint8_t *str, int *match_length) { + bpp_(bpp) {} +int Font::match_next_glyph(const uint8_t *str, int *match_length) const { int lo = 0; int hi = this->glyphs_.size() - 1; while (lo != hi) { @@ -88,18 +84,18 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in if (glyph_n < 0) { // Unknown char, skip if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].glyph_data_->advance; + x += this->get_glyphs()[0].advance; i++; continue; } const Glyph &glyph = this->glyphs_[glyph_n]; if (!has_char) { - min_x = glyph.glyph_data_->offset_x; + min_x = glyph.offset_x; } else { - min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); + min_x = std::min(min_x, x + glyph.offset_x); } - x += glyph.glyph_data_->advance; + x += glyph.advance; i += match_length; has_char = true; @@ -118,7 +114,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo // Unknown char, skip ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); if (!this->get_glyphs().empty()) { - uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance; + uint8_t glyph_width = this->get_glyphs()[0].advance; display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); x_at += glyph_width; } @@ -130,7 +126,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo const Glyph &glyph = this->get_glyphs()[glyph_n]; glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - const uint8_t *data = glyph.glyph_data_->data; + const uint8_t *data = glyph.data; const int max_x = x_at + scan_x1 + scan_width; const int max_y = y_start + scan_y1 + scan_height; @@ -168,7 +164,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo } } } - x_at += glyph.glyph_data_->advance; + x_at += glyph.advance; i += match_length; } diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 28832d647d..cb6cc89137 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -12,21 +12,19 @@ namespace font { class Font; -struct GlyphData { - const uint8_t *a_char; - const uint8_t *data; - int advance; - int offset_x; - int offset_y; - int width; - int height; -}; - class Glyph { public: - Glyph(const GlyphData *data) : glyph_data_(data) {} + constexpr Glyph(const char *a_char, const uint8_t *data, int advance, int offset_x, int offset_y, int width, + int height) + : a_char(a_char), + data(data), + advance(advance), + offset_x(offset_x), + offset_y(offset_y), + width(width), + height(height) {} - const uint8_t *get_char() const; + const uint8_t *get_char() const { return reinterpret_cast(this->a_char); } bool compare_to(const uint8_t *str) const; @@ -34,12 +32,16 @@ class Glyph { void scan_area(int *x1, int *y1, int *width, int *height) const; - const GlyphData *get_glyph_data() const { return this->glyph_data_; } + const char *a_char; + const uint8_t *data; + int advance; + int offset_x; + int offset_y; + int width; + int height; protected: friend Font; - - const GlyphData *glyph_data_; }; class Font @@ -50,8 +52,8 @@ class Font public: /** Construct the font with the given glyphs. * - * @param data A vector of glyphs, must be sorted lexicographically. - * @param data_nr The number of glyphs in data. + * @param data A list of glyphs, must be sorted lexicographically. + * @param data_nr The number of glyphs * @param baseline The y-offset from the top of the text to the baseline. * @param height The y-offset from the top of the text to the bottom. * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p). @@ -59,10 +61,10 @@ class Font * @param capheight The height of capital letters, usually measured at the "X" glyph. * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps. */ - Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp = 1); - int match_next_glyph(const uint8_t *str, int *match_length); + int match_next_glyph(const uint8_t *str, int *match_length) const; #ifdef USE_DISPLAY void print(int x_start, int y_start, display::Display *display, Color color, const char *text, @@ -78,10 +80,10 @@ class Font inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } - const std::vector> &get_glyphs() const { return glyphs_; } + const ConstVector &get_glyphs() const { return glyphs_; } protected: - std::vector> glyphs_; + ConstVector glyphs_; int baseline_; int height_; int descender_; diff --git a/esphome/components/lvgl/font.cpp b/esphome/components/lvgl/font.cpp index a0d5127570..1976fb9608 100644 --- a/esphome/components/lvgl/font.cpp +++ b/esphome/components/lvgl/font.cpp @@ -43,7 +43,7 @@ FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } -const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { +const font::Glyph *FontEngine::get_glyph_data(uint32_t unicode_letter) { if (unicode_letter == last_letter_) return this->last_data_; uint8_t unicode[5]; @@ -67,7 +67,7 @@ const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { int glyph_n = this->font_->match_next_glyph(unicode, &match_length); if (glyph_n < 0) return nullptr; - this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data(); + this->last_data_ = &this->font_->get_glyphs()[glyph_n]; this->last_letter_ = unicode_letter; return this->last_data_; } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 1ae05f933f..196a0d1cb4 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -140,7 +140,7 @@ class FontEngine { FontEngine(font::Font *esp_font); const lv_font_t *get_lv_font(); - const font::GlyphData *get_glyph_data(uint32_t unicode_letter); + const font::Glyph *get_glyph_data(uint32_t unicode_letter); uint16_t baseline{}; uint16_t height{}; uint8_t bpp{}; @@ -148,7 +148,7 @@ class FontEngine { protected: font::Font *font_{}; uint32_t last_letter_{}; - const font::GlyphData *last_data_{}; + const font::Glyph *last_data_{}; lv_font_t lv_font_{}; }; #endif // USE_LVGL_FONT diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 52a0746057..16eab8b8f6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -111,6 +111,23 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); /// @name Container utilities ///@{ +/// Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory) +/// Avoids copying data from flash to RAM by keeping a pointer to the flash data. +/// Similar to std::span but with minimal overhead for embedded systems. + +template class ConstVector { + public: + constexpr ConstVector(const T *data, size_t size) : data_(data), size_(size) {} + + const constexpr T &operator[](size_t i) const { return data_[i]; } + constexpr size_t size() const { return size_; } + constexpr bool empty() const { return size_ == 0; } + + protected: + const T *data_; + size_t size_; +}; + /// Minimal static vector - saves memory by avoiding std::vector overhead template class StaticVector { public: From 5710cab972485a60af1488a62b71dca775002fb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:03:43 -0600 Subject: [PATCH 002/102] [ld2412] Fix stuck targets by adding timeout filter (#11919) --- esphome/components/ld2412/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index abb823faad..0bfbd9bf1d 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -31,36 +31,84 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor ), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), @@ -74,7 +122,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -82,7 +136,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From 6b158e760d379033b811cd7bf73be0f95b33a09c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:04:25 -0600 Subject: [PATCH 003/102] [ld2410] Add timeout filter to prevent stuck targets (#11920) --- esphome/components/ld2410/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index fca2b2ceca..3bd34963bc 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -31,35 +31,83 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), @@ -73,7 +121,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -81,7 +135,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From fc546ca3f6b63aaa43e208fe472c12eaa9c194b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:20:57 -0600 Subject: [PATCH 004/102] [scheduler] Fix timing breakage after 49 days of uptime on ESP8266/RP2040 (#11924) --- esphome/core/scheduler.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d285af2d0e..d2e0f0dab4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -609,13 +609,12 @@ uint64_t Scheduler::millis_64_(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { this->millis_major_++; major++; + this->last_millis_ = now; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ - } - - // Only update if time moved forward - if (now > last) { + } else if (now > last) { + // Only update if time moved forward this->last_millis_ = now; } From ea2b4c3e2500a21d01d28a255a755c34391e237a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:21:06 -0600 Subject: [PATCH 005/102] [binary_sensor] Modernize to C++17 nested namespaces and remove redundant qualifications (#11929) --- .../components/binary_sensor/automation.cpp | 18 ++++++++---------- esphome/components/binary_sensor/automation.h | 6 ++---- .../components/binary_sensor/binary_sensor.cpp | 8 ++------ .../components/binary_sensor/binary_sensor.h | 7 ++----- esphome/components/binary_sensor/filter.cpp | 8 ++------ esphome/components/binary_sensor/filter.h | 8 ++------ 6 files changed, 18 insertions(+), 37 deletions(-) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 64a0d3db8d..66d8d6e90f 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -1,12 +1,11 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "binary_sensor.automation"; -void binary_sensor::MultiClickTrigger::on_state_(bool state) { +void MultiClickTrigger::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { *this->at_index_ = *this->at_index_ + 1; } -void binary_sensor::MultiClickTrigger::schedule_cooldown_() { +void MultiClickTrigger::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { @@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout("is_valid"); this->cancel_timeout("is_not_valid"); } -void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { +void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { this->is_valid_ = true; return; @@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { this->is_valid_ = true; }); } -void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { +void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { this->set_timeout("is_not_valid", max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); }); } -void binary_sensor::MultiClickTrigger::cancel() { +void MultiClickTrigger::cancel() { ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); this->is_valid_ = false; this->schedule_cooldown_(); } -void binary_sensor::MultiClickTrigger::trigger_() { +void MultiClickTrigger::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); this->cancel_timeout("trigger"); @@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { return length >= min_length && length <= max_length; } } -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f6971a2fc4..f8b130e08a 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -9,8 +9,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { struct MultiClickTriggerEvent { bool state; @@ -172,5 +171,4 @@ template class BinarySensorInvalidateAction : public Action filters) { } bool BinarySensor::is_status_binary_sensor() const { return false; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index c1661d710f..0dca3e1520 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -6,9 +6,7 @@ #include -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { class BinarySensor; void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); @@ -70,5 +68,4 @@ class BinarySensorInitiallyOff : public BinarySensor { bool has_state() const override { return true; } }; -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 8f31cf6fc2..9c7238f6d7 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -2,9 +2,7 @@ #include "binary_sensor.h" -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; @@ -132,6 +130,4 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 2d473c3b64..59bc43eeba 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -4,9 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { class BinarySensor; @@ -139,6 +137,4 @@ class SettleFilter : public Filter, public Component { bool steady_{true}; }; -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor From 6f4042f401a27e8f6c46fa4b52f0e4f3c406be84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:21:38 -0600 Subject: [PATCH 006/102] Add tests for sensor timeout filters (#11923) --- .../fixtures/sensor_timeout_filter.yaml | 150 ++++++++++++++ .../integration/test_sensor_timeout_filter.py | 185 ++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 tests/integration/fixtures/sensor_timeout_filter.yaml create mode 100644 tests/integration/test_sensor_timeout_filter.py diff --git a/tests/integration/fixtures/sensor_timeout_filter.yaml b/tests/integration/fixtures/sensor_timeout_filter.yaml new file mode 100644 index 0000000000..dbd4db3242 --- /dev/null +++ b/tests/integration/fixtures/sensor_timeout_filter.yaml @@ -0,0 +1,150 @@ +esphome: + name: test-timeout-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensors that we'll use to publish values +sensor: + - platform: template + name: "Source Timeout Last" + id: source_timeout_last + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Reset" + id: source_timeout_reset + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Static" + id: source_timeout_static + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Lambda" + id: source_timeout_lambda + accuracy_decimals: 1 + + # Test 1: TimeoutFilter - "last" mode (outputs last received value) + - platform: copy + source_id: source_timeout_last + name: "Timeout Last Sensor" + id: timeout_last_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode to use TimeoutFilter class + + # Test 2: TimeoutFilter - reset behavior (same filter, different source) + - platform: copy + source_id: source_timeout_reset + name: "Timeout Reset Sensor" + id: timeout_reset_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode + + # Test 3: TimeoutFilterConfigured - static value mode + - platform: copy + source_id: source_timeout_static + name: "Timeout Static Sensor" + id: timeout_static_sensor + filters: + - timeout: + timeout: 100ms + value: 99.9 + + # Test 4: TimeoutFilterConfigured - lambda mode + - platform: copy + source_id: source_timeout_lambda + name: "Timeout Lambda Sensor" + id: timeout_lambda_sensor + filters: + - timeout: + timeout: 100ms + value: !lambda "return -1.0;" + +# Scripts to publish values with controlled timing +script: + # Test 1: Single value followed by timeout + - id: test_timeout_last_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_last + state: 42.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 2: Multiple values before timeout (should reset timer) + - id: test_timeout_reset_script + then: + # Publish first value + - sensor.template.publish: + id: source_timeout_reset + state: 10.0 + # Wait 50ms (halfway to timeout) + - delay: 50ms + # Publish second value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 20.0 + # Wait 50ms (halfway to timeout again) + - delay: 50ms + # Publish third value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 30.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 3: Static value timeout + - id: test_timeout_static_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_static + state: 55.5 + # Wait for timeout to fire + - delay: 150ms + + # Test 4: Lambda value timeout + - id: test_timeout_lambda_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_lambda + state: 77.7 + # Wait for timeout to fire + - delay: 150ms + +# Buttons to trigger each test scenario +button: + - platform: template + name: "Test Timeout Last Button" + id: test_timeout_last_button + on_press: + - script.execute: test_timeout_last_script + + - platform: template + name: "Test Timeout Reset Button" + id: test_timeout_reset_button + on_press: + - script.execute: test_timeout_reset_script + + - platform: template + name: "Test Timeout Static Button" + id: test_timeout_static_button + on_press: + - script.execute: test_timeout_static_script + + - platform: template + name: "Test Timeout Lambda Button" + id: test_timeout_lambda_button + on_press: + - script.execute: test_timeout_lambda_script diff --git a/tests/integration/test_sensor_timeout_filter.py b/tests/integration/test_sensor_timeout_filter.py new file mode 100644 index 0000000000..9b4704bb7b --- /dev/null +++ b/tests/integration/test_sensor_timeout_filter.py @@ -0,0 +1,185 @@ +"""Test sensor timeout filter functionality.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_timeout_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test TimeoutFilter and TimeoutFilterConfigured with all modes.""" + loop = asyncio.get_running_loop() + + # Track state changes for all sensors + timeout_last_states: list[float] = [] + timeout_reset_states: list[float] = [] + timeout_static_states: list[float] = [] + timeout_lambda_states: list[float] = [] + + # Futures for each test scenario + test1_complete = loop.create_future() # TimeoutFilter - last mode + test2_complete = loop.create_future() # TimeoutFilter - reset behavior + test3_complete = loop.create_future() # TimeoutFilterConfigured - static value + test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + if state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + + # Test 1: TimeoutFilter - last mode + if sensor_name == "timeout_last_sensor": + timeout_last_states.append(state.state) + # Expect 2 values: initial 42.0 + timeout fires with 42.0 + if len(timeout_last_states) >= 2 and not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: TimeoutFilter - reset behavior + elif sensor_name == "timeout_reset_sensor": + timeout_reset_states.append(state.state) + # Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0 + if len(timeout_reset_states) >= 4 and not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: TimeoutFilterConfigured - static value + elif sensor_name == "timeout_static_sensor": + timeout_static_states.append(state.state) + # Expect 2 values: initial 55.5 + timeout fires with 99.9 + if len(timeout_static_states) >= 2 and not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: TimeoutFilterConfigured - lambda + elif sensor_name == "timeout_lambda_sensor": + timeout_lambda_states.append(state.state) + # Expect 2 values: initial 77.7 + timeout fires with -1.0 + if len(timeout_lambda_states) >= 2 and not test4_complete.done(): + test4_complete.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + + key_to_sensor = build_key_to_entity_mapping( + entities, + [ + "timeout_last_sensor", + "timeout_reset_sensor", + "timeout_static_sensor", + "timeout_lambda_sensor", + ], + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Helper to find buttons by object_id substring + def find_button(object_id_substring: str) -> int: + """Find a button by object_id substring and return its key.""" + button = next( + (e for e in entities if object_id_substring in e.object_id.lower()), + None, + ) + assert button is not None, f"Button '{object_id_substring}' not found" + return button.key + + # Find all test buttons + test1_button_key = find_button("test_timeout_last_button") + test2_button_key = find_button("test_timeout_reset_button") + test3_button_key = find_button("test_timeout_static_button") + test4_button_key = find_button("test_timeout_lambda_button") + + # === Test 1: TimeoutFilter - last mode === + client.button_command(test1_button_key) + try: + await asyncio.wait_for(test1_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}") + + assert len(timeout_last_states) == 2, ( + f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}" + ) + assert timeout_last_states[0] == pytest.approx(42.0), ( + f"Test 1: First state should be 42.0, got {timeout_last_states[0]}" + ) + assert timeout_last_states[1] == pytest.approx(42.0), ( + f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}" + ) + + # === Test 2: TimeoutFilter - reset behavior === + client.button_command(test2_button_key) + try: + await asyncio.wait_for(test2_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}") + + assert len(timeout_reset_states) == 4, ( + f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}" + ) + assert timeout_reset_states[0] == pytest.approx(10.0), ( + f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}" + ) + assert timeout_reset_states[1] == pytest.approx(20.0), ( + f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}" + ) + assert timeout_reset_states[2] == pytest.approx(30.0), ( + f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}" + ) + assert timeout_reset_states[3] == pytest.approx(30.0), ( + f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}" + ) + + # === Test 3: TimeoutFilterConfigured - static value === + client.button_command(test3_button_key) + try: + await asyncio.wait_for(test3_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}") + + assert len(timeout_static_states) == 2, ( + f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}" + ) + assert timeout_static_states[0] == pytest.approx(55.5), ( + f"Test 3: First state should be 55.5, got {timeout_static_states[0]}" + ) + assert timeout_static_states[1] == pytest.approx(99.9), ( + f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}" + ) + + # === Test 4: TimeoutFilterConfigured - lambda === + client.button_command(test4_button_key) + try: + await asyncio.wait_for(test4_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}") + + assert len(timeout_lambda_states) == 2, ( + f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}" + ) + assert timeout_lambda_states[0] == pytest.approx(77.7), ( + f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}" + ) + assert timeout_lambda_states[1] == pytest.approx(-1.0), ( + f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}" + ) From 4fc4da6ed2d69d9cdb47d777abc24e0e5e0c29c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 07:35:31 -0600 Subject: [PATCH 007/102] [analyze-memory] Show all core symbols > 100 B instead of top 15 (#11909) --- esphome/analyze_memory/cli.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 718f42330d..44ade221f8 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,11 @@ from . import ( class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" + # Symbol size threshold for detailed analysis + SYMBOL_SIZE_THRESHOLD: int = ( + 100 # Show symbols larger than this in detailed analysis + ) + # Column width constants COL_COMPONENT: int = 29 COL_FLASH_TEXT: int = 14 @@ -191,14 +196,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" ) - # Top 15 largest core symbols + # All core symbols above threshold lines.append("") - lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) + large_core_symbols = [ + (symbol, demangled, size) + for symbol, demangled, size in sorted_core_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): + lines.append( + f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_core_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * self.TABLE_WIDTH) @@ -268,13 +280,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols > 100 bytes for better visibility + # Show all symbols above threshold for better visibility large_symbols = [ - (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100 + (sym, dem, size) + for sym, dem, size in sorted_symbols + if size > self.SYMBOL_SIZE_THRESHOLD ] lines.append( - f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size) in enumerate(large_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") From 320120883cab070fa21ba187e13f0f3c6447c3b8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:47:54 +1000 Subject: [PATCH 008/102] [lvgl] Migrate lv_font creation into Font class and optimise (#11915) --- esphome/components/font/__init__.py | 2 + esphome/components/font/font.cpp | 263 ++++++++++++++++------- esphome/components/font/font.h | 33 +-- esphome/components/lvgl/__init__.py | 14 +- esphome/components/lvgl/font.cpp | 76 ------- esphome/components/lvgl/lv_validation.py | 5 +- esphome/components/lvgl/lvgl_esphome.h | 26 +-- esphome/components/lvgl/types.py | 1 - tests/components/lvgl/lvgl-package.yaml | 4 +- tests/components/lvgl/test.host.yaml | 6 + 10 files changed, 232 insertions(+), 198 deletions(-) delete mode 100644 esphome/components/lvgl/font.cpp diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 32e803f405..2667dbdbdf 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -486,6 +486,8 @@ class GlyphInfo: def glyph_to_glyphinfo(glyph, font, size, bpp): + # Convert to 32 bit unicode codepoint + glyph = ord(glyph) scale = 256 // (1 << bpp) if not font.is_scalable: sizes = [pt_to_px(x.size) for x in font.available_sizes] diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index add403fe98..5e3bf1dd20 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -6,42 +6,147 @@ namespace esphome { namespace font { - static const char *const TAG = "font"; -// Compare the char at the string position with this char. -// Return true if this char is less than or equal the other. -bool Glyph::compare_to(const uint8_t *str) const { - // 1 -> this->char_ - // 2 -> str - for (uint32_t i = 0;; i++) { - if (this->a_char[i] == '\0') - return true; - if (str[i] == '\0') - return false; - if (this->a_char[i] > str[i]) - return false; - if (this->a_char[i] < str[i]) - return true; +#ifdef USE_LVGL_FONT +const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { + auto *fe = (Font *) font->dsc; + const auto *gd = fe->get_glyph_data_(unicode_letter); + if (gd == nullptr) { + return nullptr; } - // this should not happen - return false; + return gd->data; } -int Glyph::match_length(const uint8_t *str) const { - for (uint32_t i = 0;; i++) { - if (this->a_char[i] == '\0') - return i; - if (str[i] != this->a_char[i]) + +bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { + auto *fe = (Font *) font->dsc; + const auto *gd = fe->get_glyph_data_(unicode_letter); + if (gd == nullptr) { + return false; + } + dsc->adv_w = gd->advance; + dsc->ofs_x = gd->offset_x; + dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line; + dsc->box_w = gd->width; + dsc->box_h = gd->height; + dsc->is_placeholder = 0; + dsc->bpp = fe->get_bpp(); + return true; +} + +const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) { + if (unicode_letter == this->last_letter_ && this->last_letter_ != 0) + return this->last_data_; + auto *glyph = this->find_glyph(unicode_letter); + if (glyph == nullptr) { + return nullptr; + } + this->last_data_ = glyph; + this->last_letter_ = unicode_letter; + return glyph; +} +#endif + +/** + * Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string. + * If successful, return the codepoint and set the length to the number of bytes read. + * If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to + * 0. + * + * @param utf8_str The input string + * @param length Pointer to length storage + * @return The extracted code point + */ +static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) { + // Safely cast to uint8_t* for correct bitwise operations on bytes + const uint8_t *current = reinterpret_cast(utf8_str); + uint32_t code_point = 0; + uint8_t c1 = *current++; + + // check for end of string + if (c1 == 0) { + *length = 0; + return 0; + } + + // --- 1-Byte Sequence: 0xxxxxxx (ASCII) --- + if (c1 < 0x80) { + // Valid ASCII byte. + code_point = c1; + // Optimization: No need to check for continuation bytes. + } + // --- 2-Byte Sequence: 110xxxxx 10xxxxxx --- + else if ((c1 & 0xE0) == 0xC0) { + uint8_t c2 = *current++; + + // Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx) + if ((c2 & 0xC0) != 0x80) { + *length = 0; return 0; + } + + code_point = (c1 & 0x1F) << 6; + code_point |= (c2 & 0x3F); + + // Error Check 2: Overlong check (2-byte must be > 0x7F) + if (code_point <= 0x7F) { + *length = 0; + return 0; + } } - // this should not happen - return 0; -} -void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->offset_x; - *y1 = this->offset_y; - *width = this->width; - *height = this->height; + // --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx --- + else if ((c1 & 0xF0) == 0xE0) { + uint8_t c2 = *current++; + uint8_t c3 = *current++; + + // Error Check 1: Check continuation bytes + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) { + *length = 0; + return 0; + } + + code_point = (c1 & 0x0F) << 12; + code_point |= (c2 & 0x3F) << 6; + code_point |= (c3 & 0x3F); + + // Error Check 2: Overlong check (3-byte must be > 0x7FF) + // Also check for surrogates (0xD800-0xDFFF) + if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) { + *length = 0; + return 0; + } + } + // --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx --- + else if ((c1 & 0xF8) == 0xF0) { + uint8_t c2 = *current++; + uint8_t c3 = *current++; + uint8_t c4 = *current++; + + // Error Check 1: Check continuation bytes + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) { + *length = 0; + return 0; + } + + code_point = (c1 & 0x07) << 18; + code_point |= (c2 & 0x3F) << 12; + code_point |= (c3 & 0x3F) << 6; + code_point |= (c4 & 0x3F); + + // Error Check 2: Overlong check (4-byte must be > 0xFFFF) + // Also check for valid Unicode range (must be <= 0x10FFFF) + if (code_point <= 0xFFFF || code_point > 0x10FFFF) { + *length = 0; + return 0; + } + } + // --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) --- + else { + *length = 0; + return 0; + } + *length = current - reinterpret_cast(utf8_str); + return code_point; } Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, @@ -53,82 +158,93 @@ Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descend linegap_(height - baseline - descender), xheight_(xheight), capheight_(capheight), - bpp_(bpp) {} -int Font::match_next_glyph(const uint8_t *str, int *match_length) const { + bpp_(bpp) { +#ifdef USE_LVGL_FONT + this->lv_font_.dsc = this; + this->lv_font_.line_height = this->get_height(); + this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline(); + this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; + this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; + this->lv_font_.subpx = LV_FONT_SUBPX_NONE; + this->lv_font_.underline_position = -1; + this->lv_font_.underline_thickness = 1; +#endif +} + +const Glyph *Font::find_glyph(uint32_t codepoint) const { int lo = 0; int hi = this->glyphs_.size() - 1; while (lo != hi) { int mid = (lo + hi + 1) / 2; - if (this->glyphs_[mid].compare_to(str)) { + if (this->glyphs_[mid].is_less_or_equal(codepoint)) { lo = mid; } else { hi = mid - 1; } } - *match_length = this->glyphs_[lo].match_length(str); - if (*match_length <= 0) - return -1; - return lo; + auto *result = &this->glyphs_[lo]; + if (result->code_point == codepoint) + return result; + return nullptr; } + #ifdef USE_DISPLAY void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { *baseline = this->baseline_; *height = this->height_; - int i = 0; int min_x = 0; bool has_char = false; int x = 0; - while (str[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length); - if (glyph_n < 0) { + for (;;) { + size_t length; + auto code_point = extract_unicode_codepoint(str, &length); + if (length == 0) + break; + str += length; + auto *glyph = this->find_glyph(code_point); + if (glyph == nullptr) { // Unknown char, skip - if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].advance; - i++; + if (!this->glyphs_.empty()) + x += this->glyphs_[0].advance; continue; } - const Glyph &glyph = this->glyphs_[glyph_n]; if (!has_char) { - min_x = glyph.offset_x; + min_x = glyph->offset_x; } else { - min_x = std::min(min_x, x + glyph.offset_x); + min_x = std::min(min_x, x + glyph->offset_x); } - x += glyph.advance; + x += glyph->advance; - i += match_length; has_char = true; } *x_offset = min_x; *width = x - min_x; } + void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) { - int i = 0; int x_at = x_start; - int scan_x1, scan_y1, scan_width, scan_height; - while (text[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length); - if (glyph_n < 0) { + for (;;) { + size_t length; + auto code_point = extract_unicode_codepoint(text, &length); + if (length == 0) + break; + text += length; + auto *glyph = this->find_glyph(code_point); + if (glyph == nullptr) { // Unknown char, skip - ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (!this->get_glyphs().empty()) { - uint8_t glyph_width = this->get_glyphs()[0].advance; - display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); + ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point); + if (!this->glyphs_.empty()) { + uint8_t glyph_width = this->glyphs_[0].advance; + display->rectangle(x_at, y_start, glyph_width, this->height_, color); x_at += glyph_width; } - - i++; continue; } - const Glyph &glyph = this->get_glyphs()[glyph_n]; - glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - - const uint8_t *data = glyph.data; - const int max_x = x_at + scan_x1 + scan_width; - const int max_y = y_start + scan_y1 + scan_height; + const uint8_t *data = glyph->data; + const int max_x = x_at + glyph->offset_x + glyph->width; + const int max_y = y_start + glyph->offset_y + glyph->height; uint8_t bitmask = 0; uint8_t pixel_data = 0; @@ -141,10 +257,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo auto b_g = (float) background.g; auto b_b = (float) background.b; auto b_w = (float) background.w; - for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { - for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { + for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) { + for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) { uint8_t pixel = 0; - for (int bit_num = 0; bit_num != this->bpp_; bit_num++) { + for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) { if (bitmask == 0) { pixel_data = progmem_read_byte(data++); bitmask = 0x80; @@ -164,12 +280,9 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo } } } - x_at += glyph.advance; - - i += match_length; + x_at += glyph->advance; } } #endif - } // namespace font } // namespace esphome diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index cb6cc89137..262ded3be4 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -6,6 +6,9 @@ #ifdef USE_DISPLAY #include "esphome/components/display/display.h" #endif +#ifdef USE_LVGL_FONT +#include +#endif namespace esphome { namespace font { @@ -14,9 +17,9 @@ class Font; class Glyph { public: - constexpr Glyph(const char *a_char, const uint8_t *data, int advance, int offset_x, int offset_y, int width, + constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width, int height) - : a_char(a_char), + : code_point(code_point), data(data), advance(advance), offset_x(offset_x), @@ -24,24 +27,15 @@ class Glyph { width(width), height(height) {} - const uint8_t *get_char() const { return reinterpret_cast(this->a_char); } + bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; } - bool compare_to(const uint8_t *str) const; - - int match_length(const uint8_t *str) const; - - void scan_area(int *x1, int *y1, int *width, int *height) const; - - const char *a_char; + const uint32_t code_point; const uint8_t *data; int advance; int offset_x; int offset_y; int width; int height; - - protected: - friend Font; }; class Font @@ -64,7 +58,7 @@ class Font Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp = 1); - int match_next_glyph(const uint8_t *str, int *match_length) const; + const Glyph *find_glyph(uint32_t codepoint) const; #ifdef USE_DISPLAY void print(int x_start, int y_start, display::Display *display, Color color, const char *text, @@ -79,6 +73,9 @@ class Font inline int get_xheight() { return this->xheight_; } inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } +#ifdef USE_LVGL_FONT + const lv_font_t *get_lv_font() const { return &this->lv_font_; } +#endif const ConstVector &get_glyphs() const { return glyphs_; } @@ -91,6 +88,14 @@ class Font int xheight_; int capheight_; uint8_t bpp_; // bits per pixel +#ifdef USE_LVGL_FONT + lv_font_t lv_font_{}; + static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter); + static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next); + const Glyph *get_glyph_data_(uint32_t unicode_letter); + uint32_t last_letter_{}; + const Glyph *last_data_{}; +#endif }; } // namespace font diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 2a24f343c3..eaa37b54dd 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -52,15 +52,7 @@ from .schemas import ( from .styles import add_top_layer, styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import add_on_boot_triggers, generate_triggers -from .types import ( - FontEngine, - IdleTrigger, - PlainTrigger, - lv_font_t, - lv_group_t, - lv_style_t, - lvgl_ns, -) +from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns from .widgets import ( LvScrActType, Widget, @@ -244,7 +236,6 @@ async def to_code(configs): cg.add_global(lvgl_ns.using) for font in helpers.esphome_fonts_used: await cg.get_variable(font) - cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): add_define( @@ -256,7 +247,8 @@ async def to_code(configs): type=lv_font_t.operator("ptr").operator("const"), ) cg.new_variable( - globfont_id, MockObj(await lvalid.lv_font.process(default_font)) + globfont_id, + MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(), ) add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: diff --git a/esphome/components/lvgl/font.cpp b/esphome/components/lvgl/font.cpp deleted file mode 100644 index 1976fb9608..0000000000 --- a/esphome/components/lvgl/font.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "lvgl_esphome.h" - -#ifdef USE_LVGL_FONT -namespace esphome { -namespace lvgl { - -static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { - auto *fe = (FontEngine *) font->dsc; - const auto *gd = fe->get_glyph_data(unicode_letter); - if (gd == nullptr) - return nullptr; - // esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data); - - return gd->data; -} - -static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { - auto *fe = (FontEngine *) font->dsc; - const auto *gd = fe->get_glyph_data(unicode_letter); - if (gd == nullptr) - return false; - dsc->adv_w = gd->advance; - dsc->ofs_x = gd->offset_x; - dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; - dsc->box_w = gd->width; - dsc->box_h = gd->height; - dsc->is_placeholder = 0; - dsc->bpp = fe->bpp; - return true; -} - -FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { - this->bpp = esp_font->get_bpp(); - this->lv_font_.dsc = this; - this->lv_font_.line_height = this->height = esp_font->get_height(); - this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline(); - this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; - this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; - this->lv_font_.subpx = LV_FONT_SUBPX_NONE; - this->lv_font_.underline_position = -1; - this->lv_font_.underline_thickness = 1; -} - -const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } - -const font::Glyph *FontEngine::get_glyph_data(uint32_t unicode_letter) { - if (unicode_letter == last_letter_) - return this->last_data_; - uint8_t unicode[5]; - memset(unicode, 0, sizeof unicode); - if (unicode_letter > 0xFFFF) { - unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7); - unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F); - unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F); - unicode[3] = 0x80 + (unicode_letter & 0x3F); - } else if (unicode_letter > 0x7FF) { - unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF); - unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F); - unicode[2] = 0x80 + (unicode_letter & 0x3F); - } else if (unicode_letter > 0x7F) { - unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F); - unicode[1] = 0x80 + (unicode_letter & 0x3F); - } else { - unicode[0] = unicode_letter; - } - int match_length; - int glyph_n = this->font_->match_next_glyph(unicode, &match_length); - if (glyph_n < 0) - return nullptr; - this->last_data_ = &this->font_->get_glyphs()[glyph_n]; - this->last_letter_ = unicode_letter; - return this->last_data_; -} -} // namespace lvgl -} // namespace esphome -#endif // USES_LVGL_FONT diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 045258555c..23c322c31f 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -493,6 +493,7 @@ class LvFont(LValidator): return LV_FONTS if is_lv_font(value): return lv_builtin_font(value) + add_lv_use("font") fontval = cv.use_id(Font)(value) esphome_fonts_used.add(fontval) return requires_component("font")(fontval) @@ -502,7 +503,9 @@ class LvFont(LValidator): async def process(self, value, args=()): if is_lv_font(value): return literal(f"&lv_font_{value}") - return literal(f"{value}_engine->get_lv_font()") + if isinstance(value, str): + return literal(f"{value}") + return await super().process(value, args) lv_font = LvFont() diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 196a0d1cb4..bd6f1fdb61 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -50,6 +50,14 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; #endif // LV_COLOR_DEPTH +#ifdef USE_LVGL_FONT +inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) { + lv_obj_set_style_text_font(obj, font->get_lv_font(), part); +} +inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { + lv_style_set_text_font(style, font->get_lv_font()); +} +#endif #ifdef USE_LVGL_IMAGE // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. @@ -134,24 +142,6 @@ template class ObjUpdateAction : public Action { protected: std::function lamb_; }; -#ifdef USE_LVGL_FONT -class FontEngine { - public: - FontEngine(font::Font *esp_font); - const lv_font_t *get_lv_font(); - - const font::Glyph *get_glyph_data(uint32_t unicode_letter); - uint16_t baseline{}; - uint16_t height{}; - uint8_t bpp{}; - - protected: - font::Font *font_{}; - uint32_t last_letter_{}; - const font::Glyph *last_data_{}; - lv_font_t lv_font_{}; -}; -#endif // USE_LVGL_FONT #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj); #endif // USE_LVGL_ANIMIMG diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 035320b6ac..b99c0ad5a3 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -45,7 +45,6 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t") lv_event_code_t = cg.global_ns.enum("lv_event_code_t") lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") lv_key_t = cg.global_ns.enum("lv_key_t") -FontEngine = lvgl_ns.class_("FontEngine") PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template()) DrawEndTrigger = esphome_ns.class_( "Trigger", automation.Trigger.template(cg.uint32, cg.uint32) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d7c342b16e..e42a813b40 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -76,7 +76,7 @@ lvgl: line_width: 8 line_rounded: true - id: date_style - text_font: roboto10 + text_font: !lambda return id(roboto10); align: center text_color: !lambda return color_id2; bg_opa: cover @@ -267,7 +267,7 @@ lvgl: snprintf(buf, sizeof(buf), "Setup: %d", 42); return std::string(buf); align: top_mid - text_font: space16 + text_font: !lambda return id(space16); - label: id: chip_info_label # Test complex setup lambda (real-world pattern) diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 39d9a0ebf3..00a8cd8c01 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -18,6 +18,7 @@ touchscreen: lvgl: - id: lvgl_0 + default_font: space16 displays: sdl0 - id: lvgl_1 displays: sdl1 @@ -39,3 +40,8 @@ lvgl: text: Click ME on_click: logger.log: Clicked + +font: + - file: "gfonts://Roboto" + id: space16 + bpp: 4 From 986d3c8f137aad0a6097606fae49b98a22949258 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:38 -0600 Subject: [PATCH 009/102] [sntp] Merge multiple instances to fix crash and undefined behavior (#11904) --- esphome/components/sntp/time.py | 68 +++++ tests/component_tests/sntp/__init__.py | 1 + .../sntp/config/sntp_test.yaml | 22 ++ tests/component_tests/sntp/test_init.py | 238 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 tests/component_tests/sntp/__init__.py create mode 100644 tests/component_tests/sntp/config/sntp_test.yaml create mode 100644 tests/component_tests/sntp/test_init.py diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index d27fc9991d..69a2436d3d 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,9 +1,14 @@ +import logging + import esphome.codegen as cg from esphome.components import time as time_ +from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_PLATFORM, CONF_SERVERS, + CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -12,13 +17,74 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] + +CONF_SNTP = "sntp" + sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] + +def _sntp_final_validate(config: ConfigType) -> None: + """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" + full_conf = fv.full_config.get() + time_confs = full_conf.get(CONF_TIME, []) + + sntp_configs: list[ConfigType] = [] + other_time_configs: list[ConfigType] = [] + + for time_conf in time_confs: + if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + sntp_configs.append(time_conf) + else: + other_time_configs.append(time_conf) + + if len(sntp_configs) <= 1: + return + + # Merge all SNTP configs into the first one + merged = sntp_configs[0] + for sntp_conf in sntp_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple SNTP configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, sntp_conf) + + # Deduplicate servers while preserving order + servers = merged[CONF_SERVERS] + unique_servers = list(dict.fromkeys(servers)) + + # Warn if we're dropping servers due to 3-server limit + if len(unique_servers) > 3: + dropped = unique_servers[3:] + unique_servers = unique_servers[:3] + _LOGGER.warning( + "SNTP supports maximum 3 servers. Dropped excess server(s): %s", + dropped, + ) + + merged[CONF_SERVERS] = unique_servers + + _LOGGER.warning( + "Found and merged %d SNTP time configurations into one instance", + len(sntp_configs), + ) + + # Replace time configs with merged SNTP + other time platforms + other_time_configs.append(merged) + full_conf[CONF_TIME] = other_time_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = cv.All( time_.TIME_SCHEMA.extend( { @@ -40,6 +106,8 @@ CONFIG_SCHEMA = cv.All( ), ) +FINAL_VALIDATE_SCHEMA = _sntp_final_validate + async def to_code(config): servers = config[CONF_SERVERS] diff --git a/tests/component_tests/sntp/__init__.py b/tests/component_tests/sntp/__init__.py new file mode 100644 index 0000000000..7d323a4980 --- /dev/null +++ b/tests/component_tests/sntp/__init__.py @@ -0,0 +1 @@ +"""Tests for SNTP component.""" diff --git a/tests/component_tests/sntp/config/sntp_test.yaml b/tests/component_tests/sntp/config/sntp_test.yaml new file mode 100644 index 0000000000..3942c9606b --- /dev/null +++ b/tests/component_tests/sntp/config/sntp_test.yaml @@ -0,0 +1,22 @@ +esphome: + name: sntp-test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "testssid" + password: "testpassword" + +# Test multiple SNTP instances that should be merged +time: + - platform: sntp + servers: + - 192.168.1.1 + - pool.ntp.org + - platform: sntp + servers: + - pool.ntp.org + - 192.168.1.2 diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py new file mode 100644 index 0000000000..9197ff55d0 --- /dev/null +++ b/tests/component_tests/sntp/test_init.py @@ -0,0 +1,238 @@ +"""Tests for SNTP time configuration validation.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME +from esphome.core import ID +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("time_configs", "expected_count", "expected_servers", "warning_messages"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + } + ], + 1, + ["192.168.1.1", "pool.ntp.org"], + [], + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_preserves_order", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_3", is_manual=False), + CONF_SERVERS: ["pool3.ntp.org"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + [ + "SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']", + "Found and merged 3 SNTP time configurations into one instance", + ], + id="three_instances_drops_excess_servers", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: [ + "192.168.1.1", + "pool.ntp.org", + "pool.ntp.org", + "192.168.1.1", + ], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_multiple_duplicates", + ), + ], +) +def test_sntp_instance_merging( + time_configs: list[dict[str, Any]], + expected_count: int, + expected_servers: list[str], + warning_messages: list[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SNTP instance merging behavior.""" + # Create a mock full config with time configs + full_conf = {CONF_TIME: time_configs.copy()} + + # Set the context var + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + # Get the updated config + updated_conf = fv.full_config.get() + + # Check if merging occurred + if len(time_configs) > 1: + # Verify only one SNTP instance remains + sntp_instances = [ + tc + for tc in updated_conf[CONF_TIME] + if tc.get(CONF_PLATFORM) == CONF_SNTP + ] + assert len(sntp_instances) == expected_count + + # Verify server list + assert sntp_instances[0][CONF_SERVERS] == expected_servers + + # Verify warnings + for expected_msg in warning_messages: + assert any( + expected_msg in record.message for record in caplog.records + ), f"Expected warning message '{expected_msg}' not found in log" + else: + # Single instance should not trigger merging or warnings + assert len(caplog.records) == 0 + # Config should be unchanged + assert updated_conf[CONF_TIME] == time_configs + finally: + fv.full_config.reset(token) + + +def test_sntp_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + # Create configs with manual IDs that are inconsistent + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=True), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=True), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple SNTP configurations but id is inconsistent", + ): + _sntp_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None: + """Test that SNTP merging doesn't affect other time platforms.""" + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: "homeassistant", + CONF_ID: ID("homeassistant_time", is_manual=False), + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + updated_conf = fv.full_config.get() + + # Should have 2 time platforms: 1 merged SNTP + 1 homeassistant + assert len(updated_conf[CONF_TIME]) == 2 + + # Find the platforms + platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} + assert platforms == {CONF_SNTP, "homeassistant"} + + # Verify SNTP was merged + sntp_instances = [ + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + ] + assert len(sntp_instances) == 1 + assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] + finally: + fv.full_config.reset(token) From 96ee38759d7f5a6dd0e9ea7ca4331007ef49e936 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:52 -0600 Subject: [PATCH 010/102] [web_server.ota] Merge multiple instances to prevent undefined behavior (#11905) --- esphome/components/web_server/ota/__init__.py | 58 ++++++- .../ota/test_web_server_ota.py | 153 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 4a98db8877..260e6aea6d 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -1,10 +1,17 @@ +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] @@ -12,6 +19,53 @@ DEPENDENCIES = ["network", "web_server_base"] web_server_ns = cg.esphome_ns.namespace("web_server") WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +def _web_server_ota_final_validate(config: ConfigType) -> None: + """Merge multiple web_server OTA instances into one. + + Multiple web_server OTA instances register duplicate HTTP handlers for /update, + causing undefined behavior. Merge them into a single instance. + """ + full_conf = fv.full_config.get() + ota_confs = full_conf.get(CONF_OTA, []) + + web_server_ota_configs: list[ConfigType] = [] + other_ota_configs: list[ConfigType] = [] + + for ota_conf in ota_confs: + if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + web_server_ota_configs.append(ota_conf) + else: + other_ota_configs.append(ota_conf) + + if len(web_server_ota_configs) <= 1: + return + + # Merge all web_server OTA configs into the first one + merged = web_server_ota_configs[0] + for ota_conf in web_server_ota_configs[1:]: + # Validate that IDs are consistent if manually specified + if ( + merged[CONF_ID].is_manual + and ota_conf[CONF_ID].is_manual + and merged[CONF_ID] != ota_conf[CONF_ID] + ): + raise cv.Invalid( + f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, ota_conf) + + _LOGGER.warning( + "Found and merged %d web_server OTA configurations into one instance", + len(web_server_ota_configs), + ) + + # Replace OTA configs with merged web_server + other OTA platforms + other_ota_configs.append(merged) + full_conf[CONF_OTA] = other_ota_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -22,6 +76,8 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate + @coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) async def to_code(config): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 0d8ff6f134..794eaac9be 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -1,6 +1,18 @@ """Tests for the web_server OTA platform.""" +from __future__ import annotations + from collections.abc import Callable +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server.ota import _web_server_ota_final_validate +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER +from esphome.core import ID +import esphome.final_validate as fv def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: @@ -100,3 +112,144 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None: # Check web server OTA component is present assert "WebServerOTAComponent" in main_cpp assert "web_server::WebServerOTAComponent" in main_cpp + + +@pytest.mark.parametrize( + ("ota_configs", "expected_count", "warning_expected"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=False), + } + ], + 1, + False, + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 1, + True, + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: "esphome", + CONF_ID: ID("ota_esphome", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 2, + True, + id="mixed_platforms_web_server_merged", + ), + ], +) +def test_web_server_ota_instance_merging( + ota_configs: list[dict[str, Any]], + expected_count: int, + warning_expected: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test web_server OTA instance merging behavior.""" + full_conf = {CONF_OTA: ota_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + + # Verify total number of OTA platforms + assert len(updated_conf[CONF_OTA]) == expected_count + + # Verify warning + if warning_expected: + assert any( + "Found and merged" in record.message + and "web_server OTA" in record.message + for record in caplog.records + ), "Expected merge warning not found in log" + else: + assert len(caplog.records) == 0, "Unexpected warnings logged" + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_consistent_manual_ids( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that consistent manual IDs can be merged successfully.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + assert len(updated_conf[CONF_OTA]) == 1 + assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web" + assert any( + "Found and merged" in record.message and "web_server OTA" in record.message + for record in caplog.records + ) + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple web_server OTA configurations but id is inconsistent", + ): + _web_server_ota_final_validate({}) + finally: + fv.full_config.reset(token) From 3b860e784c462ee07c74b79a0393d29f635d876d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:39:01 -0600 Subject: [PATCH 011/102] [web_server_idf] Fix lwIP assertion crash by shutting down sockets on connection close (#11937) --- .../components/web_server_idf/web_server_idf.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 0dab5e7e8c..ce91569de2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -489,10 +489,18 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); - // Mark as dead by setting fd to 0 - will be cleaned up in the main loop - rsp->fd_.store(0); - // Note: We don't delete or remove from set here to avoid race conditions + int fd = rsp->fd_.exchange(0); // Atomically get and clear fd + + if (fd > 0) { + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Immediately shut down the socket to prevent lwIP from delivering more data + // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack + // tries to deliver queued data after the session is marked as dead + // See: https://github.com/esphome/esphome/issues/11936 + shutdown(fd, SHUT_RDWR); + // Note: We don't close() the socket - httpd owns it and will close it + } + // Session will be cleaned up in the main loop to avoid race conditions } // helper for allowing only unique entries in the queue From aa097a2fe6ba533a23b1598d44290c677d03ef04 Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Mon, 17 Nov 2025 07:25:00 +0600 Subject: [PATCH 012/102] [uart] Setup uart pins only if flags are set (#11914) Co-authored-by: J. Nick Koston --- .../components/uart/uart_component_esp8266.cpp | 18 +++++++++++++----- .../components/uart/uart_component_esp_idf.cpp | 18 +++++++++++++----- .../uart/uart_component_libretiny.cpp | 2 +- .../components/uart/uart_component_rp2040.cpp | 18 +++++++++++++----- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 7a453dbb50..c84a877ef4 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -56,11 +56,19 @@ uint32_t ESP8266UartComponent::get_config() { } void ESP8266UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + 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) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } // Use Arduino HardwareSerial UARTs if all used pins match the ones diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 70a13c9e37..61ca8c1c0c 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -133,11 +133,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + 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) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 8d1d28fce4..1e408b169b 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -53,7 +53,7 @@ void LibreTinyUARTComponent::setup() { auto shouldFallbackToSoftwareSerial = [&]() -> bool { auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { - return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE; + return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE; }; if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index c78691653d..cd3905b5c1 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -52,11 +52,19 @@ uint16_t RP2040UartComponent::get_config() { } void RP2040UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + 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) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } uint16_t config = get_config(); From aed80732f965ec9dc93fb1bde0dc5d9e603f866e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 14 Nov 2025 00:55:48 +1000 Subject: [PATCH 013/102] [esp32] Make esp-idf default framework for P4 (#11884) --- esphome/components/esp32/__init__.py | 163 ++++++++++++--------------- 1 file changed, 74 insertions(+), 89 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 61511cba0c..9741dc76a1 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -381,8 +381,9 @@ PLATFORM_VERSION_LOOKUP = { } -def _check_versions(value): - value = value.copy() +def _check_versions(config): + config = config.copy() + value = config[CONF_FRAMEWORK] if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: @@ -447,7 +448,7 @@ def _check_versions(value): "If there are connectivity or build issues please remove the manual version." ) - return value + return config def _parse_platform_version(value): @@ -598,89 +599,72 @@ def _validate_idf_component(config: ConfigType) -> ConfigType: FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" -FRAMEWORK_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of( - FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO - ), - cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_RELEASE): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, - cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { - cv.string_strict: cv.string_strict - }, - cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( - *LOG_LEVELS_IDF, upper=True - ), - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( - { - cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( - *ASSERTION_LEVELS, upper=True - ), - cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( - *COMPILER_OPTIMIZATIONS, upper=True - ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, - cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, - cv.Optional( - CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False - ): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, - # DHCP server is needed for WiFi AP mode. When WiFi component is used, - # it will handle disabling DHCP server when AP is not configured. - # Default to false (disabled) when WiFi is not used. - cv.OnlyWithout( - CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_SELECT, default=True - ): cv.boolean, - cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, - cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( - min=8192, max=32768 - ), - } - ), - cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - cv.All( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.git_ref, - cv.Optional(CONF_REF): cv.string, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), - } - ), - _validate_idf_component, - ) - ), - } - ), - _check_versions, +FRAMEWORK_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO), + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_RELEASE): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { + cv.string_strict: cv.string_strict + }, + cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( + *LOG_LEVELS_IDF, upper=True + ), + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( + *ASSERTION_LEVELS, upper=True + ), + cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( + *COMPILER_OPTIMIZATIONS, upper=True + ), + cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, + # DHCP server is needed for WiFi AP mode. When WiFi component is used, + # it will handle disabling DHCP server when AP is not configured. + # Default to false (disabled) when WiFi is not used. + cv.OnlyWithout( + CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False + ): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_MDNS_QUERIES, default=True): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True + ): cv.boolean, + cv.Optional(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( + min=8192, max=32768 + ), + } + ), + cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh), + } + ), + _validate_idf_component, + ) + ), + } ) @@ -743,11 +727,11 @@ def _show_framework_migration_message(name: str, variant: str) -> None: def _set_default_framework(config): + config = config.copy() if CONF_FRAMEWORK not in config: - config = config.copy() - - variant = config[CONF_VARIANT] config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) + if CONF_TYPE not in config[CONF_FRAMEWORK]: + variant = config[CONF_VARIANT] if variant in ARDUINO_ALLOWED_VARIANTS: config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO _show_framework_migration_message( @@ -787,6 +771,7 @@ CONFIG_SCHEMA = cv.All( ), _detect_variant, _set_default_framework, + _check_versions, set_core_data, cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) From fe00e209fff4f9c2e3e3b4c4664a300503488e94 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:52:08 +1000 Subject: [PATCH 014/102] [esp32] Add sdkconfig flag to make OTA work for 32MB flash (#11883) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 81 +++++++++---------- tests/components/esp32/test.esp32-p4-idf.yaml | 27 +++++++ 2 files changed, 67 insertions(+), 41 deletions(-) create mode 100644 tests/components/esp32/test.esp32-p4-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9741dc76a1..0f85e585f7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -498,6 +498,8 @@ def final_validate(config): from esphome.components.psram import DOMAIN as PSRAM_DOMAIN errs = [] + conf_fw = config[CONF_FRAMEWORK] + advanced = conf_fw[CONF_ADVANCED] full_config = fv.full_config.get() if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS): pio_flash_size_key = "board_upload.flash_size" @@ -514,22 +516,14 @@ def final_validate(config): f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" ) ) - if ( - config[CONF_VARIANT] != VARIANT_ESP32 - and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) - and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] - ): + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_IGNORE_EFUSE_MAC_CRC]: errs.append( cv.Invalid( f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], ) ) - if ( - config.get(CONF_FRAMEWORK, {}) - .get(CONF_ADVANCED, {}) - .get(CONF_EXECUTE_FROM_PSRAM) - ): + if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( cv.Invalid( @@ -545,6 +539,17 @@ def final_validate(config): ) ) + if ( + config[CONF_FLASH_SIZE] == "32MB" + and "ota" in full_config + and not advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES] + ): + errs.append( + cv.Invalid( + f"OTA with 32MB flash requires '{CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES}' to be set in the '{CONF_ADVANCED}' section of the esp32 configuration", + path=[CONF_FLASH_SIZE], + ) + ) if errs: raise cv.MultipleInvalid(errs) @@ -620,10 +625,12 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( *COMPILER_OPTIMIZATIONS, upper=True ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, + cv.Optional( + CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, default=False + ): cv.boolean, cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -644,7 +651,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 ), @@ -790,9 +797,7 @@ def _configure_lwip_max_sockets(conf: dict) -> None: from esphome.components.socket import KEY_SOCKET_CONSUMERS # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS - user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get( - "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()) @@ -962,23 +967,18 @@ async def to_code(config): # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available # for the Network library to compile, even if not actively used - if ( - CONF_ENABLE_LWIP_DHCP_SERVER in advanced - and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] - and not ( - conf[CONF_TYPE] == FRAMEWORK_ARDUINO - and "ethernet" in CORE.loaded_integrations - ) + if advanced.get(CONF_ENABLE_LWIP_DHCP_SERVER) is False and not ( + conf[CONF_TYPE] == FRAMEWORK_ARDUINO and "ethernet" in CORE.loaded_integrations ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): + if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]: add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) - if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): + if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]: add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) _configure_lwip_max_sockets(conf) - if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): + if advanced[CONF_EXECUTE_FROM_PSRAM]: add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) @@ -989,23 +989,22 @@ async def to_code(config): # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default) # - Up to 200% slower under load when all operations queue through tcpip_thread # Enabling this makes ESP-IDF socket performance match Arduino framework. - if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True): + if advanced[CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING]: add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True) - if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): + if advanced[CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY]: add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) # Disable placing libc locks in IRAM to save RAM # This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled) # use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM. - if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): + if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]: add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) # Disable VFS support for termios (terminal I/O functions) # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). # Saves approximately 1.8KB of flash when disabled (default). add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_TERMIOS", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), + "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] ) # Disable VFS support for select() with file descriptors @@ -1019,8 +1018,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_SELECT", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), + "CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT] ) # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) @@ -1033,8 +1031,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_DIR", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), + "CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR] ) cg.add_platformio_option("board_build.partitions", "partitions.csv") @@ -1048,7 +1045,7 @@ async def to_code(config): add_idf_sdkconfig_option(flag, assertion_level == key) add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) - compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION) + compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION] for key, flag in COMPILER_OPTIMIZATIONS.items(): add_idf_sdkconfig_option(flag, compiler_optimization == key) @@ -1057,18 +1054,20 @@ async def to_code(config): conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT], ) - if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC): + if advanced[CONF_IGNORE_EFUSE_MAC_CRC]: add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False) - if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): + if advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]: _LOGGER.warning( "Using experimental features in ESP-IDF may result in unexpected failures." ) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) + if config[CONF_FLASH_SIZE] == "32MB": + add_idf_sdkconfig_option( + "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True + ) - cg.add_define( - "ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE) - ) + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( "USE_ESP_IDF_VERSION_CODE", diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..a4c930f236 --- /dev/null +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -0,0 +1,27 @@ +esp32: + variant: esp32p4 + flash_size: 32MB + cpu_frequency: 400MHz + framework: + type: esp-idf + advanced: + enable_idf_experimental_features: yes + +ota: + platform: esphome + +wifi: + ssid: MySSID + password: password1 + +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 From eb54c0026dfcab03fbb841ce5fd374554fa61960 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:08:06 +0100 Subject: [PATCH 015/102] [light] Fix missing `ColorMode::BRIGHTNESS` case in logging (#11836) --- esphome/components/light/light_call.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 8365ac77cd..b15ff84b97 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -52,8 +52,10 @@ static void log_invalid_parameter(const char *name, const LogString *message) { } static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::UNKNOWN) - return LOG_STR("Unknown"); + if (color_mode == ColorMode::ON_OFF) + return LOG_STR("On/Off"); + if (color_mode == ColorMode::BRIGHTNESS) + return LOG_STR("Brightness"); if (color_mode == ColorMode::WHITE) return LOG_STR("White"); if (color_mode == ColorMode::COLOR_TEMPERATURE) @@ -68,7 +70,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { return LOG_STR("RGB + cold/warm white"); if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) return LOG_STR("RGB + color temperature"); - return LOG_STR(""); + return LOG_STR("Unknown"); } // Helper to log percentage values From 7ce94c27feeade6a50082ed9f14165c056fb2a71 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:13:48 +0100 Subject: [PATCH 016/102] [wifi] Allow `use_psram` with Arduino (#11902) --- esphome/components/wifi/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 4dbb425e4b..11bd7798e2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -12,7 +12,6 @@ from esphome.components.network import ( from esphome.components.psram import is_guaranteed as psram_is_guaranteed from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.config_validation import only_with_esp_idf from esphome.const import ( CONF_AP, CONF_BSSID, @@ -352,7 +351,7 @@ CONFIG_SCHEMA = cv.All( single=True ), cv.Optional(CONF_USE_PSRAM): cv.All( - only_with_esp_idf, cv.requires_component("psram"), cv.boolean + cv.only_on_esp32, cv.requires_component("psram"), cv.boolean ), } ), From 97c4914573b5b7444f44c526e1fb11d13cdcfe38 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:06:08 -0500 Subject: [PATCH 017/102] [uart] Improve error handling and validate buffer size (#11895) Co-authored-by: J. Nick Koston --- esphome/components/uart/__init__.py | 19 ++++++++++ .../uart/uart_component_esp_idf.cpp | 35 +++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index cbc11d0db0..7b0d9726b8 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,3 +1,4 @@ +from logging import getLogger import math import re @@ -35,6 +36,8 @@ from esphome.core import CORE, ID import esphome.final_validate as fv from esphome.yaml_util import make_data_base +_LOGGER = getLogger(__name__) + CODEOWNERS = ["@esphome/core"] uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -130,6 +133,21 @@ def validate_host_config(config): return config +def validate_rx_buffer_size(config): + if CORE.is_esp32: + # ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum) + # rx_buffer_size must be greater than the hardware FIFO length + min_buffer_size = 128 + if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size: + _LOGGER.warning( + "UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware " + "FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.", + config[CONF_RX_BUFFER_SIZE], + min_buffer_size, + ) + return config + + def _uart_declare_type(value): if CORE.is_esp8266: return cv.declare_id(ESP8266UartComponent)(value) @@ -247,6 +265,7 @@ CONFIG_SCHEMA = cv.All( ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT), validate_host_config, + validate_rx_buffer_size, ) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 73813d2d5b..70a13c9e37 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -91,6 +91,16 @@ void IDFUARTComponent::setup() { this->uart_num_ = static_cast(next_uart_num++); this->lock_ = xSemaphoreCreateMutex(); +#if (SOC_UART_LP_NUM >= 1) + size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); +#else + size_t fifo_len = SOC_UART_FIFO_LEN; +#endif + if (this->rx_buffer_size_ <= fifo_len) { + ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len); + this->rx_buffer_size_ = fifo_len * 2; + } + xSemaphoreTake(this->lock_, portMAX_DELAY); this->load_settings(false); @@ -237,8 +247,12 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_write_bytes(this->uart_num_, data, len); + int32_t write_len = uart_write_bytes(this->uart_num_, data, len); xSemaphoreGive(this->lock_); + if (write_len != (int32_t) len) { + ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); + this->mark_failed(); + } #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_TX, data[i]); @@ -267,6 +281,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { size_t length_to_read = len; + int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; xSemaphoreTake(this->lock_, portMAX_DELAY); @@ -277,25 +292,31 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; } if (length_to_read > 0) - uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } #endif - return true; + return read_len == (int32_t) length_to_read; } int IDFUARTComponent::available() { - size_t available; + size_t available = 0; + esp_err_t err; xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_get_buffered_data_len(this->uart_num_, &available); - if (this->has_peek_) - available++; + err = uart_get_buffered_data_len(this->uart_num_, &available); xSemaphoreGive(this->lock_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); + this->mark_failed(); + } + if (this->has_peek_) { + available++; + } return available; } From 6440b5fbf5d05e7b572d9272c8ea8865b59ed513 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:03:43 -0600 Subject: [PATCH 018/102] [ld2412] Fix stuck targets by adding timeout filter (#11919) --- esphome/components/ld2412/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index abb823faad..0bfbd9bf1d 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -31,36 +31,84 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor ), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), @@ -74,7 +122,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -82,7 +136,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From d559f9f52e26cc779528fdb5c015cb73560536f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:04:25 -0600 Subject: [PATCH 019/102] [ld2410] Add timeout filter to prevent stuck targets (#11920) --- esphome/components/ld2410/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index fca2b2ceca..3bd34963bc 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -31,35 +31,83 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), @@ -73,7 +121,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -81,7 +135,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From 36868ee7b173628e838951d3fb3c899e66b74866 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:20:57 -0600 Subject: [PATCH 020/102] [scheduler] Fix timing breakage after 49 days of uptime on ESP8266/RP2040 (#11924) --- esphome/core/scheduler.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d285af2d0e..d2e0f0dab4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -609,13 +609,12 @@ uint64_t Scheduler::millis_64_(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { this->millis_major_++; major++; + this->last_millis_ = now; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ - } - - // Only update if time moved forward - if (now > last) { + } else if (now > last) { + // Only update if time moved forward this->last_millis_ = now; } From f19296ac7f59c4b2610b127cd1663e11648cf645 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 07:35:31 -0600 Subject: [PATCH 021/102] [analyze-memory] Show all core symbols > 100 B instead of top 15 (#11909) --- esphome/analyze_memory/cli.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 718f42330d..44ade221f8 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,11 @@ from . import ( class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" + # Symbol size threshold for detailed analysis + SYMBOL_SIZE_THRESHOLD: int = ( + 100 # Show symbols larger than this in detailed analysis + ) + # Column width constants COL_COMPONENT: int = 29 COL_FLASH_TEXT: int = 14 @@ -191,14 +196,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" ) - # Top 15 largest core symbols + # All core symbols above threshold lines.append("") - lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) + large_core_symbols = [ + (symbol, demangled, size) + for symbol, demangled, size in sorted_core_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): + lines.append( + f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_core_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * self.TABLE_WIDTH) @@ -268,13 +280,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols > 100 bytes for better visibility + # Show all symbols above threshold for better visibility large_symbols = [ - (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100 + (sym, dem, size) + for sym, dem, size in sorted_symbols + if size > self.SYMBOL_SIZE_THRESHOLD ] lines.append( - f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size) in enumerate(large_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") From 91514894817fa318186b29dec48af397f9222ca9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:38 -0600 Subject: [PATCH 022/102] [sntp] Merge multiple instances to fix crash and undefined behavior (#11904) --- esphome/components/sntp/time.py | 68 +++++ tests/component_tests/sntp/__init__.py | 1 + .../sntp/config/sntp_test.yaml | 22 ++ tests/component_tests/sntp/test_init.py | 238 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 tests/component_tests/sntp/__init__.py create mode 100644 tests/component_tests/sntp/config/sntp_test.yaml create mode 100644 tests/component_tests/sntp/test_init.py diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index d27fc9991d..69a2436d3d 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,9 +1,14 @@ +import logging + import esphome.codegen as cg from esphome.components import time as time_ +from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_PLATFORM, CONF_SERVERS, + CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -12,13 +17,74 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] + +CONF_SNTP = "sntp" + sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] + +def _sntp_final_validate(config: ConfigType) -> None: + """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" + full_conf = fv.full_config.get() + time_confs = full_conf.get(CONF_TIME, []) + + sntp_configs: list[ConfigType] = [] + other_time_configs: list[ConfigType] = [] + + for time_conf in time_confs: + if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + sntp_configs.append(time_conf) + else: + other_time_configs.append(time_conf) + + if len(sntp_configs) <= 1: + return + + # Merge all SNTP configs into the first one + merged = sntp_configs[0] + for sntp_conf in sntp_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple SNTP configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, sntp_conf) + + # Deduplicate servers while preserving order + servers = merged[CONF_SERVERS] + unique_servers = list(dict.fromkeys(servers)) + + # Warn if we're dropping servers due to 3-server limit + if len(unique_servers) > 3: + dropped = unique_servers[3:] + unique_servers = unique_servers[:3] + _LOGGER.warning( + "SNTP supports maximum 3 servers. Dropped excess server(s): %s", + dropped, + ) + + merged[CONF_SERVERS] = unique_servers + + _LOGGER.warning( + "Found and merged %d SNTP time configurations into one instance", + len(sntp_configs), + ) + + # Replace time configs with merged SNTP + other time platforms + other_time_configs.append(merged) + full_conf[CONF_TIME] = other_time_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = cv.All( time_.TIME_SCHEMA.extend( { @@ -40,6 +106,8 @@ CONFIG_SCHEMA = cv.All( ), ) +FINAL_VALIDATE_SCHEMA = _sntp_final_validate + async def to_code(config): servers = config[CONF_SERVERS] diff --git a/tests/component_tests/sntp/__init__.py b/tests/component_tests/sntp/__init__.py new file mode 100644 index 0000000000..7d323a4980 --- /dev/null +++ b/tests/component_tests/sntp/__init__.py @@ -0,0 +1 @@ +"""Tests for SNTP component.""" diff --git a/tests/component_tests/sntp/config/sntp_test.yaml b/tests/component_tests/sntp/config/sntp_test.yaml new file mode 100644 index 0000000000..3942c9606b --- /dev/null +++ b/tests/component_tests/sntp/config/sntp_test.yaml @@ -0,0 +1,22 @@ +esphome: + name: sntp-test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "testssid" + password: "testpassword" + +# Test multiple SNTP instances that should be merged +time: + - platform: sntp + servers: + - 192.168.1.1 + - pool.ntp.org + - platform: sntp + servers: + - pool.ntp.org + - 192.168.1.2 diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py new file mode 100644 index 0000000000..9197ff55d0 --- /dev/null +++ b/tests/component_tests/sntp/test_init.py @@ -0,0 +1,238 @@ +"""Tests for SNTP time configuration validation.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME +from esphome.core import ID +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("time_configs", "expected_count", "expected_servers", "warning_messages"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + } + ], + 1, + ["192.168.1.1", "pool.ntp.org"], + [], + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_preserves_order", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_3", is_manual=False), + CONF_SERVERS: ["pool3.ntp.org"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + [ + "SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']", + "Found and merged 3 SNTP time configurations into one instance", + ], + id="three_instances_drops_excess_servers", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: [ + "192.168.1.1", + "pool.ntp.org", + "pool.ntp.org", + "192.168.1.1", + ], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_multiple_duplicates", + ), + ], +) +def test_sntp_instance_merging( + time_configs: list[dict[str, Any]], + expected_count: int, + expected_servers: list[str], + warning_messages: list[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SNTP instance merging behavior.""" + # Create a mock full config with time configs + full_conf = {CONF_TIME: time_configs.copy()} + + # Set the context var + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + # Get the updated config + updated_conf = fv.full_config.get() + + # Check if merging occurred + if len(time_configs) > 1: + # Verify only one SNTP instance remains + sntp_instances = [ + tc + for tc in updated_conf[CONF_TIME] + if tc.get(CONF_PLATFORM) == CONF_SNTP + ] + assert len(sntp_instances) == expected_count + + # Verify server list + assert sntp_instances[0][CONF_SERVERS] == expected_servers + + # Verify warnings + for expected_msg in warning_messages: + assert any( + expected_msg in record.message for record in caplog.records + ), f"Expected warning message '{expected_msg}' not found in log" + else: + # Single instance should not trigger merging or warnings + assert len(caplog.records) == 0 + # Config should be unchanged + assert updated_conf[CONF_TIME] == time_configs + finally: + fv.full_config.reset(token) + + +def test_sntp_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + # Create configs with manual IDs that are inconsistent + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=True), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=True), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple SNTP configurations but id is inconsistent", + ): + _sntp_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None: + """Test that SNTP merging doesn't affect other time platforms.""" + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: "homeassistant", + CONF_ID: ID("homeassistant_time", is_manual=False), + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + updated_conf = fv.full_config.get() + + # Should have 2 time platforms: 1 merged SNTP + 1 homeassistant + assert len(updated_conf[CONF_TIME]) == 2 + + # Find the platforms + platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} + assert platforms == {CONF_SNTP, "homeassistant"} + + # Verify SNTP was merged + sntp_instances = [ + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + ] + assert len(sntp_instances) == 1 + assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] + finally: + fv.full_config.reset(token) From 3fd58f1a917f3d69af101a544f2688d70ec69228 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:52 -0600 Subject: [PATCH 023/102] [web_server.ota] Merge multiple instances to prevent undefined behavior (#11905) --- esphome/components/web_server/ota/__init__.py | 58 ++++++- .../ota/test_web_server_ota.py | 153 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 4a98db8877..260e6aea6d 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -1,10 +1,17 @@ +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] @@ -12,6 +19,53 @@ DEPENDENCIES = ["network", "web_server_base"] web_server_ns = cg.esphome_ns.namespace("web_server") WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +def _web_server_ota_final_validate(config: ConfigType) -> None: + """Merge multiple web_server OTA instances into one. + + Multiple web_server OTA instances register duplicate HTTP handlers for /update, + causing undefined behavior. Merge them into a single instance. + """ + full_conf = fv.full_config.get() + ota_confs = full_conf.get(CONF_OTA, []) + + web_server_ota_configs: list[ConfigType] = [] + other_ota_configs: list[ConfigType] = [] + + for ota_conf in ota_confs: + if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + web_server_ota_configs.append(ota_conf) + else: + other_ota_configs.append(ota_conf) + + if len(web_server_ota_configs) <= 1: + return + + # Merge all web_server OTA configs into the first one + merged = web_server_ota_configs[0] + for ota_conf in web_server_ota_configs[1:]: + # Validate that IDs are consistent if manually specified + if ( + merged[CONF_ID].is_manual + and ota_conf[CONF_ID].is_manual + and merged[CONF_ID] != ota_conf[CONF_ID] + ): + raise cv.Invalid( + f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, ota_conf) + + _LOGGER.warning( + "Found and merged %d web_server OTA configurations into one instance", + len(web_server_ota_configs), + ) + + # Replace OTA configs with merged web_server + other OTA platforms + other_ota_configs.append(merged) + full_conf[CONF_OTA] = other_ota_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -22,6 +76,8 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate + @coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) async def to_code(config): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 0d8ff6f134..794eaac9be 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -1,6 +1,18 @@ """Tests for the web_server OTA platform.""" +from __future__ import annotations + from collections.abc import Callable +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server.ota import _web_server_ota_final_validate +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER +from esphome.core import ID +import esphome.final_validate as fv def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: @@ -100,3 +112,144 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None: # Check web server OTA component is present assert "WebServerOTAComponent" in main_cpp assert "web_server::WebServerOTAComponent" in main_cpp + + +@pytest.mark.parametrize( + ("ota_configs", "expected_count", "warning_expected"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=False), + } + ], + 1, + False, + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 1, + True, + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: "esphome", + CONF_ID: ID("ota_esphome", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 2, + True, + id="mixed_platforms_web_server_merged", + ), + ], +) +def test_web_server_ota_instance_merging( + ota_configs: list[dict[str, Any]], + expected_count: int, + warning_expected: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test web_server OTA instance merging behavior.""" + full_conf = {CONF_OTA: ota_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + + # Verify total number of OTA platforms + assert len(updated_conf[CONF_OTA]) == expected_count + + # Verify warning + if warning_expected: + assert any( + "Found and merged" in record.message + and "web_server OTA" in record.message + for record in caplog.records + ), "Expected merge warning not found in log" + else: + assert len(caplog.records) == 0, "Unexpected warnings logged" + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_consistent_manual_ids( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that consistent manual IDs can be merged successfully.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + assert len(updated_conf[CONF_OTA]) == 1 + assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web" + assert any( + "Found and merged" in record.message and "web_server OTA" in record.message + for record in caplog.records + ) + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple web_server OTA configurations but id is inconsistent", + ): + _web_server_ota_final_validate({}) + finally: + fv.full_config.reset(token) From 9e02e3191798010befd85006f17ebebc4a03b360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:39:01 -0600 Subject: [PATCH 024/102] [web_server_idf] Fix lwIP assertion crash by shutting down sockets on connection close (#11937) --- .../components/web_server_idf/web_server_idf.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 0dab5e7e8c..ce91569de2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -489,10 +489,18 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); - // Mark as dead by setting fd to 0 - will be cleaned up in the main loop - rsp->fd_.store(0); - // Note: We don't delete or remove from set here to avoid race conditions + int fd = rsp->fd_.exchange(0); // Atomically get and clear fd + + if (fd > 0) { + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Immediately shut down the socket to prevent lwIP from delivering more data + // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack + // tries to deliver queued data after the session is marked as dead + // See: https://github.com/esphome/esphome/issues/11936 + shutdown(fd, SHUT_RDWR); + // Note: We don't close() the socket - httpd owns it and will close it + } + // Session will be cleaned up in the main loop to avoid race conditions } // helper for allowing only unique entries in the queue From 6c6b03bda05a32519b0690e11b12706f1e2df24d Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Mon, 17 Nov 2025 07:25:00 +0600 Subject: [PATCH 025/102] [uart] Setup uart pins only if flags are set (#11914) Co-authored-by: J. Nick Koston --- .../components/uart/uart_component_esp8266.cpp | 18 +++++++++++++----- .../components/uart/uart_component_esp_idf.cpp | 18 +++++++++++++----- .../uart/uart_component_libretiny.cpp | 2 +- .../components/uart/uart_component_rp2040.cpp | 18 +++++++++++++----- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 7a453dbb50..c84a877ef4 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -56,11 +56,19 @@ uint32_t ESP8266UartComponent::get_config() { } void ESP8266UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + 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) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } // Use Arduino HardwareSerial UARTs if all used pins match the ones diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 70a13c9e37..61ca8c1c0c 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -133,11 +133,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + 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) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 8d1d28fce4..1e408b169b 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -53,7 +53,7 @@ void LibreTinyUARTComponent::setup() { auto shouldFallbackToSoftwareSerial = [&]() -> bool { auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { - return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE; + return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE; }; if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index c78691653d..cd3905b5c1 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -52,11 +52,19 @@ uint16_t RP2040UartComponent::get_config() { } void RP2040UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + 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) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } uint16_t config = get_config(); From a38c4e0c6e4c4e4bf960db6f1e75f74afdfcb574 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:32:09 +1300 Subject: [PATCH 026/102] Bump version to 2025.11.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 8766b8f00c..04046d9ce6 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 = 2025.11.0b2 +PROJECT_NUMBER = 2025.11.0b3 # 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 8c6020a868..2ca0018fdb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b2" +__version__ = "2025.11.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 10bdb47eae14bd3fd1cd761737b0633cd9c9894b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 20:37:06 -0600 Subject: [PATCH 027/102] [cover] Modernize to C++17 nested namespaces (#11935) --- esphome/components/cover/automation.h | 6 ++---- esphome/components/cover/cover.cpp | 6 ++---- esphome/components/cover/cover.h | 6 ++---- esphome/components/cover/cover_traits.h | 6 ++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 752e0398c1..c0345a7cc6 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "cover.h" -namespace esphome { -namespace cover { +namespace esphome::cover { template class OpenAction : public Action { public: @@ -131,5 +130,4 @@ class CoverClosedTrigger : public Trigger<> { } }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 3062dba28a..8f735982f1 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace cover { +namespace esphome::cover { static const char *const TAG = "cover"; @@ -212,5 +211,4 @@ void CoverRestoreState::apply(Cover *cover) { cover->publish_state(); } -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index d5db6cfb4f..6c69c05e71 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -7,8 +7,7 @@ #include "cover_traits.h" -namespace esphome { -namespace cover { +namespace esphome::cover { const extern float COVER_OPEN; const extern float COVER_CLOSED; @@ -157,5 +156,4 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { ESPPreferenceObject rtc_; }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover_traits.h b/esphome/components/cover/cover_traits.h index 79001c3b03..723516318b 100644 --- a/esphome/components/cover/cover_traits.h +++ b/esphome/components/cover/cover_traits.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace cover { +namespace esphome::cover { class CoverTraits { public: @@ -26,5 +25,4 @@ class CoverTraits { bool supports_stop_{false}; }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover From 9e1f8d83f884b8fb9c168c1ea3909ef3fc365c21 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 17 Nov 2025 08:03:11 +0100 Subject: [PATCH 028/102] [config] Support !remove and !extend with LVGL-style configs (#11534) --- esphome/config.py | 59 +++++++++++-------- .../05-extend-remove.approved.yaml | 24 ++++++++ .../substitutions/05-extend-remove.input.yaml | 44 ++++++++++++++ 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index e508ca585b..4c8019de75 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -338,21 +338,44 @@ def check_replaceme(value): ) -def _build_list_index(lst): +def _get_item_id(item: Any) -> str | Extend | Remove | None: + """Attempts to get a list item's ID""" + if not isinstance(item, dict): + return None # not a dict, can't have ID + # 1.- Check regular case: + # - id: my_id + item_id = item.get(CONF_ID) + if item_id is None and len(item) == 1: + # 2.- Check single-key dict case: + # - obj: + # id: my_id + item = next(iter(item.values())) + if isinstance(item, dict): + item_id = item.get(CONF_ID) + if isinstance(item_id, Extend): + # Remove instances of Extend so they don't overwrite the original item when merging: + del item[CONF_ID] + return item_id + + +def _build_list_index( + lst: list[Any], +) -> tuple[ + OrderedDict[str | Extend | Remove, Any], list[tuple[int, str, Any]], set[str] +]: index = OrderedDict() extensions, removals = [], set() - for item in lst: + for pos, item in enumerate(lst): if item is None: removals.add(None) continue - item_id = None - if isinstance(item, dict) and (item_id := item.get(CONF_ID)): - if isinstance(item_id, Extend): - extensions.append(item) - continue - if isinstance(item_id, Remove): - removals.add(item_id.value) - continue + item_id = _get_item_id(item) + if isinstance(item_id, Extend): + extensions.append((pos, item_id.value, item)) + continue + if isinstance(item_id, Remove): + removals.add(item_id.value) + continue if not item_id or item_id in index: # no id or duplicate -> pass through with identity-based key item_id = id(item) @@ -360,7 +383,7 @@ def _build_list_index(lst): return index, extensions, removals -def resolve_extend_remove(value, is_key=None): +def resolve_extend_remove(value: Any, is_key: bool = False) -> None: if isinstance(value, ESPLiteralValue): return # do not check inside literal blocks if isinstance(value, list): @@ -368,26 +391,16 @@ def resolve_extend_remove(value, is_key=None): if extensions or removals: # Rebuild the original list after # processing all extensions and removals - for item in extensions: - item_id = item[CONF_ID].value + for pos, item_id, item in extensions: if item_id in removals: continue old = index.get(item_id) if old is None: # Failed to find source for extension - # Find index of item to show error at correct position - i = next( - ( - i - for i, d in enumerate(value) - if d.get(CONF_ID) == item[CONF_ID] - ) - ) - with cv.prepend_path(i): + with cv.prepend_path(pos): raise cv.Invalid( f"Source for extension of ID '{item_id}' was not found." ) - item[CONF_ID] = item_id index[item_id] = merge_config(old, item) for item_id in removals: index.pop(item_id, None) diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml index a479370f4b..35e3e6258f 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml @@ -7,3 +7,27 @@ some_component: value: 2 - id: component2 value: 5 +lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 3 + y: 2 + width: 4 + - obj: + id: object3 + x: 6 + y: 12 + widgets: + - obj: + id: object4 + x: 14 + y: 9 + width: 15 + height: 13 + - obj: + id: object5 + x: 10 + y: 11 diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml index 2e0e60798d..617f09c31c 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml @@ -13,6 +13,30 @@ packages: value: 5 - id: component3 value: 6 + - lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 1 + y: 2 + - obj: + id: object2 + x: 5 + - obj: + id: object3 + x: 6 + y: 7 + widgets: + - obj: + id: object4 + x: 8 + y: 9 + - obj: + id: object5 + x: 10 + y: 11 some_component: - id: !extend ${A} @@ -20,3 +44,23 @@ some_component: - id: component2 value: 3 - id: !remove ${C} + +lvgl: + pages: + - id: !extend page1 + widgets: + - obj: + id: !extend object1 + x: 3 + width: 4 + - obj: + id: !remove object2 + - obj: + id: !extend object3 + y: 12 + height: 13 + widgets: + - obj: + id: !extend object4 + x: 14 + width: 15 From 3d6c361037098f2a7990d1c41c0b4b50788afb64 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:32:08 -0500 Subject: [PATCH 029/102] [core] Add support for setting environment variables (#11953) --- esphome/const.py | 1 + esphome/core/config.py | 15 +++++++++++++++ tests/components/esphome/common.yaml | 3 +++ 3 files changed, 19 insertions(+) diff --git a/esphome/const.py b/esphome/const.py index a25114d80e..b4cd3cfd1c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -336,6 +336,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_ENVIRONMENT_VARIABLES = "environment_variables" CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" diff --git a/esphome/core/config.py b/esphome/core/config.py index 763f9ebd9f..0a239c5f5e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, CONF_DEVICES, + CONF_ENVIRONMENT_VARIABLES, CONF_ESPHOME, CONF_FRIENDLY_NAME, CONF_ID, @@ -215,6 +216,11 @@ CONFIG_SCHEMA = cv.All( cv.string_strict: cv.Any([cv.string], cv.string), } ), + cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + { + cv.string_strict: cv.string, + } + ), cv.Optional(CONF_ON_BOOT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), @@ -426,6 +432,12 @@ async def _add_platformio_options(pio_options): cg.add_platformio_option(key, val) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_environment_variables(env_vars: dict[str, str]) -> None: + # Set environment variables for the build process + os.environ.update(env_vars) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): @@ -563,6 +575,9 @@ async def to_code(config: ConfigType) -> None: if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + if config[CONF_ENVIRONMENT_VARIABLES]: + CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES]) + # Process areas all_areas: list[dict[str, str | core.ID]] = [] if CONF_AREA in config: diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index b2d7bccaa5..db75b08b38 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,6 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio + environment_variables: + TEST_ENV_VAR: "test_value" + BUILD_NUMBER: "12345" area: id: testing_area name: Testing Area From 7a238028a7de73b37a6edd8abf0083cdca00aeb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:38:44 -0600 Subject: [PATCH 030/102] Bump ruamel-yaml-clib from 0.2.14 to 0.2.15 (#11956) 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 40802422f2..6ae050b35b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ aioesphomeapi==42.7.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import -ruamel.yaml.clib==0.2.14 # dashboard_import +ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==11.3.0 cairosvg==2.8.2 From 23f85162d0dd2216001812d79b60786a12f8a130 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:39:01 -0600 Subject: [PATCH 031/102] Bump actions/checkout from 5.0.0 to 5.0.1 (#11957) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci-clang-tidy-hash.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .../workflows/ci-memory-impact-comment.yml | 2 +- .github/workflows/ci.yml | 30 +++++++++---------- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 8 ++--- .github/workflows/sync-device-classes.yml | 4 +-- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index dd1bc29d83..fb284c9d8c 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -22,7 +22,7 @@ jobs: if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Generate a token id: generate-token diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 400373679f..be5af1aff1 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 78d1c2b87f..aebf07949d 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 7111c61dda..9bb983b993 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,7 +43,7 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml index eea1d2c148..c82ae30f55 100644 --- a/.github/workflows/ci-memory-impact-comment.yml +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -49,7 +49,7 @@ jobs: - name: Check out code from base repository if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Always check out from the base repository (esphome/esphome), never from forks # Use the PR's target branch to ensure we run trusted code from the main repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16837b3186..5293c62d34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT @@ -70,7 +70,7 @@ jobs: if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -91,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -132,7 +132,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python id: restore-python uses: ./.github/actions/restore-python @@ -183,7 +183,7 @@ jobs: component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -237,7 +237,7 @@ jobs: if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python 3.13 id: python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 @@ -273,7 +273,7 @@ jobs: if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python @@ -321,7 +321,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -400,7 +400,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -489,7 +489,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -577,7 +577,7 @@ jobs: version: 1.0 - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -662,7 +662,7 @@ jobs: if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -688,7 +688,7 @@ jobs: skip: ${{ steps.check-script.outputs.skip }} steps: - name: Check out target branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ github.base_ref }} @@ -840,7 +840,7 @@ jobs: flash_usage: ${{ steps.extract.outputs.flash_usage }} steps: - name: Check out PR branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -908,7 +908,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2273975328..adb2a9d79f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75d88abf29..a064f6ef3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,7 +60,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -92,7 +92,7 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -168,7 +168,7 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Download digests uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 9479645ccc..4fc287b067 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,10 +13,10 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Checkout Home Assistant - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: repository: home-assistant/core path: lib/home-assistant From 1a73f49cd23080a500be9fd9d98de3ebccd02fbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 17:20:18 -0600 Subject: [PATCH 032/102] [number] Modernize to C++17 nested namespaces (#11945) --- esphome/components/number/automation.cpp | 6 ++---- esphome/components/number/automation.h | 6 ++---- esphome/components/number/number.cpp | 6 ++---- esphome/components/number/number.h | 6 ++---- esphome/components/number/number_call.cpp | 6 ++---- esphome/components/number/number_call.h | 6 ++---- esphome/components/number/number_traits.cpp | 6 ++---- esphome/components/number/number_traits.h | 6 ++---- 8 files changed, 16 insertions(+), 32 deletions(-) diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index bfc59d0465..78ffc255fe 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number.automation"; @@ -52,5 +51,4 @@ void ValueRangeTrigger::on_state_(float state) { this->rtc_.save(&in_range); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 79eba883c4..a7cd04f083 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace number { +namespace esphome::number { class NumberStateTrigger : public Trigger { public: @@ -91,5 +90,4 @@ template class NumberInRangeCondition : public Condition float max_{NAN}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index f12e0e9e1e..992100ead0 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -43,5 +42,4 @@ void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index da91d70d53..472e06ad61 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -6,8 +6,7 @@ #include "number_call.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; void log_number(const char *tag, const char *prefix, const char *type, Number *obj); @@ -53,5 +52,4 @@ class Number : public EntityBase { CallbackManager state_callback_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 669dd65184..27a857c112 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -2,8 +2,7 @@ #include "number.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -125,5 +124,4 @@ void NumberCall::perform() { this->parent_->control(target_value); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 807207f0ec..0f6889dcb6 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; @@ -44,5 +43,4 @@ class NumberCall { bool cycle_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp index 89035661f5..1e4239ceca 100644 --- a/esphome/components/number/number_traits.cpp +++ b/esphome/components/number/number_traits.cpp @@ -1,10 +1,8 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index fa68c2390a..5ccbb9ba48 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace number { +namespace esphome::number { enum NumberMode : uint8_t { NUMBER_MODE_AUTO = 0, @@ -35,5 +34,4 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas NumberMode mode_{NUMBER_MODE_AUTO}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number From fdc7ae776071c0638774f122d3f216f2ed45af48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 17:20:32 -0600 Subject: [PATCH 033/102] [wifi] Skip redundant setter calls for default values (#11943) --- esphome/components/wifi/__init__.py | 9 ++++++--- esphome/components/wifi/wifi_component.h | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 11bd7798e2..f543d972c9 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -479,11 +479,14 @@ async def to_code(config): cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) if config[CONF_FAST_CONNECT]: cg.add_define("USE_WIFI_FAST_CONNECT") - cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) + # passive_scan defaults to false in C++ - only set if true + if config[CONF_PASSIVE_SCAN]: + cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) - - cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + # 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)) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2fd7fa6cd4..66e2ccf1cb 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -526,7 +526,7 @@ class WiFiComponent : public Component { bool btm_{false}; bool rrm_{false}; #endif - bool enable_on_boot_; + bool enable_on_boot_{true}; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; From 0923bcd2ca1a77ff8777bd71749a403b37ce00d4 Mon Sep 17 00:00:00 2001 From: strange_v Date: Tue, 18 Nov 2025 02:32:17 +0100 Subject: [PATCH 034/102] [mipi_rgb] Fix GUITION-4848S040 colors (#11709) --- esphome/components/mipi_rgb/mipi_rgb.cpp | 15 ++++++++------- esphome/components/mipi_rgb/models/guition.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 00c9c8cbff..080fb08c09 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -350,6 +350,7 @@ void MipiRgb::dump_config() { "\n Width: %u" "\n Height: %u" "\n Rotation: %d degrees" + "\n PCLK Inverted: %s" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -357,18 +358,18 @@ void MipiRgb::dump_config() { "\n VSync Back Porch: %u" "\n VSync Front Porch: %u" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz" + "\n Pixel Clock: %uMHz" "\n Reset Pin: %s" "\n DE Pin: %s" "\n PCLK Pin: %s" "\n HSYNC Pin: %s" "\n VSYNC Pin: %s", - this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, - this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, - this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, - get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), - get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), - get_pin_name(this->vsync_pin_).c_str()); + this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_), + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_), + (unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(), + get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(), + get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str()); if (this->madctl_ & MADCTL_BGR) { this->dump_pins_(8, 13, "Blue", 0); diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py index da433e686e..915b8beda0 100644 --- a/esphome/components/mipi_rgb/models/guition.py +++ b/esphome/components/mipi_rgb/models/guition.py @@ -11,6 +11,7 @@ st7701s.extend( vsync_pin=17, pclk_pin=21, pclk_frequency="12MHz", + pclk_inverted=False, pixel_mode="18bit", mirror_x=True, mirror_y=True, From 0d6c9623ce38f8aed4692006f52135f7e80d490e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 20:02:16 -0600 Subject: [PATCH 035/102] [dashboard_import] Store package import URL in .rodata instead of RAM (#11951) --- esphome/components/dashboard_import/dashboard_import.cpp | 6 +++--- esphome/components/dashboard_import/dashboard_import.h | 6 ++---- esphome/components/mdns/mdns_component.cpp | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp index c04696fd53..d4a95b81f6 100644 --- a/esphome/components/dashboard_import/dashboard_import.cpp +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -3,10 +3,10 @@ namespace esphome { namespace dashboard_import { -static std::string g_package_import_url; // NOLINT +static const char *g_package_import_url = ""; // NOLINT -const std::string &get_package_import_url() { return g_package_import_url; } -void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } +const char *get_package_import_url() { return g_package_import_url; } +void set_package_import_url(const char *url) { g_package_import_url = url; } } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h index edcda6b803..488bf80a2e 100644 --- a/esphome/components/dashboard_import/dashboard_import.h +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -1,12 +1,10 @@ #pragma once -#include - namespace esphome { namespace dashboard_import { -const std::string &get_package_import_url(); -void set_package_import_url(std::string url); +const char *get_package_import_url(); +void set_package_import_url(const char *url); } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 2c3150ff5d..b66129404e 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -135,8 +135,7 @@ void MDNSComponent::compile_records_(StaticVector Date: Tue, 18 Nov 2025 14:11:49 +1000 Subject: [PATCH 036/102] [build] Don't clear pio cache unless requested (#11966) --- esphome/writer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 8eee445cf1..b866a804b3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -121,7 +121,7 @@ def update_storage_json() -> None: ) else: _LOGGER.info("Core config or version changed, cleaning build files...") - clean_build() + clean_build(clear_pio_cache=False) elif storage_should_update_cmake_cache(old, new): _LOGGER.info("Integrations changed, cleaning cmake cache...") clean_cmake_cache() @@ -301,7 +301,7 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def clean_build(): +def clean_build(clear_pio_cache: bool = True): import shutil # Allow skipping cache cleaning for integration tests @@ -322,6 +322,9 @@ def clean_build(): _LOGGER.info("Deleting %s", dependencies_lock) dependencies_lock.unlink() + if not clear_pio_cache: + return + # Clean PlatformIO cache to resolve CMake compiler detection issues # This helps when toolchain paths change or get corrupted try: From 11d0d4d1288c28aeb03b7bd27c36a857f9b308b4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:27:50 +1000 Subject: [PATCH 037/102] [lvgl] Apply scale to spinbox value (#11946) --- esphome/components/lvgl/widgets/spinbox.py | 5 ++++- tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index ac23ded723..c6f25e9587 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE +from esphome.cpp_generator import MockObj from ..automation import action_to_code from ..defines import ( @@ -114,7 +115,9 @@ class SpinboxType(WidgetType): w.obj, digits, digits - config[CONF_DECIMAL_PLACES] ) if (value := config.get(CONF_VALUE)) is not None: - lv.spinbox_set_value(w.obj, await lv_float.process(value)) + lv.spinbox_set_value( + w.obj, MockObj(await lv_float.process(value)) * w.get_scale() + ) def get_scale(self, config): return 10 ** config[CONF_DECIMAL_PLACES] diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index e42a813b40..eabceff9d9 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -703,7 +703,9 @@ lvgl: on_value: - lvgl.spinbox.update: id: spinbox_id - value: !lambda return x; + value: !lambda |- + static float yyy = 83.0; + return yyy + .8; - button: styles: spin_button id: spin_up From 33983b051bf9a979e929c983ed4661d57ebbed1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 10:51:47 -0600 Subject: [PATCH 038/102] [ld24xx] Use stack allocation for MAC and version formatting (#11961) --- esphome/components/ld2410/ld2410.cpp | 26 ++++++++++++-------------- esphome/components/ld2412/ld2412.cpp | 26 ++++++++++++-------------- esphome/components/ld2450/ld2450.cpp | 26 ++++++++++++-------------- esphome/components/ld24xx/ld24xx.h | 24 +++++++++++++++++++++++- 4 files changed, 59 insertions(+), 43 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 608882565f..391f2024cd 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -13,8 +13,6 @@ namespace esphome { namespace ld2410 { static const char *const TAG = "ld2410"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -181,15 +179,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2410Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2410:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); @@ -448,12 +446,12 @@ bool LD2410Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -506,9 +504,9 @@ bool LD2410Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 5323a9a658..4f2fd7c2bd 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -14,8 +14,6 @@ namespace esphome { namespace ld2412 { static const char *const TAG = "ld2412"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -200,15 +198,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2412Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2412:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", @@ -492,12 +490,12 @@ bool LD2412Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -544,9 +542,9 @@ bool LD2412Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index c9d4da47a4..8e5287aec7 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -17,8 +17,6 @@ namespace esphome { namespace ld2450 { static const char *const TAG = "ld2450"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -192,15 +190,15 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2450:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); @@ -642,12 +640,12 @@ bool LD2450Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -663,9 +661,9 @@ bool LD2450Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld24xx/ld24xx.h b/esphome/components/ld24xx/ld24xx.h index 1cd5e01163..e695b00705 100644 --- a/esphome/components/ld24xx/ld24xx.h +++ b/esphome/components/ld24xx/ld24xx.h @@ -1,11 +1,12 @@ #pragma once #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include +#include #ifdef USE_SENSOR -#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ @@ -39,6 +40,27 @@ namespace esphome { namespace ld24xx { +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +// Helper function to format MAC address with stack allocation +// Returns pointer to UNKNOWN_MAC constant or formatted buffer +// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator) +inline const char *format_mac_str(const uint8_t *mac_address, std::span buffer) { + if (mac_address_is_valid(mac_address)) { + format_mac_addr_upper(mac_address, buffer.data()); + return buffer.data(); + } + return UNKNOWN_MAC; +} + +// Helper function to format firmware version with stack allocation +// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety) +inline void format_version_str(const uint8_t *version, std::span buffer) { + snprintf(buffer.data(), buffer.size(), VERSION_FMT, version[1], version[0], version[5], version[4], version[3], + version[2]); +} + #ifdef USE_SENSOR // Helper class to store a sensor with a deduplicator & publish state only when the value changes template class SensorWithDedup { From c59af222170518a7b85154dba9b26c53a8db0d33 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:40:31 -0500 Subject: [PATCH 039/102] [esp32] Fix Arduino build on some ESP32 S2 boards (#11972) --- esphome/components/esp32/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0f85e585f7..6f577d2926 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -931,6 +931,12 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) + # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency + if get_esp32_variant() == VARIANT_ESP32S2: + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1") + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-Wno-nonnull-compare") add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) From 1888f5ffd582e3afdbfdff8faf9efbac8202ada4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 12:16:18 -0600 Subject: [PATCH 040/102] [scheduler] Add defensive nullptr checks and explicit locking requirements (#11974) --- esphome/core/scheduler.cpp | 14 ++++++++------ esphome/core/scheduler.h | 35 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d2e0f0dab4..09d50ee7c8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); @@ -556,7 +556,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); + total_cancelled += + this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -565,19 +566,20 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // (removing the last element doesn't break heap structure) if (!this->items_.empty()) { auto &last_item = this->items_.back(); - if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) { this->recycle_item_(std::move(this->items_.back())); this->items_.pop_back(); total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + size_t heap_cancelled = + this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry); total_cancelled += heap_cancelled; this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index fd16840240..bea1503df0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -243,8 +243,18 @@ class Scheduler { } // Helper function to check if item matches criteria for cancellation - inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + // IMPORTANT: Must be called with scheduler lock held + inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, + const char *name_cstr, 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 + // functions should be safe regardless of caller behavior. + // 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)) { return false; @@ -304,8 +314,8 @@ 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_ - // and has_cancelled_timeout_in_container_ in scheduler.h) + // 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_++; @@ -393,10 +403,10 @@ class Scheduler { // Helper to mark matching items in a container as removed // Returns the number of items marked for removal - // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must be called with scheduler lock held template - size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry) { + size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, + 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) @@ -405,7 +415,7 @@ class Scheduler { // the vector can still contain nullptr items from the processing loop. This check prevents crashes. if (!item) continue; - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) this->set_item_removed_(item.get(), true); count++; @@ -415,9 +425,10 @@ class Scheduler { } // Template helper to check if any item in a container matches our criteria + // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, - bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, + const char *name_cstr, 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 @@ -426,8 +437,8 @@ class Scheduler { if (!item) continue; if (is_item_removed_(item.get()) && - this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } From fe2befcec2c50704afb816f5434da1871393522d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:18:09 -0500 Subject: [PATCH 041/102] [bme68x] Print error when no sensors are configured (#11976) --- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index f5dcfd65a1..91383c8d45 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -70,6 +70,9 @@ void BME68xBSEC2Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, this->bme68x_status_); + if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) { + ESP_LOGE(TAG, "No sensors, add at least one sensor to the config"); + } } if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { From 72e4b16a5b58d0a2f542d619daad19873f2e5020 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:29:40 -0500 Subject: [PATCH 042/102] [sfa30] Fix negative temperature values (#11973) --- esphome/components/sfa30/sfa30.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index 99709d5fbb..bbe3bcd7d2 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -73,17 +73,17 @@ void SFA30Component::update() { } if (this->formaldehyde_sensor_ != nullptr) { - const float formaldehyde = raw_data[0] / 5.0f; + const float formaldehyde = static_cast(raw_data[0]) / 5.0f; this->formaldehyde_sensor_->publish_state(formaldehyde); } if (this->humidity_sensor_ != nullptr) { - const float humidity = raw_data[1] / 100.0f; + const float humidity = static_cast(raw_data[1]) / 100.0f; this->humidity_sensor_->publish_state(humidity); } if (this->temperature_sensor_ != nullptr) { - const float temperature = raw_data[2] / 200.0f; + const float temperature = static_cast(raw_data[2]) / 200.0f; this->temperature_sensor_->publish_state(temperature); } From 81fe5deaa9f376764facd3cd5f26c04e9d4e8aac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 08:12:42 +1300 Subject: [PATCH 043/102] Bump github/codeql-action from 4.31.3 to 4.31.4 (#11977) 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 adb2a9d79f..21fff10c95 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@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.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@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 with: category: "/language:${{matrix.language}}" From 70ed9c7c4db30952faabdd8b7c6373cc28e41419 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 13:17:21 -0600 Subject: [PATCH 044/102] [wifi] Fix captive portal unusable when WiFi credentials are wrong (#11965) --- esphome/components/captive_portal/__init__.py | 10 +++ .../captive_portal/captive_portal.cpp | 10 ++- .../captive_portal/captive_portal.h | 4 ++ esphome/components/esp32_improv/__init__.py | 6 +- .../esp32_improv/esp32_improv_component.cpp | 1 + .../esp32_improv/esp32_improv_component.h | 1 + .../web_server_idf/web_server_idf.cpp | 15 +++++ .../web_server_idf/web_server_idf.h | 4 ++ esphome/components/wifi/__init__.py | 8 ++- esphome/components/wifi/wifi_component.cpp | 62 +++++++++++++++---- esphome/components/wifi/wifi_component.h | 5 ++ 11 files changed, 111 insertions(+), 15 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 9bd3ef8a05..25d0a22083 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -72,6 +72,16 @@ def _final_validate(config: ConfigType) -> ConfigType: "Add 'ap:' to your WiFi configuration to enable the captive portal." ) + # 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 + # 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. + from esphome.components import socket + + socket.consume_sockets(4, "captive_portal")(config) + return config diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..459ac557c8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -50,8 +50,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, "Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - wifi::global_wifi_component->save_wifi_sta(ssid, psk); - wifi::global_wifi_component->start_scanning(); + // Defer save to main loop thread to avoid NVS operations from HTTP thread + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); request->redirect(ESPHOME_F("/?save")); } @@ -63,6 +63,12 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); +#ifdef USE_ESP32 + // Enable LRU socket purging to handle captive portal detection probe bursts + // OS captive portal detection makes many simultaneous HTTP requests which can + // exhaust sockets. LRU purging automatically closes oldest idle connections. + this->base_->get_server()->set_lru_purge_enable(true); +#endif } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index f48c286f0c..ae9b9dfba0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,6 +40,10 @@ class CaptivePortal : public AsyncWebHandler, public Component { void end() { this->active_ = false; this->disable_loop(); // Stop processing DNS requests +#ifdef USE_ESP32 + // Disable LRU socket purging now that captive portal is done + this->base_->get_server()->set_lru_purge_enable(false); +#endif this->base_->deinit(); if (this->dns_server_ != nullptr) { this->dns_server_->stop(); diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1a7194da81..2e69d400ca 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -20,6 +20,10 @@ CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +# Default WiFi timeout - aligned with WiFi component ap_timeout +# Allows sufficient time to try all BSSIDs before starting provisioning mode +DEFAULT_WIFI_TIMEOUT = "90s" + improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") @@ -59,7 +63,7 @@ CONFIG_SCHEMA = ( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_WIFI_TIMEOUT, default="1min" + CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( { diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 398b1d4251..0ad54bbb15 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -127,6 +127,7 @@ void ESP32ImprovComponent::loop() { // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; // Clear flag after starting ESP_LOGD(TAG, "Service started!"); } } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 989552ea56..8f4cfd7958 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -45,6 +45,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void start(); void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK void add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ce91569de2..f5a66f6bd9 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -94,6 +94,18 @@ void AsyncWebServer::end() { } } +void AsyncWebServer::set_lru_purge_enable(bool enable) { + if (this->lru_purge_enable_ == enable) { + return; // No change needed + } + this->lru_purge_enable_ = enable; + // If server is already running, restart it with new config + if (this->server_) { + this->end(); + this->begin(); + } +} + void AsyncWebServer::begin() { if (this->server_) { this->end(); @@ -101,6 +113,8 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) + config.lru_purge_enable = this->lru_purge_enable_; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -242,6 +256,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_set_hdr(*this, "Connection", "close"); httpd_resp_send(*this, nullptr, 0); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 5ec6fec009..b9f690b462 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -199,9 +199,13 @@ class AsyncWebServer { return *handler; } + void set_lru_purge_enable(bool enable); + httpd_handle_t get_server() { return this->server_; } + protected: uint16_t port_{}; httpd_handle_t server_{}; + bool lru_purge_enable_{false}; static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index f543d972c9..2b21478f30 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -69,6 +69,12 @@ CONF_MIN_AUTH_MODE = "min_auth_mode" # Limited to 127 because selected_sta_index_ is int8_t in C++ MAX_WIFI_NETWORKS = 127 +# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection +# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only +# get best-effort connection attempts. Longer timeout ensures we exhaust all options +# before falling back to AP mode. Aligned with improv wifi_timeout default. +DEFAULT_AP_TIMEOUT = "90s" + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -177,7 +183,7 @@ CONF_AP_TIMEOUT = "ap_timeout" WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( { cv.Optional( - CONF_AP_TIMEOUT, default="1min" + CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT ): cv.positive_time_period_milliseconds, } ) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51a5a47323..30340601fb 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -199,7 +199,12 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; /// Cooldown duration in milliseconds after adapter restart or repeated failures /// Allows WiFi hardware to stabilize before next connection attempt -static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000; +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500; + +/// Cooldown duration when fallback AP is active and captive portal may be running +/// Longer interval gives users time to configure WiFi without constant connection attempts +/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { @@ -275,7 +280,9 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { } } - if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + // If we didn't scan this cycle, treat all networks as potentially hidden + // Otherwise, only retry networks that weren't seen in the scan + if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } @@ -417,10 +424,6 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - // Enter cooldown state to allow WiFi hardware to stabilize after restart - // Don't set retry_phase_ or num_retried_ here - state machine handles transitions - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); this->error_from_callback_ = false; } @@ -441,7 +444,16 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // Skip cooldown if new credentials were provided while connecting + if (this->skip_cooldown_next_cycle_) { + this->skip_cooldown_next_cycle_ = false; + this->check_connecting_finished(); + break; + } + // Use longer cooldown when captive portal/improv is active to avoid disrupting user config + bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; + if (now - this->action_started_ > cooldown_duration) { // After cooldown we either restarted the adapter because of // a failure, or something tried to connect over and over // so we entered cooldown. In both cases we call @@ -495,7 +507,8 @@ void WiFiComponent::loop() { #endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && + !esp32_improv::global_improv_component->should_start()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); @@ -605,6 +618,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; + // When new credentials are set (e.g., from improv), skip cooldown to retry immediately + this->skip_cooldown_next_cycle_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -666,6 +681,17 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) + this->connect_soon_(); +} + +void WiFiComponent::connect_soon_() { + // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing + if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { + ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); + this->retry_connect(); + } } void WiFiComponent::start_connecting(const WiFiAP &ap) { @@ -963,6 +989,7 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; + this->did_scan_this_cycle_ = true; if (this->scan_result_.empty()) { ESP_LOGW(TAG, "No networks found"); @@ -1229,9 +1256,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: - // After restart, go back to explicit hidden if we went through it initially, otherwise scan - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; + // After restart, go back to explicit hidden if we went through it initially + if (this->went_through_explicit_hidden_phase_()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // Skip scanning when captive portal/improv is active to avoid disrupting AP + // Even passive scans can cause brief AP disconnections on ESP32 + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + return WiFiRetryPhase::SCAN_CONNECTING; } // Should never reach here @@ -1319,6 +1353,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { this->restart_adapter(); } + // Clear scan flag - we're starting a new retry cycle + this->did_scan_this_cycle_ = false; + // Always enter cooldown after restart (or skip-restart) to allow stabilization + // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 66e2ccf1cb..b3548078bc 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -291,6 +291,7 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. @@ -424,6 +425,8 @@ class WiFiComponent : public Component { return true; } + void connect_soon_(); + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); @@ -529,6 +532,8 @@ class WiFiComponent : public Component { bool enable_on_boot_{true}; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; + bool did_scan_this_cycle_{false}; + bool skip_cooldown_next_cycle_{false}; // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; From 6c8577678c63c01c12734f9c0cbc0966268bbffa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:01:07 -0600 Subject: [PATCH 045/102] [captive_portal] Warn when enabled without WiFi AP configured (#11856) --- esphome/components/captive_portal/__init__.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 99acb76bcf..9bd3ef8a05 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,9 +1,12 @@ +import logging + import esphome.codegen as cg from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_AP, CONF_ID, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -14,6 +17,10 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: @@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + wifi_conf = full_config.get("wifi") + + if wifi_conf is None: + # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway + raise cv.Invalid("Captive portal requires the wifi component to be configured") + + if CONF_AP not in wifi_conf: + _LOGGER.warning( + "Captive portal is enabled but no WiFi AP is configured. " + "The captive portal will not be accessible. " + "Add 'ap:' to your WiFi configuration to enable the captive portal." + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) From 3b25fdbc5f458067494a87ddb8f59e4669ae85ad Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:32:08 -0500 Subject: [PATCH 046/102] [core] Add support for setting environment variables (#11953) --- esphome/const.py | 1 + esphome/core/config.py | 15 +++++++++++++++ tests/components/esphome/common.yaml | 3 +++ 3 files changed, 19 insertions(+) diff --git a/esphome/const.py b/esphome/const.py index 2ca0018fdb..8360531bff 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -336,6 +336,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_ENVIRONMENT_VARIABLES = "environment_variables" CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" diff --git a/esphome/core/config.py b/esphome/core/config.py index 763f9ebd9f..0a239c5f5e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, CONF_DEVICES, + CONF_ENVIRONMENT_VARIABLES, CONF_ESPHOME, CONF_FRIENDLY_NAME, CONF_ID, @@ -215,6 +216,11 @@ CONFIG_SCHEMA = cv.All( cv.string_strict: cv.Any([cv.string], cv.string), } ), + cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + { + cv.string_strict: cv.string, + } + ), cv.Optional(CONF_ON_BOOT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), @@ -426,6 +432,12 @@ async def _add_platformio_options(pio_options): cg.add_platformio_option(key, val) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_environment_variables(env_vars: dict[str, str]) -> None: + # Set environment variables for the build process + os.environ.update(env_vars) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): @@ -563,6 +575,9 @@ async def to_code(config: ConfigType) -> None: if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + if config[CONF_ENVIRONMENT_VARIABLES]: + CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES]) + # Process areas all_areas: list[dict[str, str | core.ID]] = [] if CONF_AREA in config: diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index b2d7bccaa5..db75b08b38 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,6 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio + environment_variables: + TEST_ENV_VAR: "test_value" + BUILD_NUMBER: "12345" area: id: testing_area name: Testing Area From e8998a79c71309164c0d4f4ab8cb0cce6c1b25c0 Mon Sep 17 00:00:00 2001 From: strange_v Date: Tue, 18 Nov 2025 02:32:17 +0100 Subject: [PATCH 047/102] [mipi_rgb] Fix GUITION-4848S040 colors (#11709) --- esphome/components/mipi_rgb/mipi_rgb.cpp | 15 ++++++++------- esphome/components/mipi_rgb/models/guition.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 00c9c8cbff..080fb08c09 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -350,6 +350,7 @@ void MipiRgb::dump_config() { "\n Width: %u" "\n Height: %u" "\n Rotation: %d degrees" + "\n PCLK Inverted: %s" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -357,18 +358,18 @@ void MipiRgb::dump_config() { "\n VSync Back Porch: %u" "\n VSync Front Porch: %u" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz" + "\n Pixel Clock: %uMHz" "\n Reset Pin: %s" "\n DE Pin: %s" "\n PCLK Pin: %s" "\n HSYNC Pin: %s" "\n VSYNC Pin: %s", - this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, - this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, - this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, - get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), - get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), - get_pin_name(this->vsync_pin_).c_str()); + this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_), + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_), + (unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(), + get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(), + get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str()); if (this->madctl_ & MADCTL_BGR) { this->dump_pins_(8, 13, "Blue", 0); diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py index da433e686e..915b8beda0 100644 --- a/esphome/components/mipi_rgb/models/guition.py +++ b/esphome/components/mipi_rgb/models/guition.py @@ -11,6 +11,7 @@ st7701s.extend( vsync_pin=17, pclk_pin=21, pclk_frequency="12MHz", + pclk_inverted=False, pixel_mode="18bit", mirror_x=True, mirror_y=True, From 70aa94b8a40845b23f573d425845e11376f24fe3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:27:50 +1000 Subject: [PATCH 048/102] [lvgl] Apply scale to spinbox value (#11946) --- esphome/components/lvgl/widgets/spinbox.py | 5 ++++- tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index ac23ded723..c6f25e9587 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE +from esphome.cpp_generator import MockObj from ..automation import action_to_code from ..defines import ( @@ -114,7 +115,9 @@ class SpinboxType(WidgetType): w.obj, digits, digits - config[CONF_DECIMAL_PLACES] ) if (value := config.get(CONF_VALUE)) is not None: - lv.spinbox_set_value(w.obj, await lv_float.process(value)) + lv.spinbox_set_value( + w.obj, MockObj(await lv_float.process(value)) * w.get_scale() + ) def get_scale(self, config): return 10 ** config[CONF_DECIMAL_PLACES] diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d7c342b16e..fba860a407 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -703,7 +703,9 @@ lvgl: on_value: - lvgl.spinbox.update: id: spinbox_id - value: !lambda return x; + value: !lambda |- + static float yyy = 83.0; + return yyy + .8; - button: styles: spin_button id: spin_up From 93215f17375679b1c847b2a359bfe19533a9dff0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:40:31 -0500 Subject: [PATCH 049/102] [esp32] Fix Arduino build on some ESP32 S2 boards (#11972) --- esphome/components/esp32/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0f85e585f7..6f577d2926 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -931,6 +931,12 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) + # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency + if get_esp32_variant() == VARIANT_ESP32S2: + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1") + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-Wno-nonnull-compare") add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) From 6db73df649ce1d63c66fd1ead771a31bd5c5f912 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 12:16:18 -0600 Subject: [PATCH 050/102] [scheduler] Add defensive nullptr checks and explicit locking requirements (#11974) --- esphome/core/scheduler.cpp | 14 ++++++++------ esphome/core/scheduler.h | 35 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d2e0f0dab4..09d50ee7c8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); @@ -556,7 +556,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); + total_cancelled += + this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -565,19 +566,20 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // (removing the last element doesn't break heap structure) if (!this->items_.empty()) { auto &last_item = this->items_.back(); - if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) { this->recycle_item_(std::move(this->items_.back())); this->items_.pop_back(); total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + size_t heap_cancelled = + this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry); total_cancelled += heap_cancelled; this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index fd16840240..bea1503df0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -243,8 +243,18 @@ class Scheduler { } // Helper function to check if item matches criteria for cancellation - inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + // IMPORTANT: Must be called with scheduler lock held + inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, + const char *name_cstr, 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 + // functions should be safe regardless of caller behavior. + // 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)) { return false; @@ -304,8 +314,8 @@ 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_ - // and has_cancelled_timeout_in_container_ in scheduler.h) + // 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_++; @@ -393,10 +403,10 @@ class Scheduler { // Helper to mark matching items in a container as removed // Returns the number of items marked for removal - // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must be called with scheduler lock held template - size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry) { + size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, + 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) @@ -405,7 +415,7 @@ class Scheduler { // the vector can still contain nullptr items from the processing loop. This check prevents crashes. if (!item) continue; - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) this->set_item_removed_(item.get(), true); count++; @@ -415,9 +425,10 @@ class Scheduler { } // Template helper to check if any item in a container matches our criteria + // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, - bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, + const char *name_cstr, 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 @@ -426,8 +437,8 @@ class Scheduler { if (!item) continue; if (is_item_removed_(item.get()) && - this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } From f18bc626909d68efde5156f59f85a817bc866971 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:29:40 -0500 Subject: [PATCH 051/102] [sfa30] Fix negative temperature values (#11973) --- esphome/components/sfa30/sfa30.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index 99709d5fbb..bbe3bcd7d2 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -73,17 +73,17 @@ void SFA30Component::update() { } if (this->formaldehyde_sensor_ != nullptr) { - const float formaldehyde = raw_data[0] / 5.0f; + const float formaldehyde = static_cast(raw_data[0]) / 5.0f; this->formaldehyde_sensor_->publish_state(formaldehyde); } if (this->humidity_sensor_ != nullptr) { - const float humidity = raw_data[1] / 100.0f; + const float humidity = static_cast(raw_data[1]) / 100.0f; this->humidity_sensor_->publish_state(humidity); } if (this->temperature_sensor_ != nullptr) { - const float temperature = raw_data[2] / 200.0f; + const float temperature = static_cast(raw_data[2]) / 200.0f; this->temperature_sensor_->publish_state(temperature); } From f436f6ee2e3de361cdb5441aa0accf60986abb69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 13:17:21 -0600 Subject: [PATCH 052/102] [wifi] Fix captive portal unusable when WiFi credentials are wrong (#11965) --- esphome/components/captive_portal/__init__.py | 10 +++ .../captive_portal/captive_portal.cpp | 10 ++- .../captive_portal/captive_portal.h | 4 ++ esphome/components/esp32_improv/__init__.py | 6 +- .../esp32_improv/esp32_improv_component.cpp | 1 + .../esp32_improv/esp32_improv_component.h | 1 + .../web_server_idf/web_server_idf.cpp | 15 +++++ .../web_server_idf/web_server_idf.h | 4 ++ esphome/components/wifi/__init__.py | 8 ++- esphome/components/wifi/wifi_component.cpp | 62 +++++++++++++++---- esphome/components/wifi/wifi_component.h | 5 ++ 11 files changed, 111 insertions(+), 15 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 9bd3ef8a05..25d0a22083 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -72,6 +72,16 @@ def _final_validate(config: ConfigType) -> ConfigType: "Add 'ap:' to your WiFi configuration to enable the captive portal." ) + # 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 + # 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. + from esphome.components import socket + + socket.consume_sockets(4, "captive_portal")(config) + return config diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..459ac557c8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -50,8 +50,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, "Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - wifi::global_wifi_component->save_wifi_sta(ssid, psk); - wifi::global_wifi_component->start_scanning(); + // Defer save to main loop thread to avoid NVS operations from HTTP thread + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); request->redirect(ESPHOME_F("/?save")); } @@ -63,6 +63,12 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); +#ifdef USE_ESP32 + // Enable LRU socket purging to handle captive portal detection probe bursts + // OS captive portal detection makes many simultaneous HTTP requests which can + // exhaust sockets. LRU purging automatically closes oldest idle connections. + this->base_->get_server()->set_lru_purge_enable(true); +#endif } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index f48c286f0c..ae9b9dfba0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,6 +40,10 @@ class CaptivePortal : public AsyncWebHandler, public Component { void end() { this->active_ = false; this->disable_loop(); // Stop processing DNS requests +#ifdef USE_ESP32 + // Disable LRU socket purging now that captive portal is done + this->base_->get_server()->set_lru_purge_enable(false); +#endif this->base_->deinit(); if (this->dns_server_ != nullptr) { this->dns_server_->stop(); diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1a7194da81..2e69d400ca 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -20,6 +20,10 @@ CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +# Default WiFi timeout - aligned with WiFi component ap_timeout +# Allows sufficient time to try all BSSIDs before starting provisioning mode +DEFAULT_WIFI_TIMEOUT = "90s" + improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") @@ -59,7 +63,7 @@ CONFIG_SCHEMA = ( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_WIFI_TIMEOUT, default="1min" + CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( { diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 398b1d4251..0ad54bbb15 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -127,6 +127,7 @@ void ESP32ImprovComponent::loop() { // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; // Clear flag after starting ESP_LOGD(TAG, "Service started!"); } } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 989552ea56..8f4cfd7958 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -45,6 +45,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void start(); void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK void add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ce91569de2..f5a66f6bd9 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -94,6 +94,18 @@ void AsyncWebServer::end() { } } +void AsyncWebServer::set_lru_purge_enable(bool enable) { + if (this->lru_purge_enable_ == enable) { + return; // No change needed + } + this->lru_purge_enable_ = enable; + // If server is already running, restart it with new config + if (this->server_) { + this->end(); + this->begin(); + } +} + void AsyncWebServer::begin() { if (this->server_) { this->end(); @@ -101,6 +113,8 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) + config.lru_purge_enable = this->lru_purge_enable_; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -242,6 +256,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_set_hdr(*this, "Connection", "close"); httpd_resp_send(*this, nullptr, 0); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 5ec6fec009..b9f690b462 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -199,9 +199,13 @@ class AsyncWebServer { return *handler; } + void set_lru_purge_enable(bool enable); + httpd_handle_t get_server() { return this->server_; } + protected: uint16_t port_{}; httpd_handle_t server_{}; + bool lru_purge_enable_{false}; static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 11bd7798e2..5b3b30e0e9 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -69,6 +69,12 @@ CONF_MIN_AUTH_MODE = "min_auth_mode" # Limited to 127 because selected_sta_index_ is int8_t in C++ MAX_WIFI_NETWORKS = 127 +# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection +# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only +# get best-effort connection attempts. Longer timeout ensures we exhaust all options +# before falling back to AP mode. Aligned with improv wifi_timeout default. +DEFAULT_AP_TIMEOUT = "90s" + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -177,7 +183,7 @@ CONF_AP_TIMEOUT = "ap_timeout" WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( { cv.Optional( - CONF_AP_TIMEOUT, default="1min" + CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT ): cv.positive_time_period_milliseconds, } ) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 880928c3e3..e31d7bbf32 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -199,7 +199,12 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; /// Cooldown duration in milliseconds after adapter restart or repeated failures /// Allows WiFi hardware to stabilize before next connection attempt -static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000; +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500; + +/// Cooldown duration when fallback AP is active and captive portal may be running +/// Longer interval gives users time to configure WiFi without constant connection attempts +/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { @@ -275,7 +280,9 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { } } - if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + // If we didn't scan this cycle, treat all networks as potentially hidden + // Otherwise, only retry networks that weren't seen in the scan + if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } @@ -417,10 +424,6 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - // Enter cooldown state to allow WiFi hardware to stabilize after restart - // Don't set retry_phase_ or num_retried_ here - state machine handles transitions - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); this->error_from_callback_ = false; } @@ -441,7 +444,16 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // Skip cooldown if new credentials were provided while connecting + if (this->skip_cooldown_next_cycle_) { + this->skip_cooldown_next_cycle_ = false; + this->check_connecting_finished(); + break; + } + // Use longer cooldown when captive portal/improv is active to avoid disrupting user config + bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; + if (now - this->action_started_ > cooldown_duration) { // After cooldown we either restarted the adapter because of // a failure, or something tried to connect over and over // so we entered cooldown. In both cases we call @@ -495,7 +507,8 @@ void WiFiComponent::loop() { #endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && + !esp32_improv::global_improv_component->should_start()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); @@ -605,6 +618,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; + // When new credentials are set (e.g., from improv), skip cooldown to retry immediately + this->skip_cooldown_next_cycle_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -666,6 +681,17 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) + this->connect_soon_(); +} + +void WiFiComponent::connect_soon_() { + // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing + if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { + ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); + this->retry_connect(); + } } void WiFiComponent::start_connecting(const WiFiAP &ap) { @@ -961,6 +987,7 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; + this->did_scan_this_cycle_ = true; if (this->scan_result_.empty()) { ESP_LOGW(TAG, "No networks found"); @@ -1227,9 +1254,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: - // After restart, go back to explicit hidden if we went through it initially, otherwise scan - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; + // After restart, go back to explicit hidden if we went through it initially + if (this->went_through_explicit_hidden_phase_()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // Skip scanning when captive portal/improv is active to avoid disrupting AP + // Even passive scans can cause brief AP disconnections on ESP32 + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + return WiFiRetryPhase::SCAN_CONNECTING; } // Should never reach here @@ -1317,6 +1351,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { this->restart_adapter(); } + // Clear scan flag - we're starting a new retry cycle + this->did_scan_this_cycle_ = false; + // Always enter cooldown after restart (or skip-restart) to allow stabilization + // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 5023cf3428..2e0a9816c6 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -291,6 +291,7 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. @@ -424,6 +425,8 @@ class WiFiComponent : public Component { return true; } + void connect_soon_(); + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); @@ -529,6 +532,8 @@ class WiFiComponent : public Component { bool enable_on_boot_; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; + bool did_scan_this_cycle_{false}; + bool skip_cooldown_next_cycle_{false}; // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; From 2681a14d05cbc1df1f434b2d5270b5b326e54140 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:17:33 +1300 Subject: [PATCH 053/102] Bump version to 2025.11.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 04046d9ce6..c7b2187964 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 = 2025.11.0b3 +PROJECT_NUMBER = 2025.11.0b4 # 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 8360531bff..7a3a79f270 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b3" +__version__ = "2025.11.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 29374837c68d8643c61b35803130b829eca84268 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 17:06:34 -0600 Subject: [PATCH 054/102] [wifi, captive_portal, web_server, wifi_info] Use stack allocation for MAC address formatting (#11963) --- esphome/components/captive_portal/captive_portal.cpp | 6 ++++-- esphome/components/web_server/web_server.cpp | 4 ++-- esphome/components/wifi/wifi_component.cpp | 6 ++++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 5 ++++- esphome/core/helpers.cpp | 12 +++++++++--- esphome/core/helpers.h | 5 +++++ 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 459ac557c8..4eb00835b1 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -13,14 +13,16 @@ static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json")); stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); + char mac_s[18]; + const char *mac_str = get_mac_address_pretty_into_buffer(mac_s); #ifdef USE_ESP8266 stream->print(ESPHOME_F("{\"mac\":\"")); - stream->print(get_mac_address_pretty().c_str()); + stream->print(mac_str); stream->print(ESPHOME_F("\",\"name\":\"")); stream->print(App.get_name().c_str()); stream->print(ESPHOME_F("\",\"aps\":[{}")); #else - stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); + stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str()); #endif for (auto &scan : wifi::global_wifi_component->get_scan_result()) { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5a8128ba43..cc51463fe7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -359,8 +359,8 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse(200, ""); response->addHeader(HEADER_CORS_ALLOW_PNA, "true"); response->addHeader(HEADER_PNA_NAME, App.get_name().c_str()); - std::string mac = get_mac_address_pretty(); - response->addHeader(HEADER_PNA_ID, mac.c_str()); + char mac_s[18]; + response->addHeader(HEADER_PNA_ID, get_mac_address_pretty_into_buffer(mac_s)); request->send(response); } #endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 30340601fb..6f698bc2a8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -341,10 +341,11 @@ void WiFiComponent::setup() { } void WiFiComponent::start() { + char mac_s[18]; ESP_LOGCONFIG(TAG, "Starting\n" " Local MAC: %s", - get_mac_address_pretty().c_str()); + get_mac_address_pretty_into_buffer(mac_s)); this->last_connected_ = millis(); uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; @@ -826,7 +827,8 @@ void WiFiComponent::print_connect_params_() { char bssid_s[18]; format_mac_addr_upper(bssid.data(), bssid_s); - ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + char mac_s[18]; + ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty_into_buffer(mac_s)); if (this->is_disabled()) { ESP_LOGCONFIG(TAG, " Disabled"); return; diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 04889d6bb3..0814336c43 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -126,7 +126,10 @@ class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { public: - void setup() override { this->publish_state(get_mac_address_pretty()); } + void setup() override { + char mac_s[18]; + this->publish_state(get_mac_address_pretty_into_buffer(mac_s)); + } void dump_config() override; }; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 568acb9f1b..50af71649c 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -638,9 +638,8 @@ std::string get_mac_address() { } std::string get_mac_address_pretty() { - uint8_t mac[6]; - get_mac_address_raw(mac); - return format_mac_address_pretty(mac); + char buf[18]; + return std::string(get_mac_address_pretty_into_buffer(buf)); } void get_mac_address_into_buffer(std::span buf) { @@ -649,6 +648,13 @@ void get_mac_address_into_buffer(std::span buf) { format_mac_addr_lower_no_sep(mac, buf.data()); } +const char *get_mac_address_pretty_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + #ifndef USE_ESP32 bool has_custom_mac_address() { return false; } #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 16eab8b8f6..d8c1f4647e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1052,6 +1052,11 @@ std::string get_mac_address_pretty(); /// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). void get_mac_address_into_buffer(std::span buf); +/// Get the device MAC address into the given buffer, in colon-separated uppercase hex notation. +/// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). +/// Returns pointer to the buffer for convenience. +const char *get_mac_address_pretty_into_buffer(std::span buf); + #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). void set_mac_address(uint8_t *mac); From 45c994e4de71c99bc0fb8051c8ca9ea484348ee8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 21:56:23 -0600 Subject: [PATCH 055/102] [light] Modernize namespace declarations to C++17 syntax (#11986) --- esphome/components/light/addressable_light.cpp | 6 ++---- esphome/components/light/addressable_light.h | 6 ++---- esphome/components/light/addressable_light_effect.h | 6 ++---- esphome/components/light/addressable_light_wrapper.h | 6 ++---- esphome/components/light/automation.cpp | 6 ++---- esphome/components/light/automation.h | 6 ++---- esphome/components/light/base_light_effects.h | 6 ++---- esphome/components/light/color_mode.h | 6 ++---- esphome/components/light/esp_color_correction.cpp | 6 ++---- esphome/components/light/esp_color_correction.h | 6 ++---- esphome/components/light/esp_color_view.h | 6 ++---- esphome/components/light/esp_hsv_color.cpp | 6 ++---- esphome/components/light/esp_hsv_color.h | 6 ++---- esphome/components/light/esp_range_view.cpp | 6 ++---- esphome/components/light/esp_range_view.h | 6 ++---- esphome/components/light/light_call.cpp | 6 ++---- esphome/components/light/light_color_values.h | 6 ++---- esphome/components/light/light_effect.cpp | 6 ++---- esphome/components/light/light_effect.h | 6 ++---- esphome/components/light/light_json_schema.cpp | 6 ++---- esphome/components/light/light_json_schema.h | 6 ++---- esphome/components/light/light_output.cpp | 6 ++---- esphome/components/light/light_output.h | 6 ++---- esphome/components/light/light_state.cpp | 6 ++---- esphome/components/light/light_state.h | 6 ++---- esphome/components/light/light_transformer.h | 6 ++---- esphome/components/light/transformers.h | 6 ++---- 27 files changed, 54 insertions(+), 108 deletions(-) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 5cbdcb0e86..2f6ffc9a38 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -1,8 +1,7 @@ #include "addressable_light.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.addressable"; @@ -112,5 +111,4 @@ optional AddressableLightTransformer::apply() { return {}; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 393cc679bc..2e4b984ce4 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -14,8 +14,7 @@ #include "esphome/components/power_supply/power_supply.h" #endif -namespace esphome { -namespace light { +namespace esphome::light { /// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). Color color_from_light_color_values(LightColorValues val); @@ -116,5 +115,4 @@ class AddressableLightTransformer : public LightTransformer { Color target_color_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0847db3770..a85ea4661d 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -7,8 +7,7 @@ #include "esphome/components/light/light_state.h" #include "esphome/components/light/addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static int16_t sin16_c(uint16_t theta) { static const uint16_t BASE[] = {0, 6393, 12539, 18204, 23170, 27245, 30273, 32137}; @@ -371,5 +370,4 @@ class AddressableFlickerEffect : public AddressableLightEffect { uint8_t intensity_{13}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index d358502430..8665e62a79 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { class AddressableLightWrapper : public light::AddressableLight { public: @@ -123,5 +122,4 @@ class AddressableLightWrapper : public light::AddressableLight { ColorMode color_mode_{ColorMode::UNKNOWN}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp index 8c1785f061..ddac2f9341 100644 --- a/esphome/components/light/automation.cpp +++ b/esphome/components/light/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.automation"; @@ -11,5 +10,4 @@ void addressableset_warn_about_scale(const char *field) { field); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 8899db8bba..9893c15e0c 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -4,8 +4,7 @@ #include "light_state.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { enum class LimitMode { CLAMP, DO_NOTHING }; @@ -216,5 +215,4 @@ template class AddressableSet : public Action { } }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 515afc5c59..2eeae574e7 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "light_effect.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static float random_cubic_float() { const float r = random_float() * 2.0f - 1.0f; @@ -235,5 +234,4 @@ class FlickerLightEffect : public LightEffect { float alpha_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index aa3448c145..0750ae250d 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -3,8 +3,7 @@ #include #include "esphome/core/finite_set_mask.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. enum class ColorCapability : uint8_t { @@ -210,5 +209,4 @@ inline bool has_capability(const ColorModeMask &mask, ColorCapability capability return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index e5e68264cc..1b511a94b2 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -2,8 +2,7 @@ #include "light_color_values.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { void ESPColorCorrection::calculate_gamma_table(float gamma) { for (uint16_t i = 0; i < 256; i++) { @@ -23,5 +22,4 @@ void ESPColorCorrection::calculate_gamma_table(float gamma) { } } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 14c065058c..d275e045b7 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -2,8 +2,7 @@ #include "esphome/core/color.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorCorrection { public: @@ -73,5 +72,4 @@ class ESPColorCorrection { uint8_t local_brightness_{255}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_view.h b/esphome/components/light/esp_color_view.h index 35117e7dd8..440a23e9c9 100644 --- a/esphome/components/light/esp_color_view.h +++ b/esphome/components/light/esp_color_view.h @@ -4,8 +4,7 @@ #include "esp_hsv_color.h" #include "esp_color_correction.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorSettable { public: @@ -106,5 +105,4 @@ class ESPColorView : public ESPColorSettable { const ESPColorCorrection *color_correction_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.cpp b/esphome/components/light/esp_hsv_color.cpp index 450c2e11ce..07205ea6d0 100644 --- a/esphome/components/light/esp_hsv_color.cpp +++ b/esphome/components/light/esp_hsv_color.cpp @@ -1,7 +1,6 @@ #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { Color ESPHSVColor::to_rgb() const { // based on FastLED's hsv rainbow to rgb @@ -70,5 +69,4 @@ Color ESPHSVColor::to_rgb() const { return rgb; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h index cdde91c71c..4b54039258 100644 --- a/esphome/components/light/esp_hsv_color.h +++ b/esphome/components/light/esp_hsv_color.h @@ -3,8 +3,7 @@ #include "esphome/core/color.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace light { +namespace esphome::light { struct ESPHSVColor { union { @@ -32,5 +31,4 @@ struct ESPHSVColor { Color to_rgb() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp index e1f0a507bd..58d552031a 100644 --- a/esphome/components/light/esp_range_view.cpp +++ b/esphome/components/light/esp_range_view.cpp @@ -1,8 +1,7 @@ #include "esp_range_view.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t HOT interpret_index(int32_t index, int32_t size) { if (index < 0) @@ -92,5 +91,4 @@ ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index 07d18af79f..f5e4ebb83f 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -3,8 +3,7 @@ #include "esp_color_view.h" #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t interpret_index(int32_t index, int32_t size); @@ -76,5 +75,4 @@ class ESPRangeIterator { int32_t i_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b15ff84b97..b3bdb16c73 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -647,5 +646,4 @@ LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) return *this; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 04d7d1e7d8..bedfad2c35 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -4,8 +4,7 @@ #include "color_mode.h" #include -namespace esphome { -namespace light { +namespace esphome::light { inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } @@ -310,5 +309,4 @@ class LightColorValues { ColorMode color_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.cpp b/esphome/components/light/light_effect.cpp index a210b48e5b..81b923f7f9 100644 --- a/esphome/components/light/light_effect.cpp +++ b/esphome/components/light/light_effect.cpp @@ -1,8 +1,7 @@ #include "light_effect.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { uint32_t LightEffect::get_index() const { if (this->state_ == nullptr) { @@ -32,5 +31,4 @@ uint32_t LightEffect::get_index_in_parent_() const { return 0; // Not found } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index d4c2dc3582..aa1f6f7899 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -2,8 +2,7 @@ #include "esphome/core/component.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightState; @@ -55,5 +54,4 @@ class LightEffect { uint32_t get_index_in_parent_() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index e754c453b5..1c9b92f504 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -3,8 +3,7 @@ #ifdef USE_JSON -namespace esphome { -namespace light { +namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema @@ -169,7 +168,6 @@ void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject } } -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h index c92dd7b655..dac81e32e3 100644 --- a/esphome/components/light/light_json_schema.h +++ b/esphome/components/light/light_json_schema.h @@ -8,8 +8,7 @@ #include "light_call.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightJSONSchema { public: @@ -22,7 +21,6 @@ class LightJSONSchema { static void parse_color_json(LightState &state, LightCall &call, JsonObject root); }; -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp index e805a0b694..a86e8e5bf1 100644 --- a/esphome/components/light/light_output.cpp +++ b/esphome/components/light/light_output.cpp @@ -1,12 +1,10 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { std::unique_ptr LightOutput::create_default_transition() { return make_unique(); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 73ba0371cd..c82d270be8 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -5,8 +5,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Interface to write LightStates to hardware. class LightOutput { @@ -29,5 +28,4 @@ class LightOutput { virtual void write_state(LightState *state) = 0; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 4c253ec5a8..36b2af03a5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -5,8 +5,7 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -304,5 +303,4 @@ void LightState::save_remote_values_() { this->rtc_.save(&saved); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index bf63c0ec27..06519cdc14 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -15,8 +15,7 @@ #include #include -namespace esphome { -namespace light { +namespace esphome::light { class LightOutput; @@ -298,5 +297,4 @@ class LightState : public EntityBase, public Component { LightRestoreMode restore_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index a84183c03c..079c2d2ae0 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "light_color_values.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Base class for all light color transformers, such as transitions or flashes. class LightTransformer { @@ -59,5 +58,4 @@ class LightTransformer { LightColorValues target_values_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 71d41a66d3..a26713b723 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -6,8 +6,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightTransitionTransformer : public LightTransformer { public: @@ -118,5 +117,4 @@ class LightFlashTransformer : public LightTransformer { bool begun_lightstate_restore_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light From b3ef05e5e137fd47ec4f781050af9a571f7689b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 22:00:39 -0600 Subject: [PATCH 056/102] [ld24xx] Modernize namespace declarations to C++17 syntax (#11988) --- esphome/components/ld2410/automation.h | 6 ++---- esphome/components/ld2410/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2410/button/factory_reset_button.h | 6 ++---- esphome/components/ld2410/button/query_button.cpp | 6 ++---- esphome/components/ld2410/button/query_button.h | 6 ++---- esphome/components/ld2410/button/restart_button.cpp | 6 ++---- esphome/components/ld2410/button/restart_button.h | 6 ++---- esphome/components/ld2410/ld2410.cpp | 6 ++---- esphome/components/ld2410/ld2410.h | 6 ++---- esphome/components/ld2410/number/gate_threshold_number.cpp | 6 ++---- esphome/components/ld2410/number/gate_threshold_number.h | 6 ++---- esphome/components/ld2410/number/light_threshold_number.cpp | 6 ++---- esphome/components/ld2410/number/light_threshold_number.h | 6 ++---- .../ld2410/number/max_distance_timeout_number.cpp | 6 ++---- .../components/ld2410/number/max_distance_timeout_number.h | 6 ++---- esphome/components/ld2410/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2410/select/baud_rate_select.h | 6 ++---- .../components/ld2410/select/distance_resolution_select.cpp | 6 ++---- .../components/ld2410/select/distance_resolution_select.h | 6 ++---- .../components/ld2410/select/light_out_control_select.cpp | 6 ++---- esphome/components/ld2410/select/light_out_control_select.h | 6 ++---- esphome/components/ld2410/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2410/switch/bluetooth_switch.h | 6 ++---- .../components/ld2410/switch/engineering_mode_switch.cpp | 6 ++---- esphome/components/ld2410/switch/engineering_mode_switch.h | 6 ++---- esphome/components/ld2412/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2412/button/factory_reset_button.h | 6 ++---- esphome/components/ld2412/button/query_button.cpp | 6 ++---- esphome/components/ld2412/button/query_button.h | 6 ++---- esphome/components/ld2412/button/restart_button.cpp | 6 ++---- esphome/components/ld2412/button/restart_button.h | 6 ++---- .../button/start_dynamic_background_correction_button.cpp | 6 ++---- .../button/start_dynamic_background_correction_button.h | 6 ++---- esphome/components/ld2412/ld2412.cpp | 6 ++---- esphome/components/ld2412/ld2412.h | 6 ++---- esphome/components/ld2412/number/gate_threshold_number.cpp | 6 ++---- esphome/components/ld2412/number/gate_threshold_number.h | 6 ++---- esphome/components/ld2412/number/light_threshold_number.cpp | 6 ++---- esphome/components/ld2412/number/light_threshold_number.h | 6 ++---- .../ld2412/number/max_distance_timeout_number.cpp | 6 ++---- .../components/ld2412/number/max_distance_timeout_number.h | 6 ++---- esphome/components/ld2412/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2412/select/baud_rate_select.h | 6 ++---- .../components/ld2412/select/distance_resolution_select.cpp | 6 ++---- .../components/ld2412/select/distance_resolution_select.h | 6 ++---- .../components/ld2412/select/light_out_control_select.cpp | 6 ++---- esphome/components/ld2412/select/light_out_control_select.h | 6 ++---- esphome/components/ld2412/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2412/switch/bluetooth_switch.h | 6 ++---- .../components/ld2412/switch/engineering_mode_switch.cpp | 6 ++---- esphome/components/ld2412/switch/engineering_mode_switch.h | 6 ++---- .../ld2420/binary_sensor/ld2420_binary_sensor.cpp | 6 ++---- .../components/ld2420/binary_sensor/ld2420_binary_sensor.h | 6 ++---- esphome/components/ld2420/button/reconfig_buttons.cpp | 6 ++---- esphome/components/ld2420/button/reconfig_buttons.h | 6 ++---- esphome/components/ld2420/ld2420.cpp | 6 ++---- esphome/components/ld2420/ld2420.h | 6 ++---- esphome/components/ld2420/number/gate_config_number.cpp | 6 ++---- esphome/components/ld2420/number/gate_config_number.h | 6 ++---- esphome/components/ld2420/select/operating_mode_select.cpp | 6 ++---- esphome/components/ld2420/select/operating_mode_select.h | 6 ++---- esphome/components/ld2420/sensor/ld2420_sensor.cpp | 6 ++---- esphome/components/ld2420/sensor/ld2420_sensor.h | 6 ++---- .../components/ld2420/text_sensor/ld2420_text_sensor.cpp | 6 ++---- esphome/components/ld2420/text_sensor/ld2420_text_sensor.h | 6 ++---- esphome/components/ld2450/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2450/button/factory_reset_button.h | 6 ++---- esphome/components/ld2450/button/restart_button.cpp | 6 ++---- esphome/components/ld2450/button/restart_button.h | 6 ++---- esphome/components/ld2450/ld2450.cpp | 6 ++---- esphome/components/ld2450/ld2450.h | 6 ++---- .../components/ld2450/number/presence_timeout_number.cpp | 6 ++---- esphome/components/ld2450/number/presence_timeout_number.h | 6 ++---- esphome/components/ld2450/number/zone_coordinate_number.cpp | 6 ++---- esphome/components/ld2450/number/zone_coordinate_number.h | 6 ++---- esphome/components/ld2450/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2450/select/baud_rate_select.h | 6 ++---- esphome/components/ld2450/select/zone_type_select.cpp | 6 ++---- esphome/components/ld2450/select/zone_type_select.h | 6 ++---- esphome/components/ld2450/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2450/switch/bluetooth_switch.h | 6 ++---- esphome/components/ld2450/switch/multi_target_switch.cpp | 6 ++---- esphome/components/ld2450/switch/multi_target_switch.h | 6 ++---- esphome/components/ld24xx/ld24xx.h | 6 ++---- 84 files changed, 168 insertions(+), 336 deletions(-) diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h index f4f1c197b2..614453b575 100644 --- a/esphome/components/ld2410/automation.h +++ b/esphome/components/ld2410/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { template class BluetoothPasswordSetAction : public Action { public: @@ -18,5 +17,4 @@ template class BluetoothPasswordSetAction : public Action LD2410Component *ld2410_comp_; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.cpp b/esphome/components/ld2410/button/factory_reset_button.cpp index a848b02a9d..0223df7086 100644 --- a/esphome/components/ld2410/button/factory_reset_button.cpp +++ b/esphome/components/ld2410/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.h b/esphome/components/ld2410/button/factory_reset_button.h index 45bf979033..715a8c4056 100644 --- a/esphome/components/ld2410/button/factory_reset_button.h +++ b/esphome/components/ld2410/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/query_button.h b/esphome/components/ld2410/button/query_button.h index c7a47e32d8..7a786901ae 100644 --- a/esphome/components/ld2410/button/query_button.h +++ b/esphome/components/ld2410/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.cpp b/esphome/components/ld2410/button/restart_button.cpp index de0d36c1ef..0d5002d3c6 100644 --- a/esphome/components/ld2410/button/restart_button.cpp +++ b/esphome/components/ld2410/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.h b/esphome/components/ld2410/button/restart_button.h index d00dc05a53..9bf8639a8c 100644 --- a/esphome/components/ld2410/button/restart_button.h +++ b/esphome/components/ld2410/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 391f2024cd..bb2e4e2f4c 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -9,8 +9,7 @@ #include "esphome/core/application.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { static const char *const TAG = "ld2410"; @@ -782,5 +781,4 @@ void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 52cf76b5b6..efe585fb76 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { using namespace ld24xx; @@ -133,5 +132,4 @@ class LD2410Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.cpp b/esphome/components/ld2410/number/gate_threshold_number.cpp index 5d040554d7..65e864a4d7 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.cpp +++ b/esphome/components/ld2410/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(this->gate_); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.h b/esphome/components/ld2410/number/gate_threshold_number.h index 2806ecce63..63491f18d3 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.h +++ b/esphome/components/ld2410/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/light_threshold_number.h b/esphome/components/ld2410/number/light_threshold_number.h index 8f014373c0..3c5e433416 100644 --- a/esphome/components/ld2410/number/light_threshold_number.h +++ b/esphome/components/ld2410/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_max_distances_timeout(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.h b/esphome/components/ld2410/number/max_distance_timeout_number.h index 7d91b4b5fe..35f4cbbfae 100644 --- a/esphome/components/ld2410/number/max_distance_timeout_number.h +++ b/esphome/components/ld2410/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h index 9385c8cf7e..fb1d016b1f 100644 --- a/esphome/components/ld2410/select/baud_rate_select.h +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index 4fc4c5af02..635bf206d3 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h index 1a04f843a6..be2389d36e 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.h +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h index e8cd8f1d6a..608c311af4 100644 --- a/esphome/components/ld2410/select/light_out_control_select.h +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/bluetooth_switch.h b/esphome/components/ld2410/switch/bluetooth_switch.h index 35ae1ec0c9..07804e2292 100644 --- a/esphome/components/ld2410/switch/bluetooth_switch.h +++ b/esphome/components/ld2410/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.cpp b/esphome/components/ld2410/switch/engineering_mode_switch.cpp index 967c87c887..4f2f08b03e 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2410/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.h b/esphome/components/ld2410/switch/engineering_mode_switch.h index e521200cd6..4dd8e16653 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.h +++ b/esphome/components/ld2410/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedparent_->factory_reset(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h index 36a3fffcd5..1ef6b23b80 100644 --- a/esphome/components/ld2412/button/factory_reset_button.h +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h index 595ef6d1e9..373e135802 100644 --- a/esphome/components/ld2412/button/query_button.h +++ b/esphome/components/ld2412/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp index aca0d17841..430f6c998f 100644 --- a/esphome/components/ld2412/button/restart_button.cpp +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h index 5cd582e2a3..80c79f5e7d 100644 --- a/esphome/components/ld2412/button/restart_button.h +++ b/esphome/components/ld2412/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp index 9b37243b82..8ba41a03fb 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -2,10 +2,8 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h index 3af0a8a149..b1f2127896 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class StartDynamicBackgroundCorrectionButton : public button::Button, public Par void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 4f2fd7c2bd..0f6fe62d30 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -10,8 +10,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { static const char *const TAG = "ld2412"; @@ -855,5 +854,4 @@ void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 2bed34bdd8..5dd5e7bcde 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { using namespace ld24xx; @@ -137,5 +136,4 @@ class LD2412Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp index 47f8cd9107..8d12bad115 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.cpp +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h index 61d9945a0a..78c2e54d82 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.h +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h index d8727d3c98..81fd73111c 100644 --- a/esphome/components/ld2412/number/light_threshold_number.h +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_basic_config(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h index af0dcf68c5..c1e947fa19 100644 --- a/esphome/components/ld2412/number/max_distance_timeout_number.h +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h index ffe0329341..4666dd2fa0 100644 --- a/esphome/components/ld2412/select/baud_rate_select.h +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp index 5a6f46a071..95b80f87fb 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.cpp +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h index 842f63b7b1..d3b7fad2f9 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.h +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h index 7a50970d0d..9f86189878 100644 --- a/esphome/components/ld2412/select/light_out_control_select.h +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h index 730d338d87..0c0d1fa550 100644 --- a/esphome/components/ld2412/switch/bluetooth_switch.h +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp index 29ca0c22a8..28b4e5d9e6 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h index aaa404c673..4e75a8a185 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.h +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedpresence_bsensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h index ee06439090..ec52312f92 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor { public: @@ -21,5 +20,4 @@ class LD2420BinarySensor : public LD2420Listener, public Component, binary_senso binary_sensor::BinarySensor *presence_bsensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp index fb8ec2b5a6..1e748e59b8 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.cpp +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -4,13 +4,11 @@ static const char *const TAG = "ld2420.button"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); } void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); } void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); } void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h index 4e9e7a3692..72171ef386 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.h +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420ApplyConfigButton : public button::Button, public Parented { public: @@ -38,5 +37,4 @@ class LD2420FactoryResetButton : public button::Button, public Parented listeners_{}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index a373753770..998eed2188 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -4,8 +4,7 @@ static const char *const TAG = "ld2420.number"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420TimeoutNumber::control(float timeout) { this->publish_state(timeout); @@ -69,5 +68,4 @@ void LD2420StillThresholdNumbers::control(float still_threshold) { } } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h index 459a8026e3..8a8b9c61b1 100644 --- a/esphome/components/ld2420/number/gate_config_number.h +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TimeoutNumber : public number::Number, public Parented { public: @@ -74,5 +73,4 @@ class LD2420MoveThresholdNumbers : public number::Number, public Parentedparent_->set_operating_mode(this->option_at(index)); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h index f59eb33432..c1b8e0b11b 100644 --- a/esphome/components/ld2420/select/operating_mode_select.h +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Select : public Component, public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LD2420Select : public Component, public select::Select, public Parenteddistance_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h index 82730d60e3..4849cfa047 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.h +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { public: @@ -30,5 +29,4 @@ class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { std::vector energy_sensors_ = std::vector(TOTAL_GATES); }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp index f647a36936..f7b016c9d9 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const char *const TAG = "ld2420.text_sensor"; @@ -12,5 +11,4 @@ void LD2420TextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h index 073ddd5d0f..1932eaaf69 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor { public: @@ -20,5 +19,4 @@ class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::T text_sensor::TextSensor *fw_version_text_sensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp index bcac7ada2f..7a8eb5b0dd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.cpp +++ b/esphome/components/ld2450/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/factory_reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h index 8e80347119..392fc67ffd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->restart_and_read_all_info(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/restart_button.h b/esphome/components/ld2450/button/restart_button.h index a44ae5a4d2..9219011f8b 100644 --- a/esphome/components/ld2450/button/restart_button.h +++ b/esphome/components/ld2450/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 8e5287aec7..e69ef31d4f 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -13,8 +13,7 @@ #include #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { static const char *const TAG = "ld2450"; @@ -939,5 +938,4 @@ float LD2450Component::restore_from_flash_() { } #endif -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44b63be444..b94c3cac37 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -31,8 +31,7 @@ #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { using namespace ld24xx; @@ -193,5 +192,4 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.cpp b/esphome/components/ld2450/number/presence_timeout_number.cpp index ecfe71f484..19a1ada0d7 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.cpp +++ b/esphome/components/ld2450/number/presence_timeout_number.cpp @@ -1,12 +1,10 @@ #include "presence_timeout_number.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void PresenceTimeoutNumber::control(float value) { this->publish_state(value); this->parent_->set_presence_timeout(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.h b/esphome/components/ld2450/number/presence_timeout_number.h index b18699792f..09c8afca55 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.h +++ b/esphome/components/ld2450/number/presence_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class PresenceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class PresenceTimeoutNumber : public number::Number, public Parentedparent_->set_zone_coordinate(this->zone_); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/zone_coordinate_number.h b/esphome/components/ld2450/number/zone_coordinate_number.h index 72b83889c4..f5a389d712 100644 --- a/esphome/components/ld2450/number/zone_coordinate_number.h +++ b/esphome/components/ld2450/number/zone_coordinate_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneCoordinateNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class ZoneCoordinateNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index 22810d5f13..cb53118170 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index 1111428c7c..39642b99ad 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -1,12 +1,10 @@ #include "zone_type_select.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void ZoneTypeSelect::control(size_t index) { this->publish_state(index); this->parent_->set_zone_type(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index fc95ec1021..566346eb48 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneTypeSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class ZoneTypeSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.cpp b/esphome/components/ld2450/switch/bluetooth_switch.cpp index fa0d4fb06a..0e19a3e6c6 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.cpp +++ b/esphome/components/ld2450/switch/bluetooth_switch.cpp @@ -1,12 +1,10 @@ #include "bluetooth_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void BluetoothSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.h b/esphome/components/ld2450/switch/bluetooth_switch.h index 3c1c4f755c..3d48a89b57 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.h +++ b/esphome/components/ld2450/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.cpp b/esphome/components/ld2450/switch/multi_target_switch.cpp index a163e29fc5..0b1cb04a68 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.cpp +++ b/esphome/components/ld2450/switch/multi_target_switch.cpp @@ -1,12 +1,10 @@ #include "multi_target_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void MultiTargetSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_multi_target(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.h b/esphome/components/ld2450/switch/multi_target_switch.h index ca6253588d..739f308cce 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.h +++ b/esphome/components/ld2450/switch/multi_target_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class MultiTargetSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class MultiTargetSwitch : public switch_::Switch, public Parented> 8) #define lowbyte(val) (uint8_t)((val) &0xff) -namespace esphome { -namespace ld24xx { +namespace esphome::ld24xx { static const char *const UNKNOWN_MAC = "unknown"; static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; @@ -83,5 +82,4 @@ template class SensorWithDedup { Deduplicator publish_dedup; }; #endif -} // namespace ld24xx -} // namespace esphome +} // namespace esphome::ld24xx From 100ea46f03eb45b10beed6f8d36133f9d2635e70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 23:19:54 -0600 Subject: [PATCH 057/102] [tests] Fix SNTP time ID conflicts in component tests for grouped testing (#11990) --- tests/components/lvgl/common.yaml | 4 ++-- tests/components/lvgl/lvgl-package.yaml | 6 +++--- tests/components/mqtt/common.yaml | 1 + tests/components/uptime/common.yaml | 1 + tests/components/wireguard/common.yaml | 2 ++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index c70dd7568d..652ae7e7a1 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -115,8 +115,8 @@ wifi: password: PASSWORD123 time: - platform: sntp - id: time_id + - platform: sntp + id: sntp_time text: - id: lvgl_text diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index eabceff9d9..5839643638 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -478,19 +478,19 @@ lvgl: id: hello_label text: time_format: "%c" - time: time_id + time: sntp_time - lvgl.label.update: id: hello_label text: time_format: "%c" - time: !lambda return id(time_id).now(); + time: !lambda return id(sntp_time).now(); - lvgl.label.update: id: hello_label text: time_format: "%c" time: !lambda |- ESP_LOGD("label", "multi-line lambda"); - return id(time_id).now(); + return id(sntp_time).now(); on_value: logger.log: format: "state now %d" diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 3f1b83bb01..284ac30337 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -4,6 +4,7 @@ wifi: time: - platform: sntp + id: sntp_time mqtt: broker: "192.168.178.84" diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index 86b764e7ff..279258c670 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -3,6 +3,7 @@ wifi: time: - platform: sntp + id: sntp_time sensor: - platform: uptime diff --git a/tests/components/wireguard/common.yaml b/tests/components/wireguard/common.yaml index cd7ab1075e..342ffa32f6 100644 --- a/tests/components/wireguard/common.yaml +++ b/tests/components/wireguard/common.yaml @@ -4,8 +4,10 @@ wifi: time: - platform: sntp + id: sntp_time wireguard: + time_id: sntp_time address: 172.16.34.100 netmask: 255.255.255.0 # NEVER use the following keys for your VPN -- they are now public! From f2b10ad132dcc4118cd6e0ecbead9da746863b77 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:34 -0500 Subject: [PATCH 058/102] [text_sensor] Fix infinite loop in substitute filter (#11989) Co-authored-by: J. Nick Koston --- esphome/components/text_sensor/filter.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index a242b43b1c..40a37febee 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -66,10 +66,14 @@ SubstituteFilter::SubstituteFilter(const std::initializer_list &su : substitutions_(substitutions) {} optional SubstituteFilter::new_value(std::string value) { - std::size_t pos; for (const auto &sub : this->substitutions_) { - while ((pos = value.find(sub.from)) != std::string::npos) + std::size_t pos = 0; + while ((pos = value.find(sub.from, pos)) != std::string::npos) { value.replace(pos, sub.from.size(), sub.to); + // Advance past the replacement to avoid infinite loop when + // the replacement contains the search pattern (e.g., f -> foo) + pos += sub.to.size(); + } } return value; } From 73bc5252a1467a7530b921eb9be16ab6819f3038 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:57 -0500 Subject: [PATCH 059/102] [wifi] Fix positive RSSI values on 8266 (#11994) --- esphome/components/wifi/wifi_component_esp8266.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a543628e27..274a505db2 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -872,7 +872,13 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } +int8_t WiFiComponent::wifi_rssi() { + if (WiFi.status() != WL_CONNECTED) + return WIFI_RSSI_DISCONNECTED; + int8_t rssi = WiFi.RSSI(); + // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings + return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi; +} int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } From 61cef0a75c4b46df6e0d7761c78dbedfdb8aee72 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:58:47 -0500 Subject: [PATCH 060/102] [api] Fix format warnings in dump (#11999) --- esphome/components/api/api_pb2_dump.cpp | 2 +- script/api_protobuf/api_protobuf.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d9662483bf..127ef44cd8 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3b756095a1..b07a249c8d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -462,7 +462,7 @@ class Int64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -482,7 +482,7 @@ class UInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -522,7 +522,7 @@ class Fixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -1106,7 +1106,7 @@ class SFixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -1150,7 +1150,7 @@ class SInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -2546,7 +2546,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } From 8804bc28154abb1601cd029971cc0abe12a2f49a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 12:58:33 -0600 Subject: [PATCH 061/102] [web_server_idf] Fix pbuf_free crash by moving shutdown before close (#11995) --- .../web_server_idf/web_server_idf.cpp | 41 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 1 + 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f5a66f6bd9..c910ed06c5 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -87,6 +87,29 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_ } } // namespace +void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) { + // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions + // + // The race condition occurs because close() initiates lwIP teardown while + // the TCP/IP thread can still receive packets, causing assertions when + // recv_tcp() sees partially-torn-down state. + // + // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE + // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR) + // to allow the FIN packet to be sent cleanly during close(). + // + // Note: This function may be called with an already-closed socket if the network + // stack closed it. In that case, shutdown() will fail but close() is safe to call. + // + // See: https://github.com/esphome/esphome-webserver/issues/163 + + // Attempt shutdown - ignore errors as socket may already be closed + shutdown(sockfd, SHUT_RD); + + // Always close - safe even if socket is already closed by network stack + close(sockfd); +} + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -115,6 +138,8 @@ void AsyncWebServer::begin() { config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) config.lru_purge_enable = this->lru_purge_enable_; + // Use custom close function that shuts down before closing to prevent lwIP race conditions + config.close_fn = AsyncWebServer::safe_close_with_shutdown; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -505,17 +530,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); int fd = rsp->fd_.exchange(0); // Atomically get and clear fd - - if (fd > 0) { - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); - // Immediately shut down the socket to prevent lwIP from delivering more data - // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack - // tries to deliver queued data after the session is marked as dead - // See: https://github.com/esphome/esphome/issues/11936 - shutdown(fd, SHUT_RDWR); - // Note: We don't close() the socket - httpd owns it and will close it - } - // Session will be cleaned up in the main loop to avoid race conditions + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Mark as dead - will be cleaned up in the main loop + // Note: We don't delete or remove from set here to avoid race conditions + // httpd will call our custom close_fn (safe_close_with_shutdown) which handles + // shutdown() before close() to prevent lwIP race conditions } // helper for allowing only unique entries in the queue diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index b9f690b462..a139e9e4df 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -209,6 +209,7 @@ class AsyncWebServer { static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; + static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif From b02b07ffafaf7dbf0529460638608f69d095a82b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:11:45 +1300 Subject: [PATCH 062/102] [epaper_spi] Add basic `7.3in-Spectra-E6` model (#12001) --- esphome/components/epaper_spi/display.py | 2 +- esphome/components/epaper_spi/models/spectra_e6.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 9ff393b397..182c37ba40 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -102,7 +102,7 @@ def customise_schema(config): """ config = cv.Schema( { - cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"), }, extra=cv.ALLOW_EXTRA, )(config) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py index 9f0b673d69..42a5a7da72 100644 --- a/esphome/components/epaper_spi/models/spectra_e6.py +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -32,11 +32,15 @@ class SpectraE6(EpaperModel): spectra_e6 = SpectraE6("spectra-e6") -spectra_e6.extend( - "Seeed-reTerminal-E1002", +spectra_e6_7p3 = spectra_e6.extend( + "7.3in-Spectra-E6", width=800, height=480, data_rate="20MHz", +) + +spectra_e6_7p3.extend( + "Seeed-reTerminal-E1002", cs_pin=10, dc_pin=11, reset_pin=12, From 13b875c763b990a6d56104bd619f391c09ccceec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 23:19:54 -0600 Subject: [PATCH 063/102] [tests] Fix SNTP time ID conflicts in component tests for grouped testing (#11990) --- tests/components/lvgl/common.yaml | 4 ++-- tests/components/lvgl/lvgl-package.yaml | 6 +++--- tests/components/mqtt/common.yaml | 1 + tests/components/uptime/common.yaml | 1 + tests/components/wireguard/common.yaml | 2 ++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index c70dd7568d..652ae7e7a1 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -115,8 +115,8 @@ wifi: password: PASSWORD123 time: - platform: sntp - id: time_id + - platform: sntp + id: sntp_time text: - id: lvgl_text diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index fba860a407..cb5b6f59b1 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -478,19 +478,19 @@ lvgl: id: hello_label text: time_format: "%c" - time: time_id + time: sntp_time - lvgl.label.update: id: hello_label text: time_format: "%c" - time: !lambda return id(time_id).now(); + time: !lambda return id(sntp_time).now(); - lvgl.label.update: id: hello_label text: time_format: "%c" time: !lambda |- ESP_LOGD("label", "multi-line lambda"); - return id(time_id).now(); + return id(sntp_time).now(); on_value: logger.log: format: "state now %d" diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 3f1b83bb01..284ac30337 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -4,6 +4,7 @@ wifi: time: - platform: sntp + id: sntp_time mqtt: broker: "192.168.178.84" diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index 86b764e7ff..279258c670 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -3,6 +3,7 @@ wifi: time: - platform: sntp + id: sntp_time sensor: - platform: uptime diff --git a/tests/components/wireguard/common.yaml b/tests/components/wireguard/common.yaml index cd7ab1075e..342ffa32f6 100644 --- a/tests/components/wireguard/common.yaml +++ b/tests/components/wireguard/common.yaml @@ -4,8 +4,10 @@ wifi: time: - platform: sntp + id: sntp_time wireguard: + time_id: sntp_time address: 172.16.34.100 netmask: 255.255.255.0 # NEVER use the following keys for your VPN -- they are now public! From 7ef4b4f3d9d6cb8ce5414985ae59538647a3d438 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:34 -0500 Subject: [PATCH 064/102] [text_sensor] Fix infinite loop in substitute filter (#11989) Co-authored-by: J. Nick Koston --- esphome/components/text_sensor/filter.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index a242b43b1c..40a37febee 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -66,10 +66,14 @@ SubstituteFilter::SubstituteFilter(const std::initializer_list &su : substitutions_(substitutions) {} optional SubstituteFilter::new_value(std::string value) { - std::size_t pos; for (const auto &sub : this->substitutions_) { - while ((pos = value.find(sub.from)) != std::string::npos) + std::size_t pos = 0; + while ((pos = value.find(sub.from, pos)) != std::string::npos) { value.replace(pos, sub.from.size(), sub.to); + // Advance past the replacement to avoid infinite loop when + // the replacement contains the search pattern (e.g., f -> foo) + pos += sub.to.size(); + } } return value; } From 0a224f919b3889651db230fbd047cf05f4996cdf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:57 -0500 Subject: [PATCH 065/102] [wifi] Fix positive RSSI values on 8266 (#11994) --- esphome/components/wifi/wifi_component_esp8266.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bdaae5382a..0134fcaed8 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -870,7 +870,13 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } +int8_t WiFiComponent::wifi_rssi() { + if (WiFi.status() != WL_CONNECTED) + return WIFI_RSSI_DISCONNECTED; + int8_t rssi = WiFi.RSSI(); + // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings + return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi; +} int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } From 71dc2d374d18f236c7a5fdae1eee61cbe78c0320 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 12:58:33 -0600 Subject: [PATCH 066/102] [web_server_idf] Fix pbuf_free crash by moving shutdown before close (#11995) --- .../web_server_idf/web_server_idf.cpp | 41 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 1 + 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f5a66f6bd9..c910ed06c5 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -87,6 +87,29 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_ } } // namespace +void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) { + // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions + // + // The race condition occurs because close() initiates lwIP teardown while + // the TCP/IP thread can still receive packets, causing assertions when + // recv_tcp() sees partially-torn-down state. + // + // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE + // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR) + // to allow the FIN packet to be sent cleanly during close(). + // + // Note: This function may be called with an already-closed socket if the network + // stack closed it. In that case, shutdown() will fail but close() is safe to call. + // + // See: https://github.com/esphome/esphome-webserver/issues/163 + + // Attempt shutdown - ignore errors as socket may already be closed + shutdown(sockfd, SHUT_RD); + + // Always close - safe even if socket is already closed by network stack + close(sockfd); +} + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -115,6 +138,8 @@ void AsyncWebServer::begin() { config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) config.lru_purge_enable = this->lru_purge_enable_; + // Use custom close function that shuts down before closing to prevent lwIP race conditions + config.close_fn = AsyncWebServer::safe_close_with_shutdown; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -505,17 +530,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); int fd = rsp->fd_.exchange(0); // Atomically get and clear fd - - if (fd > 0) { - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); - // Immediately shut down the socket to prevent lwIP from delivering more data - // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack - // tries to deliver queued data after the session is marked as dead - // See: https://github.com/esphome/esphome/issues/11936 - shutdown(fd, SHUT_RDWR); - // Note: We don't close() the socket - httpd owns it and will close it - } - // Session will be cleaned up in the main loop to avoid race conditions + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Mark as dead - will be cleaned up in the main loop + // Note: We don't delete or remove from set here to avoid race conditions + // httpd will call our custom close_fn (safe_close_with_shutdown) which handles + // shutdown() before close() to prevent lwIP race conditions } // helper for allowing only unique entries in the queue diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index b9f690b462..a139e9e4df 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -209,6 +209,7 @@ class AsyncWebServer { static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; + static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif From 1157b4aee8b9c6a991854928a05f3169715fd0a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:11:45 +1300 Subject: [PATCH 067/102] [epaper_spi] Add basic `7.3in-Spectra-E6` model (#12001) --- esphome/components/epaper_spi/display.py | 2 +- esphome/components/epaper_spi/models/spectra_e6.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 9ff393b397..182c37ba40 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -102,7 +102,7 @@ def customise_schema(config): """ config = cv.Schema( { - cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"), }, extra=cv.ALLOW_EXTRA, )(config) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py index 9f0b673d69..42a5a7da72 100644 --- a/esphome/components/epaper_spi/models/spectra_e6.py +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -32,11 +32,15 @@ class SpectraE6(EpaperModel): spectra_e6 = SpectraE6("spectra-e6") -spectra_e6.extend( - "Seeed-reTerminal-E1002", +spectra_e6_7p3 = spectra_e6.extend( + "7.3in-Spectra-E6", width=800, height=480, data_rate="20MHz", +) + +spectra_e6_7p3.extend( + "Seeed-reTerminal-E1002", cs_pin=10, dc_pin=11, reset_pin=12, From c75abfb8949ede2182765abcdffab807565fbd05 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:17:03 -0500 Subject: [PATCH 068/102] Bump version to 2025.11.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index c7b2187964..56373c5f69 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 = 2025.11.0b4 +PROJECT_NUMBER = 2025.11.0b5 # 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 7a3a79f270..ddd36f69d7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b4" +__version__ = "2025.11.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2c3417062ae82512f51b85847ef5fe4deba41ebc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:47:40 -0600 Subject: [PATCH 069/102] Bump pyupgrade from 3.21.1 to 3.21.2 (#12002) 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 e238faa77e..7f6d3f8e26 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.14.5 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests From 1e9c7d3c6dbc8e9e4da1367aaacf70c73dc3712b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:02:52 -0500 Subject: [PATCH 070/102] Bump version to 2025.11.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 56373c5f69..1448fd010d 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 = 2025.11.0b5 +PROJECT_NUMBER = 2025.11.0 # 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 ddd36f69d7..3505ad169b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b5" +__version__ = "2025.11.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4398fd84d2ba21a3a4099946d554a6d70cc0e7ad Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:09:22 -0500 Subject: [PATCH 071/102] [graph] Fix legend border (#12000) --- esphome/components/graph/graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 88bb306408..e3b9119108 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -337,7 +337,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of return; /// Plot border - if (this->border_) { + if (legend_->border_) { int w = legend_->width_; int h = legend_->height_; buff->horizontal_line(x_offset, y_offset, w, color); From da25951f6e714fe483f5c382a6188225b62509da Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Thu, 20 Nov 2025 03:01:32 +0000 Subject: [PATCH 072/102] [socket] Fix IPv6 address parsing for BSD sockets (#11996) --- esphome/components/socket/socket.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 1c8e72b8fd..cc9232d21a 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -61,9 +61,18 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri server->sin6_family = AF_INET6; server->sin6_port = htons(port); +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + // Use standard inet_pton for BSD sockets + if (inet_pton(AF_INET6, ip_address.c_str(), &server->sin6_addr) != 1) { + errno = EINVAL; + return 0; + } +#else + // Use LWIP-specific functions ip6_addr_t ip6; inet6_aton(ip_address.c_str(), &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); +#endif return sizeof(sockaddr_in6); } #endif /* USE_NETWORK_IPV6 */ From 83307684a3dcc3e183de98e060c9324206a7a4ab Mon Sep 17 00:00:00 2001 From: B48D81EFCC <111175947+B48D81EFCC@users.noreply.github.com> Date: Thu, 20 Nov 2025 04:58:39 +0100 Subject: [PATCH 073/102] [stts22h] Add support for STTS22H temperature sensor (#11778) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/stts22h/__init__.py | 1 + esphome/components/stts22h/sensor.py | 33 ++++++ esphome/components/stts22h/stts22h.cpp | 101 ++++++++++++++++++ esphome/components/stts22h/stts22h.h | 21 ++++ tests/components/stts22h/common.yaml | 4 + tests/components/stts22h/test.esp32-idf.yaml | 4 + .../components/stts22h/test.esp8266-ard.yaml | 4 + tests/components/stts22h/test.nrf52.yaml | 4 + tests/components/stts22h/test.rp2040-ard.yaml | 4 + 10 files changed, 177 insertions(+) create mode 100644 esphome/components/stts22h/__init__.py create mode 100644 esphome/components/stts22h/sensor.py create mode 100644 esphome/components/stts22h/stts22h.cpp create mode 100644 esphome/components/stts22h/stts22h.h create mode 100644 tests/components/stts22h/common.yaml create mode 100644 tests/components/stts22h/test.esp32-idf.yaml create mode 100644 tests/components/stts22h/test.esp8266-ard.yaml create mode 100644 tests/components/stts22h/test.nrf52.yaml create mode 100644 tests/components/stts22h/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e6970af47c..250fbbd4d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -460,6 +460,7 @@ esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 esphome/components/st7920/* @marsjan155 esphome/components/statsd/* @Links2004 +esphome/components/stts22h/* @B48D81EFCC esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 diff --git a/esphome/components/stts22h/__init__.py b/esphome/components/stts22h/__init__.py new file mode 100644 index 0000000000..a33c0b554b --- /dev/null +++ b/esphome/components/stts22h/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@B48D81EFCC"] diff --git a/esphome/components/stts22h/sensor.py b/esphome/components/stts22h/sensor.py new file mode 100644 index 0000000000..094c233361 --- /dev/null +++ b/esphome/components/stts22h/sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] + +sensor_ns = cg.esphome_ns.namespace("stts22h") +stts22h = sensor_ns.class_( + "STTS22HComponent", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + stts22h, + accuracy_decimals=2, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3C)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/stts22h/stts22h.cpp b/esphome/components/stts22h/stts22h.cpp new file mode 100644 index 0000000000..614dc1da8b --- /dev/null +++ b/esphome/components/stts22h/stts22h.cpp @@ -0,0 +1,101 @@ +#include "esphome/core/log.h" +#include "stts22h.h" + +namespace esphome::stts22h { + +static const char *const TAG = "stts22h"; + +static const uint8_t WHOAMI_REG = 0x01; +static const uint8_t CTRL_REG = 0x04; +static const uint8_t TEMPERATURE_REG = 0x06; + +// CTRL_REG flags +static const uint8_t LOW_ODR_CTRL_ENABLE_FLAG = 0x80; // Flag to enable low ODR mode in CTRL_REG +static const uint8_t FREERUN_CTRL_ENABLE_FLAG = 0x04; // Flag to enable FREERUN mode in CTRL_REG +static const uint8_t ADD_INC_ENABLE_FLAG = 0x08; // Flag to enable ADD_INC (IF_ADD_INC) mode in CTRL_REG + +static const uint8_t WHOAMI_STTS22H_IDENTIFICATION = 0xA0; // ID value of STTS22H in WHOAMI_REG + +static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsius + +void STTS22HComponent::setup() { + // Check if device is a STTS22H + if (!this->is_stts22h_sensor_()) { + this->mark_failed("Device is not a STTS22H sensor"); + return; + } + + this->initialize_sensor_(); +} + +void STTS22HComponent::update() { + if (this->is_failed()) { + return; + } + + this->publish_state(this->read_temperature_()); +} + +void STTS22HComponent::dump_config() { + LOG_SENSOR("", "STTS22H", this); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +float STTS22HComponent::read_temperature_() { + uint8_t temp_reg_value[2]; + if (this->read_register(TEMPERATURE_REG, temp_reg_value, 2) != i2c::NO_ERROR) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + return NAN; + } + + // Combine the two bytes into a single 16-bit signed integer + // The STTS22H temperature data is in two's complement format + int16_t temp_raw_value = static_cast(encode_uint16(temp_reg_value[1], temp_reg_value[0])); + return temp_raw_value * SENSOR_SCALE; // Apply sensor resolution +} + +bool STTS22HComponent::is_stts22h_sensor_() { + uint8_t whoami_value; + if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return false; + } + + if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) { + this->mark_failed("Unexpected WHOAMI identifier. Sensor is not a STTS22H"); + return false; + } + + return true; +} + +void STTS22HComponent::initialize_sensor_() { + // Read current CTRL_REG configuration + uint8_t ctrl_value; + if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } + + // Enable low ODR mode and enable ADD_INC + // Before low ODR mode can be used, + // FREERUN bit must be cleared (see sensor documentation) + ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } + + // Enable LOW ODR mode and ADD_INC + ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } +} + +} // namespace esphome::stts22h diff --git a/esphome/components/stts22h/stts22h.h b/esphome/components/stts22h/stts22h.h new file mode 100644 index 0000000000..442a263e49 --- /dev/null +++ b/esphome/components/stts22h/stts22h.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::stts22h { + +class STTS22HComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + protected: + void initialize_sensor_(); + bool is_stts22h_sensor_(); + float read_temperature_(); +}; + +} // namespace esphome::stts22h diff --git a/tests/components/stts22h/common.yaml b/tests/components/stts22h/common.yaml new file mode 100644 index 0000000000..802afe2065 --- /dev/null +++ b/tests/components/stts22h/common.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: stts22h + name: Temperature + update_interval: 15s diff --git a/tests/components/stts22h/test.esp32-idf.yaml b/tests/components/stts22h/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/stts22h/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/stts22h/test.esp8266-ard.yaml b/tests/components/stts22h/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/stts22h/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/stts22h/test.nrf52.yaml b/tests/components/stts22h/test.nrf52.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/stts22h/test.nrf52.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.rp2040-ard.yaml b/tests/components/stts22h/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/stts22h/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From b346666a5264294afea8ce002cb5cf955b99339b Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Thu, 20 Nov 2025 10:05:22 +0100 Subject: [PATCH 074/102] [st7701s] Add explanatory comment (#12014) --- esphome/components/mipi_rgb/models/st7701s.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py index bfd1c9aa3f..0b0a9548ca 100644 --- a/esphome/components/mipi_rgb/models/st7701s.py +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -24,6 +24,8 @@ class ST7701S(DriverChip): sdir = 0 if transform.get(CONF_MIRROR_X): sdir |= 0x04 + # XFLIP doesn't do anything in the ST7701S, + # it's set in the madctl byte just so it can be reported at runtime by logconfig madctl |= MADCTL_XFLIP sequence.append((SDIR_CMD, sdir)) return madctl From 4825da8e9ca27c1b7d9c88bd094da5a620b53a23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:57:04 -0600 Subject: [PATCH 075/102] [select] Modernize namespace declarations to C++17 syntax (#12007) --- esphome/components/select/automation.h | 6 ++---- esphome/components/select/select.cpp | 6 ++---- esphome/components/select/select.h | 6 ++---- esphome/components/select/select_call.cpp | 6 ++---- esphome/components/select/select_call.h | 6 ++---- esphome/components/select/select_traits.cpp | 6 ++---- esphome/components/select/select_traits.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 3e42eaf98a..768f2621f7 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "select.h" -namespace esphome { -namespace select { +namespace esphome::select { class SelectStateTrigger : public Trigger { public: @@ -63,5 +62,4 @@ template class SelectOperationAction : public Action { Select *select_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 9fe7a52422..3ec413f167 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; @@ -86,5 +85,4 @@ optional Select::at(size_t index) const { const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 7459c9d146..c4d7412d50 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -6,8 +6,7 @@ #include "select_call.h" #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -114,5 +113,4 @@ class Select : public EntityBase { CallbackManager state_callback_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index aa7559e24e..aecfed0d64 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -2,8 +2,7 @@ #include "select.h" #include "esphome/core/log.h" -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; @@ -125,5 +124,4 @@ void SelectCall::perform() { parent->control(idx); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index eae7d3de1d..b31d890ef6 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace select { +namespace esphome::select { class Select; @@ -45,5 +44,4 @@ class SelectCall { bool cycle_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index e5e12bdc7a..ff52c0d85b 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -1,7 +1,6 @@ #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { void SelectTraits::set_options(const std::initializer_list &options) { this->options_ = options; } @@ -14,5 +13,4 @@ void SelectTraits::set_options(const FixedVector &options) { const FixedVector &SelectTraits::get_options() const { return this->options_; } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index ee59a030ad..78a83e5944 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include -namespace esphome { -namespace select { +namespace esphome::select { class SelectTraits { public: @@ -16,5 +15,4 @@ class SelectTraits { FixedVector options_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select From 507147376710e8d5e214d78ab51ece81f7a33339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:57:33 -0600 Subject: [PATCH 076/102] [mdns] Modernize to C++17 nested namespace syntax (#11983) --- esphome/components/mdns/mdns_component.cpp | 6 ++---- esphome/components/mdns/mdns_component.h | 6 ++---- esphome/components/mdns/mdns_esp32.cpp | 6 ++---- esphome/components/mdns/mdns_esp8266.cpp | 6 ++---- esphome/components/mdns/mdns_host.cpp | 6 ++---- esphome/components/mdns/mdns_libretiny.cpp | 6 ++---- esphome/components/mdns/mdns_rp2040.cpp | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index b66129404e..c81defd19f 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -21,8 +21,7 @@ #include "esphome/components/dashboard_import/dashboard_import.h" #endif -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -189,6 +188,5 @@ void MDNSComponent::dump_config() { #endif } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index f4237d5a69..691c45b7df 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { // Helper struct that identifies strings that may be stored in flash storage (similar to LogString) struct MDNSString; @@ -79,6 +78,5 @@ class MDNSComponent : public Component { void compile_records_(StaticVector &services); }; -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index ecdc926cc9..5547a2524b 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -7,8 +7,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -56,7 +55,6 @@ void MDNSComponent::on_shutdown() { delay(40); // Allow the mdns packets announcing service removal to be sent } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif // USE_ESP32 diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 9bbb406070..06f905884c 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -9,8 +9,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -52,7 +51,6 @@ void MDNSComponent::on_shutdown() { delay(10); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index f645d8d068..64b8c8f54b 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { // Host platform doesn't have actual mDNS implementation @@ -15,7 +14,6 @@ void MDNSComponent::setup() { void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index fb2088f719..a049fe2109 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -9,8 +9,7 @@ #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -46,7 +45,6 @@ void MDNSComponent::setup() { void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index a9f5349f14..a102e0b6c3 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -9,8 +9,7 @@ #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -51,7 +50,6 @@ void MDNSComponent::on_shutdown() { delay(40); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif From 24a6ad148c6a6fe130e1ca74859032a2b787df3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:57:49 -0600 Subject: [PATCH 077/102] [lock] Modernize to C++17 nested namespaces (#11982) --- esphome/components/lock/automation.h | 6 ++---- esphome/components/lock/lock.cpp | 6 ++---- esphome/components/lock/lock.h | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 0f596ef5e6..cba2c3fdda 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace lock { +namespace esphome::lock { template class LockAction : public Action { public: @@ -72,5 +71,4 @@ class LockUnlockTrigger : public Trigger<> { } }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 54fefe8745..b8f0fbe011 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace lock { +namespace esphome::lock { static const char *const TAG = "lock"; @@ -108,5 +107,4 @@ LockCall &LockCall::set_state(const std::string &state) { } const optional &LockCall::get_state() const { return this->state_; } -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 9737569921..8a906ef9fc 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -7,8 +7,7 @@ #include "esphome/core/preferences.h" #include -namespace esphome { -namespace lock { +namespace esphome::lock { class Lock; @@ -177,5 +176,4 @@ class Lock : public EntityBase { ESPPreferenceObject rtc_; }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock From a2321edf3c7dcb7b38adbc77816bcf23dcd44595 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:59:16 -0600 Subject: [PATCH 078/102] [network] Fix IPAddress constructor causing comparison failures and garbage output (#12005) --- esphome/components/network/ip_address.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5ec6450cce..3d8b062d0b 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -81,7 +81,12 @@ struct IPAddress { ip_addr_.type = IPADDR_TYPE_V6; } #endif /* LWIP_IPV6 */ - IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); } + IPAddress(esp_ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); +#if LWIP_IPV6 + ip_addr_.type = IPADDR_TYPE_V4; +#endif + } IPAddress(esp_ip_addr_t *other_ip) { #if LWIP_IPV6 memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); From b62053812b79f9fb9acc6965e7060c05951fc681 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 08:06:28 -0600 Subject: [PATCH 079/102] [core] Document threading model rationale in ThreadModel enum (#11979) --- esphome/components/libretiny/__init__.py | 4 ++++ esphome/const.py | 25 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index c63d6d7faa..93b66888da 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -261,6 +261,10 @@ async def component_to_code(config): cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) + # LibreTiny uses MULTI_NO_ATOMICS because platforms like BK7231N (ARM968E-S) lack + # exclusive load/store (no LDREX/STREX). std::atomic RMW operations require libatomic, + # which is not linked to save flash (4-8KB). Even if linked, libatomic would use locks + # (ATOMIC_INT_LOCK_FREE=1), so explicit FreeRTOS mutexes are simpler and equivalent. cg.add_define(ThreadModel.MULTI_NO_ATOMICS) # force using arduino framework diff --git a/esphome/const.py b/esphome/const.py index b4cd3cfd1c..2b6b60d395 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -36,7 +36,30 @@ class Framework(StrEnum): class ThreadModel(StrEnum): - """Threading model identifiers for ESPHome scheduler.""" + """Threading model identifiers for ESPHome scheduler. + + ESPHome currently uses three threading models based on platform capabilities: + + SINGLE: + - Single-threaded platforms (ESP8266, RP2040) + - No RTOS task switching + - No concurrent access to scheduler data structures + - No atomics or locks required + - Minimal overhead + + MULTI_NO_ATOMICS: + - Multi-threaded platforms without hardware atomic RMW support (e.g. LibreTiny BK7231N) + - Uses FreeRTOS or another RTOS with multiple tasks + - CPU lacks exclusive load/store instructions (ARM968E-S has no LDREX/STREX) + - std::atomic cannot provide lock-free RMW; libatomic is avoided to save flash (4–8 KB) + - Scheduler uses explicit FreeRTOS mutexes for synchronization + + MULTI_ATOMICS: + - Multi-threaded platforms with hardware atomic RMW support (ESP32, Cortex-M, Host) + - CPU provides native atomic instructions (ESP32 S32C1I, ARM LDREX/STREX) + - std::atomic is used for lock-free synchronization + - Reduced contention and better performance + """ SINGLE = "ESPHOME_THREAD_SINGLE" MULTI_NO_ATOMICS = "ESPHOME_THREAD_MULTI_NO_ATOMICS" From 5d883c6e06d649651db138807ffd6cfd880c44ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 20 Nov 2025 15:06:40 +0100 Subject: [PATCH 080/102] [nrf52,i2c] fix review comment (#11931) --- esphome/components/i2c/i2c_bus_zephyr.cpp | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp index 658dcee35c..1eb9944dcb 100644 --- a/esphome/components/i2c/i2c_bus_zephyr.cpp +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -8,6 +8,22 @@ namespace esphome::i2c { static const char *const TAG = "i2c.zephyr"; +static const char *get_speed(uint32_t dev_config) { + switch (I2C_SPEED_GET(dev_config)) { + case I2C_SPEED_STANDARD: + return "100 kHz"; + case I2C_SPEED_FAST: + return "400 kHz"; + case I2C_SPEED_FAST_PLUS: + return "1 MHz"; + case I2C_SPEED_HIGH: + return "3.4 MHz"; + case I2C_SPEED_ULTRA: + return "5 MHz"; + } + return "unknown"; +} + void ZephyrI2CBus::setup() { if (!device_is_ready(this->i2c_dev_)) { ESP_LOGE(TAG, "I2C dev is not ready."); @@ -31,21 +47,6 @@ void ZephyrI2CBus::setup() { } void ZephyrI2CBus::dump_config() { - auto get_speed = [](uint32_t dev_config) { - switch (I2C_SPEED_GET(dev_config)) { - case I2C_SPEED_STANDARD: - return "100 kHz"; - case I2C_SPEED_FAST: - return "400 kHz"; - case I2C_SPEED_FAST_PLUS: - return "1 MHz"; - case I2C_SPEED_HIGH: - return "3.4 MHz"; - case I2C_SPEED_ULTRA: - return "5 MHz"; - } - return "unknown"; - }; ESP_LOGCONFIG(TAG, "I2C Bus:\n" " SDA Pin: GPIO%u\n" From 06bef148f432c5521c8d91743a0134f672ae4be5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 08:06:52 -0600 Subject: [PATCH 081/102] [core] Optimize DelayAction for no-argument case using if constexpr (#11913) --- esphome/core/base_automation.h | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index a5e6139182..c2519da839 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -178,7 +178,6 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) void play_complex(const Ts &...x) override { - auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; // If num_running_ > 1, we have multiple instances running in parallel @@ -187,9 +186,22 @@ template class DelayAction : public Action, public Compon // WARNING: This can accumulate delays if scripts are triggered faster than they complete! // Users should set max_runs on parallel scripts to limit concurrent executions. // Issue #10264: This is a workaround for parallel script delays interfering with each other. - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + + // Optimization: For no-argument delays (most common case), use direct lambda + // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + if constexpr (sizeof...(Ts) == 0) { + App.scheduler.set_timer_common_( + this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); }, + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } else { + // For delays with arguments, use std::bind to preserve argument values + // Arguments must be copied because original references may be invalid after delay + auto f = std::bind(&DelayAction::play_next_, this, x...); + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } } float get_setup_priority() const override { return setup_priority::HARDWARE; } From 3c86f3894b9f331f9df9d49b50725e9e7337df28 Mon Sep 17 00:00:00 2001 From: omartijn <44672243+omartijn@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:24:45 +0100 Subject: [PATCH 082/102] [hc8] Add support for HC8 CO2 sensor (#11872) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hc8/__init__.py | 1 + esphome/components/hc8/hc8.cpp | 99 ++++++++++++++++++++++ esphome/components/hc8/hc8.h | 37 ++++++++ esphome/components/hc8/sensor.py | 79 +++++++++++++++++ tests/components/hc8/common.yaml | 13 +++ tests/components/hc8/test.esp32-idf.yaml | 4 + tests/components/hc8/test.esp8266-ard.yaml | 4 + tests/components/hc8/test.rp2040-ard.yaml | 4 + 9 files changed, 242 insertions(+) create mode 100644 esphome/components/hc8/__init__.py create mode 100644 esphome/components/hc8/hc8.cpp create mode 100644 esphome/components/hc8/hc8.h create mode 100644 esphome/components/hc8/sensor.py create mode 100644 tests/components/hc8/common.yaml create mode 100644 tests/components/hc8/test.esp32-idf.yaml create mode 100644 tests/components/hc8/test.esp8266-ard.yaml create mode 100644 tests/components/hc8/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 250fbbd4d4..c3d8f4350f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -202,6 +202,7 @@ esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 +esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch diff --git a/esphome/components/hc8/__init__.py b/esphome/components/hc8/__init__.py new file mode 100644 index 0000000000..e1028456b0 --- /dev/null +++ b/esphome/components/hc8/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@omartijn"] diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp new file mode 100644 index 0000000000..5b649c2735 --- /dev/null +++ b/esphome/components/hc8/hc8.cpp @@ -0,0 +1,99 @@ +#include "hc8.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::hc8 { + +static const char *const TAG = "hc8"; +static const std::array HC8_COMMAND_GET_PPM{0x64, 0x69, 0x03, 0x5E, 0x4E}; +static const std::array HC8_COMMAND_CALIBRATE_PREAMBLE{0x11, 0x03, 0x03}; + +void HC8Component::setup() { + // send an initial query to the device, this will + // get it out of "active output mode", where it + // generates data every second + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // ensure the buffer is empty + while (this->available()) + this->read(); +} + +void HC8Component::update() { + uint32_t now_ms = App.get_loop_component_start_time(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + + while (this->available()) + this->read(); + + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // the sensor is a bit slow in responding, so trying to + // read immediately after sending a query will timeout + this->set_timeout(50, [this]() { + std::array response; + if (!this->read_array(response.data(), response.size())) { + ESP_LOGW(TAG, "Reading data from HC8 failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0x64 || response[1] != 0x69) { + ESP_LOGW(TAG, "Invalid preamble from HC8!"); + this->status_set_warning(); + return; + } + + if (crc16(response.data(), 12) != encode_uint16(response[13], response[12])) { + ESP_LOGW(TAG, "HC8 Checksum mismatch"); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + const uint16_t ppm = encode_uint16(response[5], response[4]); + ESP_LOGD(TAG, "HC8 Received CO₂=%uppm", ppm); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); + }); +} + +void HC8Component::calibrate(uint16_t baseline) { + ESP_LOGD(TAG, "HC8 Calibrating baseline to %uppm", baseline); + + std::array command{}; + std::copy(begin(HC8_COMMAND_CALIBRATE_PREAMBLE), end(HC8_COMMAND_CALIBRATE_PREAMBLE), begin(command)); + command[3] = baseline >> 8; + command[4] = baseline; + command[5] = 0; + + // the last byte is a checksum over the data + for (uint8_t i = 0; i < 5; ++i) + command[5] -= command[i]; + + this->write_array(command); + this->flush(); +} + +float HC8Component::get_setup_priority() const { return setup_priority::DATA; } + +void HC8Component::dump_config() { + ESP_LOGCONFIG(TAG, "HC8:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); + + ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_); +} + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h new file mode 100644 index 0000000000..7711fb8c97 --- /dev/null +++ b/esphome/components/hc8/hc8.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome::hc8 { + +class HC8Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override; + + void setup() override; + void update() override; + void dump_config() override; + + void calibrate(uint16_t baseline); + + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; } + + protected: + sensor::Sensor *co2_sensor_{nullptr}; + uint32_t warmup_seconds_{0}; +}; + +template class HC8CalibrateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, baseline) + + void play(const Ts &...x) override { this->parent_->calibrate(this->baseline_.value(x...)); } +}; + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py new file mode 100644 index 0000000000..90698b2661 --- /dev/null +++ b/esphome/components/hc8/sensor.py @@ -0,0 +1,79 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_BASELINE, + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + ICON_MOLECULE_CO2, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +DEPENDENCIES = ["uart"] + +CONF_WARMUP_TIME = "warmup_time" + +hc8_ns = cg.esphome_ns.namespace("hc8") +HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice) +HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HC8Component), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_WARMUP_TIME, default="75s" + ): cv.positive_time_period_seconds, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hc8", + baud_rate=9600, + require_rx=True, + require_tx=True, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) + cg.add(var.set_co2_sensor(sens)) + + cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) + + +CALIBRATION_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(HC8Component), + cv.Required(CONF_BASELINE): cv.templatable(cv.uint16_t), + } +) + + +@automation.register_action( + "hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA +) +async def hc8_calibration_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_BASELINE], args, cg.uint16) + cg.add(var.set_baseline(template_)) + return var diff --git a/tests/components/hc8/common.yaml b/tests/components/hc8/common.yaml new file mode 100644 index 0000000000..ac3b454315 --- /dev/null +++ b/tests/components/hc8/common.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - hc8.calibrate: + id: hc8_sensor + baseline: 420 + +sensor: + - platform: hc8 + id: hc8_sensor + co2: + name: HC8 CO2 Value + update_interval: 15s diff --git a/tests/components/hc8/test.esp32-idf.yaml b/tests/components/hc8/test.esp32-idf.yaml new file mode 100644 index 0000000000..2d29656c94 --- /dev/null +++ b/tests/components/hc8/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.esp8266-ard.yaml b/tests/components/hc8/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5a05efa259 --- /dev/null +++ b/tests/components/hc8/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.rp2040-ard.yaml b/tests/components/hc8/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f1df2daf83 --- /dev/null +++ b/tests/components/hc8/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml From 59cd6dbf70235fa89c41eeedffa610a9c823ac76 Mon Sep 17 00:00:00 2001 From: damib Date: Thu, 20 Nov 2025 15:28:14 +0100 Subject: [PATCH 083/102] [climate_ir] Add optional humidity sensor (#9805) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/climate_ir/__init__.py | 11 ++++++++++- esphome/components/climate_ir/climate_ir.cpp | 15 ++++++++++++--- esphome/components/climate_ir/climate_ir.h | 2 ++ tests/components/climate_ir_lg/common.yaml | 12 ++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 6d66abf4cd..5315be3db6 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -3,7 +3,12 @@ import logging import esphome.codegen as cg from esphome.components import climate, remote_base, sensor import esphome.config_validation as cv -from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import ( + CONF_HUMIDITY_SENSOR, + CONF_SENSOR, + CONF_SUPPORTS_COOL, + CONF_SUPPORTS_HEAT, +) from esphome.cpp_generator import MockObjClass _LOGGER = logging.getLogger(__name__) @@ -32,6 +37,7 @@ def climate_ir_schema( cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), } ) .extend(cv.COMPONENT_SCHEMA) @@ -61,6 +67,9 @@ async def register_climate_ir(var, config): if sensor_id := config.get(CONF_SENSOR): sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) + if sensor_id := config.get(CONF_HUMIDITY_SENSOR): + sens = await cg.get_variable(sensor_id) + cg.add(var.set_humidity_sensor(sens)) async def new_climate_ir(config, *args): diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 2b95792a6c..50c8d459b0 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -11,7 +11,9 @@ climate::ClimateTraits ClimateIR::traits() { if (this->sensor_ != nullptr) { traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); } - + if (this->humidity_sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + } traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); @@ -39,9 +41,16 @@ void ClimateIR::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else { - this->current_temperature = NAN; } + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + // current humidity changed, publish state + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 62a43f0b2d..ac76d33853 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -43,6 +43,7 @@ class ClimateIR : public Component, void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } protected: float minimum_temperature_, maximum_temperature_, temperature_step_; @@ -67,6 +68,7 @@ class ClimateIR : public Component, climate::ClimatePresetMask presets_{}; sensor::Sensor *sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; }; } // namespace climate_ir diff --git a/tests/components/climate_ir_lg/common.yaml b/tests/components/climate_ir_lg/common.yaml index da0d656b21..37011b16ee 100644 --- a/tests/components/climate_ir_lg/common.yaml +++ b/tests/components/climate_ir_lg/common.yaml @@ -1,4 +1,16 @@ +sensor: + - platform: template + id: temp_sensor + lambda: return 22.0; + update_interval: 60s + - platform: template + id: humidity_sensor + lambda: return 50.0; + update_interval: 60s + climate: - platform: climate_ir_lg name: LG Climate transmitter_id: xmitr + sensor: temp_sensor + humidity_sensor: humidity_sensor From 1accb4ff3488c1763da426faea38f73408714332 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:58:21 -0500 Subject: [PATCH 084/102] [ltr501][ltr_als_ps] Rename enum to avoid collision with lwip defines (#12017) --- esphome/components/ltr501/ltr501.cpp | 10 +++++----- esphome/components/ltr501/ltr501.h | 4 ++-- esphome/components/ltr_als_ps/ltr_als_ps.cpp | 12 ++++++------ esphome/components/ltr_als_ps/ltr_als_ps.h | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index be5a4ddccf..04de91e362 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -174,7 +174,7 @@ void LTRAlsPs501Component::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -379,18 +379,18 @@ void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time) } } -DataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } data.gain = als_status.gain; - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 849ff6bc23..02c025da30 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr501 { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime501 time); void configure_gain_(AlsGain501 gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index c3ea5848c8..f9c1474c85 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -165,7 +165,7 @@ void LTRAlsPsComponent::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -376,23 +376,23 @@ void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) { } } -DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; if (als_status.data_invalid) { ESP_LOGW(TAG, "Data available but not valid"); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2c768009ab..c6052300de 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr_als_ps { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime time); void configure_gain_(AlsGain gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); From a1e507baf817f99d81593130796dff92b25b948d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:10:28 -0500 Subject: [PATCH 085/102] [cst816][packet_transport][udp][wake_on_lan] Fix error messages (#12019) --- .../cst816/touchscreen/cst816_touchscreen.cpp | 2 +- .../components/packet_transport/packet_transport.cpp | 2 +- esphome/components/udp/udp_component.cpp | 10 +++++----- esphome/components/wake_on_lan/wake_on_lan.cpp | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0ba2d9df94..8ed9fa3f87 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,8 +19,8 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: - this->mark_failed(); this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); + this->mark_failed(); return; } this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 8bde4ee505..857b40ca0e 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -195,8 +195,8 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { - this->mark_failed(); this->status_set_error("Device name exceeds 255 chars"); + this->mark_failed(); return; } this->resend_ping_key_ = this->ping_pong_enable_; diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 8a9ce612b4..7714793e1c 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,8 +21,8 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } int enable = 1; @@ -41,15 +41,15 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); - this->mark_failed(); this->status_set_error("Unable to set nonblocking"); + this->mark_failed(); return; } int enable = 1; @@ -73,8 +73,8 @@ void UDPComponent::setup() { err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); - this->mark_failed(); this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + this->mark_failed(); return; } } @@ -82,8 +82,8 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); this->status_set_error("Unable to bind socket"); + this->mark_failed(); return; } } diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index adf5a080e5..7993abd7e7 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,8 +67,8 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } int enable = 1; From 01addeae080b0cce517ed296d6ea4a998d454ae6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:11:41 -0600 Subject: [PATCH 086/102] Bump actions/checkout from 5.0.1 to 6.0.0 (#12022) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci-clang-tidy-hash.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .../workflows/ci-memory-impact-comment.yml | 2 +- .github/workflows/ci.yml | 30 +++++++++---------- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 8 ++--- .github/workflows/sync-device-classes.yml | 4 +-- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index fb284c9d8c..8d8e08a5fc 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -22,7 +22,7 @@ jobs: if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Generate a token id: generate-token diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index be5af1aff1..b377ca76d8 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index aebf07949d..9556b99015 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 9bb983b993..5287d92b10 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,7 +43,7 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml index c82ae30f55..6ca58e252e 100644 --- a/.github/workflows/ci-memory-impact-comment.yml +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -49,7 +49,7 @@ jobs: - name: Check out code from base repository if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Always check out from the base repository (esphome/esphome), never from forks # Use the PR's target branch to ensure we run trusted code from the main repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5293c62d34..9c2fab0912 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT @@ -70,7 +70,7 @@ jobs: if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -91,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -132,7 +132,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python id: restore-python uses: ./.github/actions/restore-python @@ -183,7 +183,7 @@ jobs: component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -237,7 +237,7 @@ jobs: if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python 3.13 id: python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 @@ -273,7 +273,7 @@ jobs: if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python @@ -321,7 +321,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -400,7 +400,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -489,7 +489,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -577,7 +577,7 @@ jobs: version: 1.0 - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -662,7 +662,7 @@ jobs: if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -688,7 +688,7 @@ jobs: skip: ${{ steps.check-script.outputs.skip }} steps: - name: Check out target branch - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: ref: ${{ github.base_ref }} @@ -840,7 +840,7 @@ jobs: flash_usage: ${{ steps.extract.outputs.flash_usage }} steps: - name: Check out PR branch - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -908,7 +908,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 21fff10c95..80fab8819a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a064f6ef3a..497ecd29e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,7 +60,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -92,7 +92,7 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -168,7 +168,7 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Download digests uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 4fc287b067..2e36dc517d 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,10 +13,10 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Checkout Home Assistant - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: repository: home-assistant/core path: lib/home-assistant From 0dea7a23e3d0a035cdee7ccfeea303eb083da072 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 21 Nov 2025 07:39:59 -0500 Subject: [PATCH 087/102] [jsn_sr04t] Fix model AJ_SR04M (#11992) --- esphome/components/jsn_sr04t/jsn_sr04t.cpp | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp index 077d4e58ea..84181dac48 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.cpp +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -10,7 +10,7 @@ namespace jsn_sr04t { static const char *const TAG = "jsn_sr04t.sensor"; void Jsnsr04tComponent::update() { - this->write_byte(0x55); + this->write_byte((this->model_ == AJ_SR04M) ? 0x01 : 0x55); ESP_LOGV(TAG, "Request read out from sensor"); } @@ -31,19 +31,10 @@ void Jsnsr04tComponent::loop() { } void Jsnsr04tComponent::check_buffer_() { - uint8_t checksum = 0; - switch (this->model_) { - case JSN_SR04T: - checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; - break; - case AJ_SR04M: - checksum = this->buffer_[1] + this->buffer_[2]; - break; - } - + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; if (this->buffer_[3] == checksum) { uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]); - if (distance > 250) { + if (distance > ((this->model_ == AJ_SR04M) ? 200 : 250)) { float meters = distance / 1000.0f; ESP_LOGV(TAG, "Distance from sensor: %umm, %.3fm", distance, meters); this->publish_state(meters); From 150e26dc2bfbf2155459c39c5f05873301254968 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Nov 2025 06:41:48 -0600 Subject: [PATCH 088/102] [cst816][http_request] Fix status_set_error() dangling pointer bugs (#12033) --- .../cst816/touchscreen/cst816_touchscreen.cpp | 3 ++- .../http_request/update/http_request_update.cpp | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 8ed9fa3f87..0560f1b475 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,7 +19,8 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: - this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); + ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); + this->status_set_error("Unknown chip ID"); this->mark_failed(); return; } diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 06aa6da6a4..9dbf8d181a 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -49,18 +49,18 @@ void HttpRequestUpdate::update_task(void *params) { auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); }); UPDATE_RETURN; } RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); }); container->end(); UPDATE_RETURN; } @@ -121,9 +121,9 @@ void HttpRequestUpdate::update_task(void *params) { } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); }); UPDATE_RETURN; } From 972b7e84fe800ea9dbd6b69d6f4bcc023b89e026 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 21 Nov 2025 14:38:44 +0100 Subject: [PATCH 089/102] [tests] Fix mipi_spi test board (#12031) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- tests/component_tests/mipi_spi/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index e68f6fbfba..56a52df2ab 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -220,7 +220,7 @@ def test_esp32s3_specific_errors( set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) with pytest.raises(cv.Invalid, match=error_match): @@ -250,7 +250,7 @@ def test_custom_model_with_all_options( """Test custom model configuration with all available options.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) run_schema_validation( @@ -293,7 +293,7 @@ def test_all_predefined_models( """Test all predefined display models validate successfully with appropriate defaults.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) # Enable PSRAM which is required for some models From 782aee92a77a2a3d991c97f671c259b1b961883e Mon Sep 17 00:00:00 2001 From: Marko Draca Date: Fri, 21 Nov 2025 20:50:07 +0100 Subject: [PATCH 090/102] [mcp3204] differential mode support (#7436) Co-authored-by: marko Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/mcp3204/mcp3204.cpp | 21 ++++++++++++------- esphome/components/mcp3204/mcp3204.h | 2 +- esphome/components/mcp3204/sensor/__init__.py | 3 +++ .../mcp3204/sensor/mcp3204_sensor.cpp | 5 ++--- .../mcp3204/sensor/mcp3204_sensor.h | 3 ++- tests/components/mcp3204/common.yaml | 16 +++++++++++++- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 4bb0cbed76..f0dd171a14 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -16,16 +16,21 @@ void MCP3204::dump_config() { ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); } -float MCP3204::read_data(uint8_t pin) { - uint8_t adc_primary_config = 0b00000110 | (pin >> 2); - uint8_t adc_secondary_config = pin << 6; +float MCP3204::read_data(uint8_t pin, bool differential) { + uint8_t command, b0, b1; + + command = (1 << 6) | // start bit + ((differential ? 0 : 1) << 5) | // single or differential bit + ((pin & 0x07) << 2); // pin + this->enable(); - this->transfer_byte(adc_primary_config); - uint8_t adc_primary_byte = this->transfer_byte(adc_secondary_config); - uint8_t adc_secondary_byte = this->transfer_byte(0x00); + this->transfer_byte(command); + b0 = this->transfer_byte(0x00); + b1 = this->transfer_byte(0x00); this->disable(); - uint16_t digital_value = (adc_primary_byte << 8 | adc_secondary_byte) & 0b111111111111; - return float(digital_value) / 4096.000 * this->reference_voltage_; + + uint16_t digital_value = encode_uint16(b0, b1) >> 4; + return float(digital_value) / 4096.000 * this->reference_voltage_; // in V } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/mcp3204.h b/esphome/components/mcp3204/mcp3204.h index 27261aa373..6287263a2a 100644 --- a/esphome/components/mcp3204/mcp3204.h +++ b/esphome/components/mcp3204/mcp3204.h @@ -18,7 +18,7 @@ class MCP3204 : public Component, void setup() override; void dump_config() override; float get_setup_priority() const override; - float read_data(uint8_t pin); + float read_data(uint8_t pin, bool differential); protected: float reference_voltage_; diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py index a4b177cbcf..5f9aa9fdb6 100644 --- a/esphome/components/mcp3204/sensor/__init__.py +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -13,6 +13,7 @@ MCP3204Sensor = mcp3204_ns.class_( "MCP3204Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) CONF_MCP3204_ID = "mcp3204_id" +CONF_DIFF_MODE = "diff_mode" CONFIG_SCHEMA = ( sensor.sensor_schema(MCP3204Sensor) @@ -20,6 +21,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + cv.Optional(CONF_DIFF_MODE, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -30,6 +32,7 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], config[CONF_NUMBER], + config[CONF_DIFF_MODE], ) await cg.register_parented(var, config[CONF_MCP3204_ID]) await cg.register_component(var, config) diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index ce0fd25462..4c4abef4a7 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -7,16 +7,15 @@ namespace mcp3204 { static const char *const TAG = "mcp3204.sensor"; -MCP3204Sensor::MCP3204Sensor(uint8_t pin) : pin_(pin) {} - float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } void MCP3204Sensor::dump_config() { LOG_SENSOR("", "MCP3204 Sensor", this); ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Differential Mode: %s", YESNO(this->differential_mode_)); LOG_UPDATE_INTERVAL(this); } -float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_); } +float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_, this->differential_mode_); } void MCP3204Sensor::update() { this->publish_state(this->sample()); } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 21c45590ab..5665b80b98 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -15,7 +15,7 @@ class MCP3204Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { public: - MCP3204Sensor(uint8_t pin); + MCP3204Sensor(uint8_t pin, bool differential_mode) : pin_(pin), differential_mode_(differential_mode) {} void update() override; void dump_config() override; @@ -24,6 +24,7 @@ class MCP3204Sensor : public PollingComponent, protected: uint8_t pin_; + bool differential_mode_; }; } // namespace mcp3204 diff --git a/tests/components/mcp3204/common.yaml b/tests/components/mcp3204/common.yaml index eca6ec44f4..9750f0af8e 100644 --- a/tests/components/mcp3204/common.yaml +++ b/tests/components/mcp3204/common.yaml @@ -4,7 +4,21 @@ mcp3204: sensor: - platform: mcp3204 - id: mcp3204_sensor + id: mcp3204_default_single_0 mcp3204_id: mcp3204_hub number: 0 update_interval: 5s + + - platform: mcp3204 + id: mcp3204_single_0 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: false + update_interval: 5s + + - platform: mcp3204 + id: mcp3204_diff_0_1 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: true + update_interval: 5s From 3f6f2d7d650feee79896b13903c7a26e98559d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 21 Nov 2025 21:28:42 +0100 Subject: [PATCH 091/102] [bm8563] Add bm8563 component (#11616) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/bm8563/__init__.py | 1 + esphome/components/bm8563/bm8563.cpp | 198 ++++++++++++++++++ esphome/components/bm8563/bm8563.h | 57 +++++ esphome/components/bm8563/time.py | 80 +++++++ tests/components/bm8563/common.yaml | 10 + tests/components/bm8563/test.esp32-ard.yaml | 4 + tests/components/bm8563/test.esp32-idf.yaml | 4 + tests/components/bm8563/test.esp8266-ard.yaml | 4 + tests/components/bm8563/test.rp2040-ard.yaml | 4 + 10 files changed, 363 insertions(+) create mode 100644 esphome/components/bm8563/__init__.py create mode 100644 esphome/components/bm8563/bm8563.cpp create mode 100644 esphome/components/bm8563/bm8563.h create mode 100644 esphome/components/bm8563/time.py create mode 100644 tests/components/bm8563/common.yaml create mode 100644 tests/components/bm8563/test.esp32-ard.yaml create mode 100644 tests/components/bm8563/test.esp32-idf.yaml create mode 100644 tests/components/bm8563/test.esp8266-ard.yaml create mode 100644 tests/components/bm8563/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index c3d8f4350f..d6ec7b882e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -72,6 +72,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_nus/* @tomaszduda23 esphome/components/bluetooth_proxy/* @bdraco @jesserockz +esphome/components/bm8563/* @abmantis esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth diff --git a/esphome/components/bm8563/__init__.py b/esphome/components/bm8563/__init__.py new file mode 100644 index 0000000000..20254a8b00 --- /dev/null +++ b/esphome/components/bm8563/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@abmantis"] diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp new file mode 100644 index 0000000000..07831485c1 --- /dev/null +++ b/esphome/components/bm8563/bm8563.cpp @@ -0,0 +1,198 @@ +#include "bm8563.h" +#include "esphome/core/log.h" + +namespace esphome::bm8563 { + +static const char *const TAG = "bm8563"; + +static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00; +static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01; +static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4 +static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8 +static constexpr uint8_t TIMER_CONTROL_REG = 0x0E; +static constexpr uint8_t TIMER_VALUE_REG = 0x0F; +static constexpr uint8_t CLOCK_1_HZ = 0x82; +static constexpr uint8_t CLOCK_1_60_HZ = 0x83; +// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds +static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60; + +void BM8563::setup() { + if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) { + this->mark_failed(); + return; + } +} + +void BM8563::update() { this->read_time(); } + +void BM8563::dump_config() { + ESP_LOGCONFIG(TAG, "BM8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void BM8563::start_timer(uint32_t duration_s) { + this->clear_irq_(); + this->set_timer_irq_(duration_s); +} + +void BM8563::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + + ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour, + now.minute, now.second); + + this->set_time_(now); + this->set_date_(now); +} + +void BM8563::read_time() { + ESPTime rtc_time; + this->get_time_(rtc_time); + this->get_date_(rtc_time); + rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid + ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); + + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +uint8_t BM8563::bcd2_to_byte_(uint8_t value) { + const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10; + return tmp + (value & 0x0F); +} + +uint8_t BM8563::byte_to_bcd2_(uint8_t value) { + const uint8_t bcdhigh = value / 10; + value -= bcdhigh * 10; + return (bcdhigh << 4) | value; +} + +void BM8563::get_time_(ESPTime &time) { + uint8_t buf[3] = {0}; + this->read_register(TIME_FIRST_REG, buf, 3); + + time.second = this->bcd2_to_byte_(buf[0] & 0x7f); + time.minute = this->bcd2_to_byte_(buf[1] & 0x7f); + time.hour = this->bcd2_to_byte_(buf[2] & 0x3f); +} + +void BM8563::set_time_(const ESPTime &time) { + uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)}; + this->write_register_(TIME_FIRST_REG, buf, 3); +} + +void BM8563::get_date_(ESPTime &time) { + uint8_t buf[4] = {0}; + this->read_register(DATE_FIRST_REG, buf, sizeof(buf)); + + time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f); + time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07); + time.month = this->bcd2_to_byte_(buf[2] & 0x1f); + + uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff); + + if (buf[2] & 0x80) { + time.year = 1900 + year_byte; + } else { + time.year = 2000 + year_byte; + } +} + +void BM8563::set_date_(const ESPTime &time) { + uint8_t buf[4] = { + this->byte_to_bcd2_(time.day_of_month), + this->byte_to_bcd2_(time.day_of_week), + this->byte_to_bcd2_(time.month), + this->byte_to_bcd2_(time.year % 100), + }; + + if (time.year < 2000) { + buf[2] = buf[2] | 0x80; + } + + this->write_register_(DATE_FIRST_REG, buf, 4); +} + +void BM8563::write_byte_(uint8_t reg, uint8_t value) { + if (!this->write_byte(reg, value)) { + ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value); + } +} + +void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) { + if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len); + } +} + +optional BM8563::read_register_(uint8_t reg) { + uint8_t data; + if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to read register 0x%02X", reg); + return {}; + } + return data; +} + +void BM8563::set_timer_irq_(uint32_t duration_s) { + ESP_LOGI(TAG, "Timer Duration: %u s", duration_s); + + if (duration_s > MAX_TIMER_DURATION_S) { + ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S); + return; + } + + if (duration_s > 255) { + uint8_t duration_minutes = duration_s / 60; + this->write_byte_(TIMER_VALUE_REG, duration_minutes); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ); + } else { + this->write_byte_(TIMER_VALUE_REG, duration_s); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ); + } + + auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_ctrl_status_2.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value(); + ctrl_status_2_reg_value |= (1 << 0); + ctrl_status_2_reg_value &= ~(1 << 7); + this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value); +} + +void BM8563::clear_irq_() { + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3); +} + +void BM8563::disable_irq_() { + this->clear_irq_(); + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc); +} + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/bm8563.h b/esphome/components/bm8563/bm8563.h new file mode 100644 index 0000000000..eda2d1b3c0 --- /dev/null +++ b/esphome/components/bm8563/bm8563.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome::bm8563 { + +class BM8563 : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void write_time(); + void read_time(); + void start_timer(uint32_t duration_s); + + private: + void get_time_(ESPTime &time); + void get_date_(ESPTime &time); + + void set_time_(const ESPTime &time); + void set_date_(const ESPTime &time); + + void set_timer_irq_(uint32_t duration_s); + void clear_irq_(); + void disable_irq_(); + + void write_byte_(uint8_t reg, uint8_t value); + void write_register_(uint8_t reg, const uint8_t *data, size_t len); + optional read_register_(uint8_t reg); + + uint8_t bcd2_to_byte_(uint8_t value); + uint8_t byte_to_bcd2_(uint8_t value); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->read_time(); } +}; + +template class TimerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto duration = this->duration_.value(x...); + this->parent_->start_timer(duration); + } +}; + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py new file mode 100644 index 0000000000..2785315af2 --- /dev/null +++ b/esphome/components/bm8563/time.py @@ -0,0 +1,80 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_DURATION, CONF_ID + +DEPENDENCIES = ["i2c"] + +I2C_ADDR = 0x51 + +bm8563_ns = cg.esphome_ns.namespace("bm8563") +BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice) +WriteAction = bm8563_ns.class_("WriteAction", automation.Action) +ReadAction = bm8563_ns.class_("ReadAction", automation.Action) +TimerAction = bm8563_ns.class_("TimerAction", automation.Action) + +CONFIG_SCHEMA = ( + time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BM8563), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(I2C_ADDR)) +) + + +@automation.register_action( + "bm8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_write_time_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 + + +@automation.register_action( + "bm8563.start_timer", + TimerAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(BM8563), + cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), + } + ), +) +async def bm8563_start_timer_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_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "bm8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_read_time_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 + + +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) + await time.register_time(var, config) diff --git a/tests/components/bm8563/common.yaml b/tests/components/bm8563/common.yaml new file mode 100644 index 0000000000..ec3fdd1518 --- /dev/null +++ b/tests/components/bm8563/common.yaml @@ -0,0 +1,10 @@ +esphome: + on_boot: + - bm8563.read_time + - bm8563.write_time + - bm8563.start_timer: + duration: 300s + +time: + - platform: bm8563 + i2c_id: i2c_bus diff --git a/tests/components/bm8563/test.esp32-ard.yaml b/tests/components/bm8563/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/bm8563/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp32-idf.yaml b/tests/components/bm8563/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/bm8563/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/bm8563/test.esp8266-ard.yaml b/tests/components/bm8563/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/bm8563/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/bm8563/test.rp2040-ard.yaml b/tests/components/bm8563/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/bm8563/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From a5751b294f06448375878dcb620ac6c4ec054893 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:13:23 +1300 Subject: [PATCH 092/102] [api] Rename `USE_API_SERVICES` to `USE_API_USER_DEFINED_ACTIONS` (#12029) --- esphome/components/api/__init__.py | 4 ++-- esphome/components/api/api.proto | 8 ++++---- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 2 +- esphome/components/api/api_pb2.cpp | 2 +- esphome/components/api/api_pb2.h | 4 ++-- esphome/components/api/api_pb2_dump.cpp | 4 ++-- esphome/components/api/api_pb2_service.cpp | 4 ++-- esphome/components/api/api_pb2_service.h | 6 +++--- esphome/components/api/api_server.h | 8 ++++---- esphome/components/api/custom_api_device.h | 10 +++++----- esphome/components/api/list_entities.cpp | 2 +- esphome/components/api/list_entities.h | 2 +- esphome/components/api/user_services.h | 4 ++-- esphome/core/component_iterator.cpp | 6 +++--- esphome/core/component_iterator.h | 6 +++--- esphome/core/defines.h | 2 +- 17 files changed, 38 insertions(+), 38 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index a9286c531f..7f84f2f247 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -260,9 +260,9 @@ async def to_code(config): cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) - # Set USE_API_SERVICES if any services are enabled + # Set USE_API_USER_DEFINED_ACTIONS if any services are enabled if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: - cg.add_define("USE_API_SERVICES") + cg.add_define("USE_API_USER_DEFINED_ACTIONS") # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration if config[CONF_CUSTOM_SERVICES]: diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e115e4630d..26d1fa6876 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -855,21 +855,21 @@ enum ServiceArgType { SERVICE_ARG_TYPE_STRING_ARRAY = 7; } message ListEntitiesServicesArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; ServiceArgType type = 2; } message ListEntitiesServicesResponse { option (id) = 41; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; fixed32 key = 2; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; } message ExecuteServiceArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; bool bool_ = 1; int32 legacy_int = 2; float float_ = 3; @@ -885,7 +885,7 @@ message ExecuteServiceRequest { option (id) = 42; option (source) = SOURCE_CLIENT; option (no_delay) = true; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; fixed32 key = 1; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4acd2fc15c..c60680ae43 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1541,7 +1541,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes } } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void APIConnection::execute_service(const ExecuteServiceRequest &msg) { bool found = false; for (auto *service : this->parent_->get_user_services()) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6cfd108927..af3a19909f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -221,7 +221,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_API_HOMEASSISTANT_STATES void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void execute_service(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0a073fb662..d52135a566 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -995,7 +995,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name_ref_); buffer.encode_uint32(2, static_cast(this->type)); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 358049026e..b19e92d4ff 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -63,7 +63,7 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, SERVICE_ARG_TYPE_INT = 1, @@ -1239,7 +1239,7 @@ class GetTimeResponse final : public ProtoDecodableMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name_ref_{}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 127ef44cd8..ea752ba3ba 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -206,7 +206,7 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "UNKNOWN"; } } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: @@ -1177,7 +1177,7 @@ void GetTimeResponse::dump_to(std::string &out) const { out.append(format_hex_pretty(this->timezone, this->timezone_len)); out.append("\n"); } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); dump_field(out, "name", this->name_ref_); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 9d227af0a3..3d28a137c8 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -193,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS case ExecuteServiceRequest::MESSAGE_TYPE: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); @@ -670,7 +670,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc this->subscribe_home_assistant_states(msg); } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 549b00ee6a..827b89e23c 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -79,7 +79,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_get_time_response(const GetTimeResponse &value){}; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif @@ -239,7 +239,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void execute_service(const ExecuteServiceRequest &msg) = 0; #endif #ifdef USE_API_NOISE @@ -368,7 +368,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void on_execute_service_request(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 2d58063d6c..a3a082e165 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -12,7 +12,7 @@ #include "esphome/core/log.h" #include "list_entities.h" #include "subscribe_state.h" -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif @@ -124,7 +124,7 @@ class APIServer : public Component, public Controller { #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void initialize_user_services(std::initializer_list services) { this->user_services_.assign(services); } @@ -166,7 +166,7 @@ class APIServer : public Component, public Controller { std::function f); const std::vector &get_state_subs() const; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS const std::vector &get_user_services() const { return this->user_services_; } #endif @@ -206,7 +206,7 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS std::vector user_services_; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 43ea644f0c..1006d07533 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -3,12 +3,12 @@ #include #include "api_server.h" #ifdef USE_API -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif namespace esphome::api { -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, @@ -21,7 +21,7 @@ template class CustomAPIDeviceService : public UserS T *obj_; void (T::*callback_)(Ts...); }; -#endif // USE_API_SERVICES +#endif // USE_API_USER_DEFINED_ACTIONS class CustomAPIDevice { public: @@ -49,7 +49,7 @@ class CustomAPIDevice { * @param name The name of the service to register. * @param arg_names The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { @@ -90,7 +90,7 @@ class CustomAPIDevice { * @param callback The member function to call when the service is triggered. * @param name The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(), const std::string &name) { #ifdef USE_API_CUSTOM_SERVICES auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); // NOLINT diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index da4800a45e..e18fc17801 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -82,7 +82,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done( ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 769d7b9b6e..4c90dbbad8 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_TEXT_SENSOR bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool on_service(UserServiceDescriptor *service) override; #endif #ifdef USE_CAMERA diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 2a887fc52d..501b702e6b 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -7,7 +7,7 @@ #include "esphome/core/automation.h" #include "api_pb2.h" -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace esphome::api { class UserServiceDescriptor { @@ -122,4 +122,4 @@ template class UserServiceTrigger : public UserServiceBaseprocess_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); break; @@ -185,7 +185,7 @@ void ComponentIterator::advance() { bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } #endif #ifdef USE_CAMERA diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 641d42898a..1b1bd80ac5 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -10,7 +10,7 @@ namespace esphome { -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace api { class UserServiceDescriptor; } // namespace api @@ -45,7 +45,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual bool on_service(api::UserServiceDescriptor *service); #endif #ifdef USE_CAMERA @@ -122,7 +122,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS SERVICE, #endif #ifdef USE_CAMERA diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 41f4b28cd5..03362ce07a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -124,7 +124,7 @@ #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE #define USE_API_PLAINTEXT -#define USE_API_SERVICES +#define USE_API_USER_DEFINED_ACTIONS #define USE_API_CUSTOM_SERVICES #define API_MAX_SEND_QUEUE 8 #define USE_MD5 From f42b806889f51f81f31db8810443b5462c58e8e6 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sun, 23 Nov 2025 22:03:13 +0100 Subject: [PATCH 093/102] [core] Fix error on invalid id extend/remove (#12064) --- esphome/config.py | 2 ++ .../fixtures/substitutions/05-extend-remove.approved.yaml | 6 ++++++ .../fixtures/substitutions/05-extend-remove.input.yaml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/esphome/config.py b/esphome/config.py index 4c8019de75..1c4cdd93c6 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -355,6 +355,8 @@ def _get_item_id(item: Any) -> str | Extend | Remove | None: if isinstance(item_id, Extend): # Remove instances of Extend so they don't overwrite the original item when merging: del item[CONF_ID] + elif not isinstance(item_id, (str, Remove)): + return None return item_id diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml index 35e3e6258f..773a124f25 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml @@ -31,3 +31,9 @@ lvgl: id: object5 x: 10 y: 11 + - obj: + id: + - Invalid ID + - obj: + id: + invalid: id diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml index 617f09c31c..e6d46d6dc4 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml @@ -37,6 +37,10 @@ packages: id: object5 x: 10 y: 11 + - obj: + id: ["Invalid ID"] + - obj: + id: {"invalid": "id"} some_component: - id: !extend ${A} From c91a9495e611d4d1b94cffc6e88bd40f1d1843fd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 23 Nov 2025 16:19:26 -0500 Subject: [PATCH 094/102] [ci] Fix filename (#12065) --- .../stts22h/{test.nrf52.yaml => test.nrf52-adafruit.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/components/stts22h/{test.nrf52.yaml => test.nrf52-adafruit.yaml} (100%) diff --git a/tests/components/stts22h/test.nrf52.yaml b/tests/components/stts22h/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/stts22h/test.nrf52.yaml rename to tests/components/stts22h/test.nrf52-adafruit.yaml From 5750f7fccbcf22c1df62d363cd0adb7d173be55d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 23 Nov 2025 22:25:24 -0500 Subject: [PATCH 095/102] [ci] Fix test grouping (#12067) --- tests/components/stts22h/common.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/stts22h/common.yaml b/tests/components/stts22h/common.yaml index 802afe2065..2e332f9276 100644 --- a/tests/components/stts22h/common.yaml +++ b/tests/components/stts22h/common.yaml @@ -1,4 +1,5 @@ sensor: - platform: stts22h + i2c_id: i2c_bus name: Temperature update_interval: 15s From 60d687c2c6c9bb8961763958d1b0ad78fee2b772 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 23 Nov 2025 23:31:14 -0500 Subject: [PATCH 096/102] [esp32] Fix C2 builds (#12050) --- esphome/components/esp32/__init__.py | 6 ++++++ esphome/components/esp32/pre_build.py.script | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 esphome/components/esp32/pre_build.py.script diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6f577d2926..59c6029334 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -883,6 +883,12 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) + add_extra_script( + "pre", + "pre_build.py", + Path(__file__).parent / "pre_build.py.script", + ) + add_extra_script( "post", "post_build.py", diff --git a/esphome/components/esp32/pre_build.py.script b/esphome/components/esp32/pre_build.py.script new file mode 100644 index 0000000000..af12275a0b --- /dev/null +++ b/esphome/components/esp32/pre_build.py.script @@ -0,0 +1,9 @@ +Import("env") # noqa: F821 + +# Remove custom_sdkconfig from the board config as it causes +# pioarduino to enable some strange hybrid build mode that breaks IDF +board = env.BoardConfig() +if "espidf.custom_sdkconfig" in board: + del board._manifest["espidf"]["custom_sdkconfig"] + if not board._manifest["espidf"]: + del board._manifest["espidf"] From b4b98505baed1fea37c1e2e5da11c2c8ea7d2e26 Mon Sep 17 00:00:00 2001 From: James <23900@qq.com> Date: Mon, 24 Nov 2025 23:05:02 +1300 Subject: [PATCH 097/102] [mipi_dsi] add guition JC4880P443 display (#12068) --- esphome/components/mipi_dsi/models/guition.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index 5f7db4ebda..cd566633f9 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -35,3 +35,70 @@ DriverChip( (0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00), ], ) + + +# JC4880P443 Driver Configuration (ST7701) +# Using parameters from esp_lcd_st7701.h and the working full init sequence +# ---------------------------------------------------------------------------------------------------------------------- +# * Resolution: 480x800 +# * PCLK Frequency: 34 MHz +# * DSI Lane Bit Rate: 500 Mbps (using 2-Lane DSI configuration) +# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42) +# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166) +# ---------------------------------------------------------------------------------------------------------------------- +DriverChip( + "JC4880P443", + width=480, + height=800, + hsync_back_porch=42, + hsync_pulse_width=12, + hsync_front_porch=42, + vsync_back_porch=8, + vsync_pulse_width=2, + vsync_front_porch=166, + pclk_frequency="34MHz", + lane_bit_rate="500Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=5, + initsequence=[ + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x63, 0x00), + (0xC1, 0x0D, 0x02), + (0xC2, 0x10, 0x08), + (0xCC, 0x10), + (0xB0, 0x80, 0x09, 0x53, 0x0C, 0xD0, 0x07, 0x0C, 0x09, 0x09, 0x28, 0x06, 0xD4, 0x13, 0x69, 0x2B, 0x71), + (0xB1, 0x80, 0x94, 0x5A, 0x10, 0xD3, 0x06, 0x0A, 0x08, 0x08, 0x25, 0x03, 0xD3, 0x12, 0x66, 0x6A, 0x0D), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x58), + (0xB2, 0x87), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x21), + (0xB9, 0x10, 0x1F), + (0xBB, 0x03), + (0xBC, 0x00), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x3A, 0x02), + (0xE1, 0x04, 0xA0, 0x00, 0xA0, 0x05, 0xA0, 0x00, 0xA0, 0x00, 0x40, 0x40), + (0xE2, 0x30, 0x00, 0x40, 0x40, 0x32, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00), + (0xE3, 0x00, 0x00, 0x33, 0x33), + (0xE4, 0x44, 0x44), + (0xE5, 0x09, 0x2E, 0xA0, 0xA0, 0x0B, 0x30, 0xA0, 0xA0, 0x05, 0x2A, 0xA0, 0xA0, 0x07, 0x2C, 0xA0, 0xA0), + (0xE6, 0x00, 0x00, 0x33, 0x33), + (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0x2D, 0xA0, 0xA0, 0x0A, 0x2F, 0xA0, 0xA0, 0x04, 0x29, 0xA0, 0xA0, 0x06, 0x2B, 0xA0, 0xA0), + (0xEB, 0x00, 0x00, 0x4E, 0x4E, 0x00, 0x00, 0x00), + (0xEC, 0x08, 0x01), + (0xED, 0xB0, 0x2B, 0x98, 0xA4, 0x56, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x65, 0x4A, 0x89, 0xB2, 0x0B), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + ] +) +# fmt: on From 8607a0881d4f3d3b6fe064287710f6af3f3a16b6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:10:24 -0500 Subject: [PATCH 098/102] [core] Add support for passing yaml files to clean-all (#12039) --- esphome/__main__.py | 2 +- esphome/writer.py | 8 +++++++- tests/unit_tests/test_writer.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b0c081a34f..f8fb678cb2 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1319,7 +1319,7 @@ def parse_args(argv): "clean-all", help="Clean all build and platform files." ) parser_clean_all.add_argument( - "configuration", help="Your YAML configuration directory.", nargs="*" + "configuration", help="Your YAML file or configuration directory.", nargs="*" ) parser_dashboard = subparsers.add_parser( diff --git a/esphome/writer.py b/esphome/writer.py index b866a804b3..3124e9e12c 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -343,7 +343,13 @@ def clean_build(clear_pio_cache: bool = True): def clean_all(configuration: list[str]): import shutil - data_dirs = [Path(dir) / ".esphome" for dir in configuration] + data_dirs = [] + for config in configuration: + item = Path(config) + if item.is_file() and item.suffix in (".yaml", ".yml"): + data_dirs.append(item.parent / ".esphome") + else: + data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) if "ESPHOME_DATA_DIR" in os.environ: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index a4490fbbc0..a2a358f4d3 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -737,6 +737,37 @@ def test_write_cpp_with_duplicate_markers( write_cpp("// New code") +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_file( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with a .yaml file uses parent directory.""" + # Create config directory with yaml file + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Verify .esphome directory still exists but contents cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + # Verify logging mentions the build dir + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From 1f0a5e1eeab2d86031934a3f9c9e182458d60a5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:21:32 -0600 Subject: [PATCH 099/102] [logger] Reduce UART overhead on ESP32/ESP8266 and fix buffer truncation (#11927) --- esphome/components/logger/__init__.py | 2 + esphome/components/logger/logger.cpp | 21 ++++---- esphome/components/logger/logger.h | 53 +++++++++++++++++-- esphome/components/logger/logger_esp32.cpp | 32 ++++++----- esphome/components/logger/logger_esp8266.cpp | 5 +- esphome/components/logger/logger_host.cpp | 2 +- .../components/logger/logger_libretiny.cpp | 2 +- esphome/components/logger/logger_rp2040.cpp | 2 +- esphome/components/logger/logger_zephyr.cpp | 2 +- 9 files changed, 84 insertions(+), 37 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index cf78e6ae63..39877030e9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -365,8 +365,10 @@ async def to_code(config): if CORE.is_esp32: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9a9bf89fe3..9803bf528c 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer); + // Add newline if platform needs it (ESP32 doesn't add via write_msg_) + this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + this->write_msg_(console_buffer, buffer_at); } // Reset the recursion guard for this task @@ -131,18 +133,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas // Save the offset before calling format_log_to_buffer_with_terminator_ // since it will increment tx_buffer_at_ to the end of the formatted string - uint32_t msg_start = this->tx_buffer_at_; + uint16_t msg_start = this->tx_buffer_at_; this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Write to console and send callback starting at the msg_start - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_ + msg_start); - } - size_t msg_length = + uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + + // Callbacks get message first (before console write) this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + // Write to console starting at the msg_start + this->write_tx_buffer_to_console_(msg_start, &msg_length); + global_recursion_guard_ = false; } #endif // USE_STORE_LOG_STR_IN_FLASH @@ -209,9 +212,7 @@ void Logger::process_messages_() { // This ensures all log messages appear on the console in a clean, serialized manner // Note: Messages may appear slightly out of order due to async processing, but // this is preferred over corrupted/interleaved console output - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); - } + this->write_tx_buffer_to_console_(); } } else { // No messages to process, disable loop if appropriate diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index dc8e06e0c9..8ba3dacacb 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -71,6 +71,17 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Platform-specific: does write_msg_ add its own newline? +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266) +// Allows single write call with newline included for efficiency +// true: write_msg_ adds newline itself via puts()/println() (other platforms) +// Newline should NOT be added to buffer +#if defined(USE_ESP32) || defined(USE_ESP8266) +static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; +#else +static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; +#endif + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -173,7 +184,7 @@ class Logger : public Component { protected: void process_messages_(); - void write_msg_(const char *msg); + void write_msg_(const char *msg, size_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) @@ -200,6 +211,35 @@ class Logger : public Component { } } + // Helper to add newline to buffer for platforms that need it + // Modifies buffer_at to include the newline + inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + if constexpr (!WRITE_MSG_ADDS_NEWLINE) { + // Add newline - don't need to maintain null termination + // write_msg_ now always receives explicit length, so we can safely overwrite the null terminator + // This is safe because: + // 1. Callbacks already received the message (before we add newline) + // 2. write_msg_ receives the length explicitly (doesn't need null terminator) + if (*buffer_at < buffer_size) { + buffer[(*buffer_at)++] = '\n'; + } else if (buffer_size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + buffer[buffer_size - 1] = '\n'; + *buffer_at = buffer_size; + } + } + } + + // Helper to write tx_buffer_ to console if logging is enabled + // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null + inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { + if (this->baud_rate_ > 0) { + uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; + this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); + this->write_msg_(this->tx_buffer_ + offset, *len_ptr); + } + } + // Helper to format and send a log message to both console and callbacks inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { @@ -208,10 +248,11 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console - } + // Callbacks get message WITHOUT newline (for API/MQTT/syslog) this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + + // Console gets message WITH newline (if platform needs it) + this->write_tx_buffer_to_console_(); } // Write the body of the log message to the buffer @@ -425,7 +466,9 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - uint16_t formatted_len = (ret >= remaining) ? remaining : ret; + // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 7fc79e6f54..32ef752462 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -121,25 +121,23 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { - if ( -#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC) - this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Length is now always passed explicitly - no strlen() fallback needed + +#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) + // USB CDC/JTAG - single write including newline (already in buffer) + // Use fwrite to stdout which goes through VFS to USB console + // + // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). + // They are ONLY defined when the user explicitly selects USB as the logger output in their config. + // This is compile-time selection, not runtime detection - if USB is configured, it's always used. + // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility + // to configure correctly for their hardware. This approach eliminates runtime overhead. + fwrite(msg, 1, len, stdout); #else - /* DISABLES CODE */ (false) // NOLINT + // Regular UART - single write including newline (already in buffer) + uart_write_bytes(this->uart_num_, msg, len); #endif - ) { - puts(msg); - } else { - // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen - size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); - uart_write_bytes(this->uart_num_, msg, len); - uart_write_bytes(this->uart_num_, "\n", 1); - } } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 5063d88b92..0fc73b747a 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -33,7 +33,10 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) + this->hw_serial_->write(msg, len); +} const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 4abe92286a..c5e1e6f865 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { time_t rawtime; struct tm *timeinfo; char buffer[80]; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 3edfa74480..b8017b841d 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 63727c2cda..4a8535c8e4 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index fb0c7dcca3..ec2ff3013c 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -62,7 +62,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { #ifdef CONFIG_PRINTK printk("%s\n", msg); #endif From 056b4375ebe238250675081613dcb00389e3254e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:21:47 -0600 Subject: [PATCH 100/102] [api] Reduce heap allocations in DeviceInfoResponse (#11952) --- esphome/components/api/api_connection.cpp | 12 ++++++++---- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 ++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c60680ae43..04221a237b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1451,8 +1451,11 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_AREAS resp.set_suggested_area(StringRef(App.get_area())); #endif - // mac_address must store temporary string - will be valid during send_message call - std::string mac_address = get_mac_address_pretty(); + // Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char mac_address[18]; + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, mac_address); resp.set_mac_address(StringRef(mac_address)); resp.set_esphome_version(ESPHOME_VERSION_REF); @@ -1493,8 +1496,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #endif #ifdef USE_BLUETOOTH_PROXY resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); - // bt_mac must store temporary string - will be valid during send_message call - std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + // Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char bluetooth_mac[18]; + bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac); resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a5f0fbe32f..4de541fac2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -130,11 +130,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ return flags; } - std::string get_bluetooth_mac_address_pretty() { + void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + format_mac_addr_upper(mac, output.data()); } protected: From 426734beef724112b37037641c7ea7c20f044082 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:22:01 -0600 Subject: [PATCH 101/102] [web_server_base] Replace shared_ptr with unique_ptr for AsyncWebServer (#11984) --- esphome/components/web_server_base/web_server_base.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 039a452d64..fbf0d00c06 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -111,7 +111,7 @@ class WebServerBase : public Component { this->initialized_++; return; } - this->server_ = std::make_shared(this->port_); + this->server_ = std::make_unique(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); @@ -127,7 +127,7 @@ class WebServerBase : public Component { this->server_ = nullptr; } } - std::shared_ptr get_server() const { return server_; } + AsyncWebServer *get_server() const { return this->server_.get(); } float get_setup_priority() const override; #ifdef USE_WEBSERVER_AUTH @@ -143,7 +143,7 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::shared_ptr server_{nullptr}; + std::unique_ptr server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; From 3c48e13c9f5cf391e775174748c7da77928e9b8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:22:13 -0600 Subject: [PATCH 102/102] [ethernet] Conditionally compile manual_ip to save 24 bytes RAM (#11832) --- esphome/components/ethernet/__init__.py | 1 + esphome/components/ethernet/ethernet_component.cpp | 12 ++++++++++-- esphome/components/ethernet/ethernet_component.h | 4 ++++ esphome/core/defines.h | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 2f02d227d7..b4d67635c1 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -383,6 +383,7 @@ async def to_code(config): cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) # Add compile-time define for PHY types with specific code diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index cad963b299..9a46aa2687 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -553,11 +553,14 @@ void EthernetComponent::start_connect_() { } esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { info.ip = this->manual_ip_->static_ip; info.gw = this->manual_ip_->gateway; info.netmask = this->manual_ip_->subnet; - } else { + } else +#endif + { info.ip.addr = 0; info.gw.addr = 0; info.netmask.addr = 0; @@ -578,6 +581,7 @@ void EthernetComponent::start_connect_() { err = esp_netif_set_ip_info(this->eth_netif_, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { LwIPLock lock; if (this->manual_ip_->dns1.is_set()) { @@ -590,7 +594,9 @@ void EthernetComponent::start_connect_() { d = this->manual_ip_->dns2; dns_setserver(1, &d); } - } else { + } else +#endif + { err = esp_netif_dhcpc_start(this->eth_netif_); if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); @@ -688,7 +694,9 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } +#ifdef USE_ETHERNET_MANUAL_IP void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } +#endif // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f1f0ac9cb8..bffed4dc4a 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -82,7 +82,9 @@ class EthernetComponent : public Component { void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP void set_manual_ip(const ManualIP &manual_ip); +#endif void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } network::IPAddresses get_ip_addresses(); @@ -137,7 +139,9 @@ class EthernetComponent : public Component { uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; #endif +#ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; +#endif uint32_t connect_begin_; // Group all uint8_t types together (enums and bools) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 03362ce07a..5e7f51e04c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -216,6 +216,7 @@ #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_MANUAL_IP #endif #ifdef USE_ESP_IDF